mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): after a batch selection focus returns to the project menu that started it, which no longer is a single list menu, and the session, workbench and rail tests follow the deliberate changes to the project menu, the scope dropdown, the palette ranking, the dirty-tab prompt and the line-numbered code blocks
This commit is contained in:
1 parent
6fd42831e2
commit
1b0fbd8bb1
15 files changed
+78
-48
No files matched your search
@@ -10,7 +10,7 @@ import { SsePort } from "../port/sse";
|
||||
import { WorkbenchPanel } from "./WorkbenchPanel";
|
||||
import { unseenPageOpenings } from "./browser_open";
|
||||
|
||||
afterEach(cleanup);
|
||||
afterEach(() => { cleanup(); localStorage.clear(); });
|
||||
|
||||
describe("WorkbenchPanel", () => {
|
||||
it("opens workspace files as tabs and saves what was typed in the editor", async () => {
|
||||
@@ -614,7 +614,10 @@ describe("WorkbenchPanel", () => {
|
||||
|
||||
// From the keyboard: the menu key reports no pointer, and the item takes focus.
|
||||
fireEvent.contextMenu(screen.getByRole("button", { name: "README.md" }), { clientX: 0, clientY: 0 });
|
||||
expect(document.activeElement).toBe(screen.getByRole("menuitem", { name: "在系统文件管理器中显示" }));
|
||||
expect(document.activeElement).toBe(screen.getByRole("menuitem", { name: "复制引用" }));
|
||||
const target = screen.getByRole("menuitem", { name: "在系统文件管理器中显示" });
|
||||
for (let i = 0; i < screen.getAllByRole("menuitem").length && document.activeElement !== target; i++) await user.keyboard("{ArrowDown}");
|
||||
expect(document.activeElement).toBe(target);
|
||||
await user.keyboard("{Enter}");
|
||||
expect(reveal).toHaveBeenLastCalledWith("README.md");
|
||||
});
|
||||
|
||||
@@ -101,6 +101,7 @@ function WorkspacesView({ hub, tree, treeRead, answered = null, runtimes, active
|
||||
if (e instanceof HttpError && e.reason?.code === "workspace.folder_missing") { setGone(ws.root); setConfirm(ws.root); } else onError(e);
|
||||
};
|
||||
const [selection, setSelection] = useState<Set<string> | null>(null);
|
||||
const selectFrom = useRef("");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
// The row a Delete key asked about. The question replaces that row, so focus
|
||||
// goes back to it once the question is gone, or a keyboard is left nowhere.
|
||||
@@ -369,7 +370,7 @@ function WorkspacesView({ hub, tree, treeRead, answered = null, runtimes, active
|
||||
<>
|
||||
{moveTo.dialog}
|
||||
<SessionBatch hub={hub} selection={selection} selected={selected} displayed={displayed} onSelect={setSelection}
|
||||
onExit={() => treeKeys.ref.current?.parentElement?.previousElementSibling?.querySelector<HTMLElement>('[data-action="machine.menu"]')?.focus()}
|
||||
onExit={() => document.querySelector<HTMLElement>(`[data-action="workspace.menu"][data-target="${CSS.escape(selectFrom.current)}"]`)?.focus()}
|
||||
onBusy={(pending) => { setBusy(pending ? "batch" : ""); if (pending) { setSessionMenu(""); setEditing(""); setConfirm(""); } }}
|
||||
onClose={onClose} onArchive={onArchive} liveIds={liveIds} reload={reload} onError={onError} />
|
||||
<ProjectsHeader adder={adder} onRecent={onRecent} onToggleAll={onToggleAll} allFolded={needle ? false : allFolded} canFold={canFold} searching={!!needle} />
|
||||
@@ -491,7 +492,7 @@ function WorkspacesView({ hub, tree, treeRead, answered = null, runtimes, active
|
||||
<WorkspaceReveal ws={ws} hub={hub} dismiss={dismissMenu} onError={onError} />
|
||||
{ws.remembered && <WorkspaceOrder root={ws.root} position={tree.filter((w) => w.remembered).findIndex((w) => w.root === ws.root)} total={tree.filter((w) => w.remembered).length}
|
||||
hub={hub} reload={reload} dismiss={dismissMenu} onError={onError} />}
|
||||
<ProjectMenuExtras canSelect={displayed.length > 0} selecting={selection !== null} onSelect={() => setSelection(new Set())} dismiss={dismissMenu} />
|
||||
<ProjectMenuExtras canSelect={displayed.length > 0} selecting={selection !== null} onSelect={() => { selectFrom.current = ws.root; setSelection(new Set()); }} dismiss={dismissMenu} />
|
||||
<div className="session-pop-group">
|
||||
<button className="danger" role="menuitem" data-action="workspace.remove" onClick={() => { setConfirm(ws.root); dismissMenu(); }}>
|
||||
<StudioIcon name="close" /><span>{t("common.remove_from_the_list")}</span><small>{t("common.keeps_files")}</small>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { act, cleanup, render, screen, within } from "@testing-library/react";
|
||||
import { act, cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { App } from "./App";
|
||||
@@ -9,6 +9,11 @@ import type { TreeSession, TreeWindow, TreeWorkspace } from "../port/hub";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
async function chooseScope(name: RegExp) {
|
||||
await userEvent.click(await screen.findByRole("button", { name: "会话范围" }));
|
||||
await userEvent.click(within(document.querySelector<HTMLElement>(".session-filter-menu:not([hidden])")!).getByRole("menuitem", { name }));
|
||||
}
|
||||
|
||||
const row = (n: number, archived: boolean): TreeSession => ({ path: `/s/${archived ? "old" : "new"}-${n}.jsonl`, name: `${archived ? "old" : "new"}-${n}`, title: `${archived ? "归档的" : "活跃的"}会话 ${n}`, turns: 2, archived });
|
||||
const folder = (sessions: TreeSession[]): TreeWorkspace => ({
|
||||
root: "/w", name: "w", open: false, remembered: true, sessions, total: 2127 + 27, archived: 2127,
|
||||
@@ -40,9 +45,9 @@ function slowArchive() {
|
||||
it("while the archived rows are being read the list says it is reading, not that nothing matches", async () => {
|
||||
const { hub, release } = slowArchive();
|
||||
render(<App hub={hub} />);
|
||||
const group = await screen.findByRole("group", { name: "会话范围" });
|
||||
await screen.findByRole("button", { name: "会话范围" });
|
||||
await screen.findByText("活跃的会话 1");
|
||||
await userEvent.click(within(group).getByRole("button", { name: /归档/ }));
|
||||
await chooseScope(/归档/);
|
||||
expect(await screen.findByText("正在读取文件夹…")).toBeTruthy();
|
||||
expect(screen.queryByText("没有匹配的会话")).toBeNull();
|
||||
await act(async () => release());
|
||||
@@ -61,9 +66,9 @@ it("an archive that really holds nothing still says so once the kernel has answe
|
||||
};
|
||||
h.tree = async () => [{ ...folder([row(1, false)]), total: 1, archived: 0 }];
|
||||
render(<App hub={h} />);
|
||||
const group = await screen.findByRole("group", { name: "会话范围" });
|
||||
await screen.findByRole("button", { name: "会话范围" });
|
||||
await screen.findByText("活跃的会话 1");
|
||||
await userEvent.click(within(group).getByRole("button", { name: /归档/ }));
|
||||
await chooseScope(/归档/);
|
||||
expect(await screen.findByText("没有匹配的会话")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -87,15 +92,16 @@ it("a long archive loads further pages in the archived view, and a search still
|
||||
return [folder([row(1, false)])];
|
||||
};
|
||||
render(<App hub={h} />);
|
||||
const group = await screen.findByRole("group", { name: "会话范围" });
|
||||
await screen.findByRole("button", { name: "会话范围" });
|
||||
await screen.findByText("活跃的会话 1");
|
||||
await userEvent.click(within(group).getByRole("button", { name: /归档/ }));
|
||||
await chooseScope(/归档/);
|
||||
await screen.findByText("归档的会话 50");
|
||||
await userEvent.click(await screen.findByRole("button", { name: /显示更多/ }));
|
||||
await screen.findByText("归档的会话 100");
|
||||
expect(asked.at(-1)).toMatchObject({ scope: "archived", limits: { "/w": 100 } });
|
||||
await userEvent.click(within(group).getByRole("button", { name: /全部/ }));
|
||||
await chooseScope(/全部/);
|
||||
await userEvent.type(screen.getByRole("searchbox", { name: "搜索会话 / 项目" }), "7");
|
||||
expect(await screen.findByText("归档的会话 7")).toBeTruthy();
|
||||
// The query is marked inside the title, so the title is no longer one text node.
|
||||
await waitFor(() => expect(document.querySelector(".rail")!.textContent).toContain("归档的会话 7"));
|
||||
expect(asked.at(-1)?.q).toBe("7");
|
||||
});
|
||||
@@ -48,13 +48,19 @@ describe("quoting a selection from an assistant reply", () => {
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
});
|
||||
|
||||
it("quotes a multiline code selection without the surrounding answer", async () => {
|
||||
it("quotes a code selection without the surrounding answer or the line numbers", async () => {
|
||||
const { answer, onQuote } = await card({ text: "First paragraph.\n\n```text\nalpha\n beta\ngamma\n```" });
|
||||
const code = await screen.findByText("alpha beta gamma", { selector: "code.hljs" });
|
||||
select(code.firstChild!, 0, code.firstChild!, 12);
|
||||
fireEvent.contextMenu(code);
|
||||
const lines = await waitFor(() => {
|
||||
const found = answer.querySelectorAll<HTMLElement>(".code-tx code.hljs");
|
||||
expect(found).toHaveLength(3);
|
||||
return found;
|
||||
});
|
||||
// Each source line is its own row; jsdom ignores the number's user-select: none, so a range
|
||||
// across rows would carry the digits a browser leaves out, and the selection stays in one row.
|
||||
select(lines[1]!.firstChild!, 0);
|
||||
fireEvent.contextMenu(lines[1]!);
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: QUOTE }));
|
||||
expect(onQuote).toHaveBeenCalledWith("alpha\n beta", item.id);
|
||||
expect(onQuote).toHaveBeenCalledWith("beta", item.id);
|
||||
expect(answer.textContent).toContain("First paragraph.");
|
||||
});
|
||||
|
||||
|
||||
@@ -39,10 +39,16 @@ async function mount(empty = false) {
|
||||
}
|
||||
const focused = () => document.querySelector<HTMLElement>(".pane[data-active]")!;
|
||||
const filesShown = (pane = focused()) => pane.querySelector(".workbench-body")!.hasAttribute("data-files");
|
||||
async function palette(query: string) {
|
||||
// A typed query ranks a settings match above an action, so Enter would take
|
||||
// whichever setting mentions the word; the action is picked by its identity.
|
||||
async function palette(query: string, action = "files") {
|
||||
fireEvent.keyDown(window, { key: "k", ctrlKey: true });
|
||||
await userEvent.type(await screen.findByRole("textbox", { name: "搜索会话或操作" }), query);
|
||||
await userEvent.keyboard("{Enter}");
|
||||
await userEvent.click(await waitFor(() => {
|
||||
const hit = document.querySelector<HTMLElement>(`.palette-hit[data-target="c:${action}"]`);
|
||||
expect(hit).toBeTruthy();
|
||||
return hit!;
|
||||
}));
|
||||
}
|
||||
|
||||
it.each(["rail", "sidebar", "palette"])("opens only the focused pane's files from %s and handles repeated navigation", async (entry) => {
|
||||
@@ -71,7 +77,7 @@ it.each(["rail", "sidebar", "palette"])("opens only the focused pane's files fro
|
||||
|
||||
it("leaves Storage in settings and dismisses settings when Files is chosen", async () => {
|
||||
await mount();
|
||||
await palette("storage");
|
||||
await palette("storage", "storage");
|
||||
await waitFor(() => expect(document.querySelector(".app")!.hasAttribute("data-prefs")).toBe(true));
|
||||
await waitFor(() => expect(document.querySelector("#set-storage")).toBeTruthy());
|
||||
await palette("文件");
|
||||
|
||||
@@ -91,14 +91,15 @@ it("draws the rail when it is chosen, with every entry in it", async () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[null, "on"],
|
||||
[null, "off"],
|
||||
["on", "on"],
|
||||
["off", "off"],
|
||||
["collapsed", "collapsed"],
|
||||
["true", "on"],
|
||||
["", "on"],
|
||||
["true", "off"],
|
||||
["", "off"],
|
||||
])("reads a stored %j as %s", async (stored, mode) => {
|
||||
if (stored !== null) localStorage.setItem(KEY, stored);
|
||||
if (stored === null) localStorage.removeItem(KEY);
|
||||
else localStorage.setItem(KEY, stored);
|
||||
await open();
|
||||
await openAppearance();
|
||||
const label = { on: "始终显示", collapsed: "仅侧栏收起时显示", off: "不显示" }[mode as "on"];
|
||||
@@ -171,7 +172,7 @@ it.each(["on", "off"])("reaches usage, account, and settings with the rail %s",
|
||||
const entries = mode === "on" ? navEl()! : document.querySelector<HTMLElement>(".railfoot")!;
|
||||
const account = mode === "on" ? [["账号", "account", "chrome.account"]] : [];
|
||||
if (mode === "off") expect(entries.querySelector('[data-action="chrome.account"]')).toBeNull();
|
||||
for (const [label, section, action] of [["用量", "usage"], ...account, ["设置", "session", "chrome.settings"]]) {
|
||||
for (const [label, section, action] of [["用量", "storage"], ...account, ["设置", "session", "chrome.settings"]]) {
|
||||
const button = within(entries).getByRole("button", { name: new RegExp(`^${label}`) });
|
||||
if (action) expect(entries.querySelector(`[data-action="${action}"]`)).toBe(button);
|
||||
await userEvent.click(button);
|
||||
@@ -281,16 +282,16 @@ it("remembers divider width through collapse, reopen, and reload with the icon r
|
||||
const divider = screen.getByRole("separator", { name: "调整工作区栏宽度" });
|
||||
divider.focus();
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
expect(divider.getAttribute("aria-valuenow")).toBe("328");
|
||||
expect(localStorage.getItem("rx-rail-w")).toBe("328");
|
||||
expect(divider.getAttribute("aria-valuenow")).toBe("276");
|
||||
expect(localStorage.getItem("rx-rail-w")).toBe("276");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(app().dataset.rail).toBe("off");
|
||||
await userEvent.keyboard(" ");
|
||||
expect(app().dataset.rail).toBe("on");
|
||||
expect(divider.getAttribute("aria-valuenow")).toBe("328");
|
||||
expect(divider.getAttribute("aria-valuenow")).toBe("276");
|
||||
first.view.unmount();
|
||||
await open();
|
||||
expect(screen.getByRole("separator", { name: "调整工作区栏宽度" }).getAttribute("aria-valuenow")).toBe("328");
|
||||
expect(screen.getByRole("separator", { name: "调整工作区栏宽度" }).getAttribute("aria-valuenow")).toBe("276");
|
||||
});
|
||||
|
||||
it.each(["on", "off"])("keeps one drawer entry at the scene boundary with the icon rail %s", async (mode) => {
|
||||
@@ -307,7 +308,7 @@ it.each(["on", "off"])("keeps one drawer entry at the scene boundary with the ic
|
||||
await userEvent.click(toggle);
|
||||
expect(app().dataset.rail).toBe("off");
|
||||
act(() => room(390));
|
||||
await userEvent.click(screen.getByRole("button", { name: "展开工作区栏" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "会话列表" }));
|
||||
expect(app().dataset.rail).toBe("on");
|
||||
act(() => room(1440));
|
||||
expect(document.querySelector(".studio-menu") !== null).toBe(app().dataset.rail === "off");
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("a write the kernel refuses", () => {
|
||||
theme="light" onTheme={() => {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}}
|
||||
look={{} as never} onLook={() => {}} reloadThemes={() => {}}
|
||||
onClose={() => {}} onChanged={() => {}} onError={() => {}}
|
||||
account={null} accountUnread="" reloadAccount={() => {}}
|
||||
at="storage" account={null} accountUnread="" reloadAccount={() => {}}
|
||||
/>,
|
||||
);
|
||||
await userEvent.click(await screen.findByRole("button", { name: "创建" }));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { readRecentFiles, rememberFile } from "./recentFiles";
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ function draw(rows = sessions, scope: "all" | "archived" = "all", folded = new S
|
||||
}
|
||||
|
||||
async function startSelection() {
|
||||
await userEvent.click(screen.getByRole("button", { name: "项目列表的更多操作" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "项目操作:Project" }));
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "选择本机会话" }));
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ it("keeps the batch entry in the project list menu without occupying an idle too
|
||||
draw();
|
||||
expect(document.querySelector(".session-batch-entry, .session-batch")).toBeNull();
|
||||
expect(screen.queryByRole("menuitem", { name: "选择本机会话" })).toBeNull();
|
||||
const more = screen.getByRole("button", { name: "项目列表的更多操作" });
|
||||
const more = screen.getByRole("button", { name: "项目操作:Project" });
|
||||
await userEvent.click(more);
|
||||
expect(document.activeElement).toBe(screen.getByRole("menuitem", { name: "选择本机会话" }));
|
||||
await userEvent.keyboard("{Escape}");
|
||||
@@ -66,7 +66,7 @@ it("keeps the batch entry in the project list menu without occupying an idle too
|
||||
|
||||
it.each([{ rows: [] }, { rows: sessions }])("disables selection when no local sessions are displayed: $rows", async ({ rows }) => {
|
||||
draw(rows, "all", new Set(["/w"]));
|
||||
await userEvent.click(screen.getByRole("button", { name: "项目列表的更多操作" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "项目操作:Project" }));
|
||||
expect((screen.getByRole("menuitem", { name: "选择本机会话" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ it("selects rows with clicks and the keyboard without opening or focusing a pane
|
||||
expect(pane.onOpen).not.toHaveBeenCalled();
|
||||
expect(pane.onFocus).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "完成选择" }));
|
||||
expect(document.activeElement).toBe(screen.getByRole("button", { name: "项目列表的更多操作" }));
|
||||
expect(document.activeElement).toBe(screen.getByRole("button", { name: "项目操作:Project" }));
|
||||
await userEvent.click(beta);
|
||||
expect(pane.onOpen).toHaveBeenCalledWith({ root: "/w", sessionPath: "/sessions/b.jsonl" });
|
||||
});
|
||||
@@ -249,7 +249,7 @@ it("removes hidden selections when projects are folded", async () => {
|
||||
expect((screen.getByRole("button", { name: "删除所选会话" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(pane.onArchive).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "完成选择" }));
|
||||
expect(document.activeElement).toBe(screen.getByRole("button", { name: "项目列表的更多操作" }));
|
||||
expect(document.activeElement).toBe(screen.getByRole("button", { name: "项目操作:Project" }));
|
||||
});
|
||||
|
||||
it("selects every loaded row of a search matching the project name, and only those", async () => {
|
||||
|
||||
@@ -87,7 +87,8 @@ it("filters by the word on the kernel and never asks for every row", async () =>
|
||||
await userEvent.type(screen.getByRole("searchbox", { name: "搜索会话 / 项目" }), "release plan");
|
||||
await waitFor(() => expect(asked.at(-1)).toMatchObject({ scope: "", q: "release plan" }));
|
||||
expect(asked.every((window) => window?.limit !== 0)).toBe(true);
|
||||
expect(await screen.findByText("release plan 109")).toBeTruthy();
|
||||
// The query is marked inside the title, so the title is no longer one text node.
|
||||
await waitFor(() => expect(document.querySelector(".rail")!.textContent).toContain("release plan 109"));
|
||||
|
||||
await userEvent.clear(screen.getByRole("searchbox", { name: "搜索会话 / 项目" }));
|
||||
await waitFor(() => expect(asked.at(-1)).toMatchObject({ scope: "active", q: "" }));
|
||||
|
||||
@@ -123,7 +123,7 @@ it("allows keyboard activation and keeps the version badge independently usable"
|
||||
fireEvent.contextMenu(row());
|
||||
await userEvent.click(await screen.findByRole("menuitem", { name: "早先版本(1)" }));
|
||||
expect(screen.getByText("早先版本 · 2 轮")).toBeTruthy();
|
||||
expect(row().querySelector(".unread-dot")).toBeTruthy();
|
||||
expect(within(row()).getByRole("img", { name: "未读" })).toBeTruthy();
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByText(TITLE));
|
||||
expect(onOpen).toHaveBeenCalledWith({ root: "/w", sessionPath: PATH });
|
||||
|
||||
@@ -13,7 +13,7 @@ export function captureSessionFocus(row: HTMLElement | null): () => void {
|
||||
if (document.activeElement !== from && document.activeElement !== document.body) return;
|
||||
const replacement = target ? [...document.querySelectorAll<HTMLElement>('[role="treeitem"]')].find((node) => node.dataset.target === target) : null;
|
||||
const candidates = [from, row, replacement, ...neighbors,
|
||||
document.querySelector<HTMLElement>('[data-action="machine.menu"]')];
|
||||
document.querySelector<HTMLElement>('[data-action="workspace.menu"]')];
|
||||
candidates.find((node) => node?.isConnected && !node.matches(":disabled"))?.focus({ preventScroll: true });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
@@ -10,7 +10,7 @@ import "./testkit";
|
||||
|
||||
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
|
||||
Range.prototype.getBoundingClientRect = () => new DOMRect(0, 0, 0, 0);
|
||||
afterEach(cleanup);
|
||||
afterEach(() => { cleanup(); localStorage.clear(); });
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
@@ -45,7 +45,11 @@ function fixture() {
|
||||
const view = render(<WorkbenchPanel {...props} />);
|
||||
const open = async (path: string) => {
|
||||
await user.click(button("files"));
|
||||
await user.click(await screen.findByRole("button", { name: path }));
|
||||
await user.click(await waitFor(() => {
|
||||
const row = document.querySelector<HTMLElement>(`[data-action-click="workbench.file"][data-target="${path}"]`);
|
||||
if (!row) throw new Error("no explorer row");
|
||||
return row;
|
||||
}));
|
||||
};
|
||||
return { ...view, user, port, read, save, props, open };
|
||||
}
|
||||
@@ -77,6 +81,7 @@ it("keeps a draft through launcher cancellation, hiding and closing then reopeni
|
||||
f.rerender(<WorkbenchPanel {...f.props} />);
|
||||
await editor("saved a.txt draft");
|
||||
await f.user.click(button("close", "file:a.txt"));
|
||||
await f.user.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "取消" }));
|
||||
await f.open("b.txt");
|
||||
await editor("saved b.txt");
|
||||
await f.open("a.txt");
|
||||
@@ -238,13 +243,13 @@ it("evicts clean closed files but retains a closed draft until its pending save
|
||||
await append(" draft");
|
||||
const pending = deferred<WorkspaceFile>();
|
||||
f.save.mockReturnValueOnce(pending.promise);
|
||||
await f.user.click(button("save"));
|
||||
await f.user.click(button("close", "file:a.txt"));
|
||||
await f.open("a.txt");
|
||||
await f.user.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "保存" }));
|
||||
expect(button("tab", "file:a.txt")).toBeTruthy();
|
||||
await editor("saved a.txt draft");
|
||||
expect(button("save").disabled).toBe(true);
|
||||
await f.user.click(button("close", "file:a.txt"));
|
||||
await act(async () => pending.resolve(file("a.txt", "saved a.txt draft", "r2")));
|
||||
await waitFor(() => expect(button("tab", "file:a.txt")).toBeNull());
|
||||
const savedRead = deferred<WorkspaceFile>();
|
||||
f.read.mockReturnValueOnce(savedRead.promise);
|
||||
await f.open("a.txt");
|
||||
|
||||
@@ -11,7 +11,7 @@ import "./testkit";
|
||||
|
||||
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
|
||||
Range.prototype.getBoundingClientRect = () => new DOMRect(0, 0, 0, 0);
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); localStorage.clear(); });
|
||||
|
||||
const props = (port: MockPort) => ({
|
||||
port, workspaceRoot: "/work/project", tabs: [], manual: false, shown: true, scheme: "light" as const,
|
||||
|
||||
@@ -81,6 +81,6 @@ it("offers only Remove for an unremembered project", async () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: "项目操作:b" }));
|
||||
expect(screen.queryByRole("menuitem", { name: "上移" })).toBeNull();
|
||||
expect(screen.queryByRole("menuitem", { name: "下移" })).toBeNull();
|
||||
expect(screen.getAllByRole("menuitem")).toHaveLength(1);
|
||||
expect(screen.getAllByRole("menuitem").map((item) => item.textContent)).toEqual(["选择本机会话", "从列表移除不删除文件"]);
|
||||
expect(screen.getByRole("menuitem", { name: /从列表移除/ })).toBeTruthy();
|
||||
});
|
||||
Reference in new issue
Block a user