返回 DeepSeek-Reasonix
file-navigation-dock.test.tsx
根目录 / desktop / frontend / src / __tests__ / file-navigation-dock.test.tsx
1 // Run: node --import ./scripts/svg-stub-register.mjs --import tsx src/__tests__/file-navigation-dock.test.tsx
2 //
3 // The preview-tab experience behind the navigation record: reopening activates
4 // the existing tab, the cap and its ordering are kept, source mode reuses the
5 // tab, a reveal expands the tree, and closing then reopening the dock tab is a
6 // new lifecycle generation rather than a restored one.
7
8 import assert from "node:assert/strict";
9 import React, { act } from "react";
10 import { renderFilesWorkspace, waitFor } from "./workspace-panel-test-harness";
11 import { WorkspaceDockRegion, type WorkspaceDockRegionProps } from "../app-shell/WorkspaceDockRegion";
12 import { LocaleProvider } from "../lib/i18n";
13 import { performResourceAction } from "../lib/fileNavigationCommands";
14 import { fileNavigationOwner } from "../lib/fileNavigationCommands";
15 import { FILE_PREVIEW_LIMIT, fileNavigationKey } from "../lib/fileNavigationOwner";
16 import { useActivityBarStore } from "../store/activityBar";
17
18 const reads: string[] = [];
19 const { dom, root, dockTabId } = await renderFilesWorkspace({
20 ReadPresentedFileForTab: async (_tab, _tool, path) => {
21 reads.push(`preview:${path}`);
22 return { path, body: `preview content ${path}`, size: 24, truncated: false, binary: false };
23 },
24 ReadPresentedFileSourceForTab: async (_tab, _tool, path) => {
25 reads.push(`source:${path}`);
26 return { path, body: `source content ${path}`, size: 24, truncated: false, binary: false };
27 },
28 ListDirForTab: async (_tab, dir) => dir === "" ? [{ name: "app.ts", isDir: false }] : [],
29 });
30 const props: WorkspaceDockRegionProps = {
31 visible: true, overlay: false, mode: "files", showContext: false,
32 t: key => key, onPickEntry: () => {}, remote: { onClose: () => {} }, context: {} as WorkspaceDockRegionProps["context"],
33 workspace: { open: true, tabId: "navigation-session", cwd: "/repo", maximized: false, onClose: () => {}, onToggleMaximized: () => {} },
34 workspaceKey: "tabs-test", workspaceRoot: "/repo",
35 };
36 let sessionTabId = "navigation-session";
37 const paint = (visible: boolean) => act(async () => root.render(
38 <LocaleProvider><WorkspaceDockRegion {...props} workspace={{ ...props.workspace, tabId: sessionTabId }} visible={visible} /></LocaleProvider>,
39 ));
40 const record = () => fileNavigationOwner().getSnapshot(fileNavigationKey({ sessionTabId, dockTabId }));
41 const paths = () => record()?.entries.map((entry) => entry.resource.path) ?? [];
42 const open = async (path: string, action: "preview" | "source" | "reveal-tree" = "preview") => {
43 await act(async () => performResourceAction({ hostId: "local", tabId: sessionTabId, path, source: "presented", toolCallId: "call" }, action));
44 await paint(true);
45 };
46 await paint(true);
47
48 // ── Consecutive A / B / A: one tab each, most recent last ──
49 await open("a.md");
50 await open("b.md");
51 await open("a.md");
52 assert.deepEqual(paths(), ["b.md", "a.md"], "reopening activates the existing tab instead of adding one");
53 await waitFor("reactivated preview", () => document.body.textContent?.includes("preview content a.md") === true);
54
55 // ── Source mode reuses the tab and reads the other representation ──
56 const sourceToggle = [...document.querySelectorAll<HTMLButtonElement>(".workspace-preview__window-actions button")]
57 .find(button => /source/i.test(button.getAttribute("aria-label") ?? ""));
58 assert(sourceToggle, "a presented preview exposes its source toggle");
59 await act(async () => { sourceToggle!.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); });
60 await waitFor("source preview", () => document.body.textContent?.includes("source content a.md") === true);
61 assert.deepEqual(paths(), ["b.md", "a.md"], "switching the mode reuses the file tab");
62 assert(reads.includes("source:a.md"), "the source read used the source entry point");
63
64 // ── reveal-tree expands the rail and locates the file ──
65 const hideTree = [...document.querySelectorAll<HTMLButtonElement>("button")]
66 .find(button => /hide file tree/i.test(button.getAttribute("aria-label") ?? ""));
67 if (hideTree) await act(async () => { hideTree.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); });
68 await open("a.md", "reveal-tree");
69 assert(document.querySelector(".workspace-tree") !== null, "reveal-tree shows the file tree");
70 assert.equal(document.querySelector('[data-workspace-path="app.ts"]') !== null, true, "reveal-tree lists the workspace file");
71
72 // ── The preview tab list keeps its cap and its order ──
73 for (const path of ["c.txt", "d.txt", "e.txt", "f.txt", "g.txt"]) await open(path);
74 assert.equal(paths().length, FILE_PREVIEW_LIMIT, "the dock keeps the preview tab limit");
75 assert.deepEqual(paths(), ["c.txt", "d.txt", "e.txt", "f.txt", "g.txt"], "the cap keeps the most recently used tabs in order");
76 assert.equal(document.querySelectorAll(".workspace-document-tab").length, FILE_PREVIEW_LIMIT, "the tab strip shows the same list");
77 const readsBeforeRepeat = reads.length;
78 await open("c.txt");
79 assert.deepEqual(paths(), ["d.txt", "e.txt", "f.txt", "g.txt", "c.txt"], "reopening moves the tab to the most recent position");
80 assert(reads.length > readsBeforeRepeat, "activating a tab reads it again for this dock");
81
82 // ── Closing the dock tab and reopening it is a new lifecycle generation ──
83 const closedGeneration = record()!.generation;
84 await act(async () => {
85 useActivityBarStore.getState().closeTab(dockTabId);
86 // The runtime keeps the open dock tabs of the active session retained; a
87 // standalone region mount has no runtime, so the test reconciles for it.
88 fileNavigationOwner().retain([]);
89 await Promise.resolve();
90 });
91 await paint(false);
92 assert.equal(record(), null, "a closed dock tab keeps no record");
93 // Reopening from the recently-closed list reuses the same tab id.
94 await act(async () => useActivityBarStore.getState().reopenTab(dockTabId));
95 assert.equal(useActivityBarStore.getState().activeTabId, dockTabId, "the reopened tab reuses its id");
96 await paint(true);
97 await act(async () => performResourceAction({ hostId: "local", tabId: "navigation-session", path: "h.txt", source: "presented", toolCallId: "call" }, "preview"));
98 await paint(true);
99 const reopened = record()!;
100 assert(reopened.generation > closedGeneration, "a reopened dock tab starts a new lifecycle generation");
101 // The remembered paths come back, but only as workspace files: the presented
102 // tool scope an earlier session opened them with is never restored.
103 const rememberedEntries = reopened.entries.filter(entry => entry.resource.path !== "h.txt");
104 assert(rememberedEntries.length > 0, "the reopened dock restores the remembered paths");
105 assert(rememberedEntries.every(entry => entry.resource.access.source === "workspace" && entry.resource.access.toolCallId === undefined),
106 "restored entries carry workspace access only");
107 assert.equal(reopened.selected?.resource.path, "h.txt");
108 assert.equal(reopened.selected?.resource.access.source, "presented", "the new command's own access context is used");
109
110 // ── Collapsing the dock keeps the record; expanding restores it ──
111 await paint(false);
112 await paint(true);
113 const expanded = record()!;
114 assert.equal(expanded.generation, reopened.generation, "a collapse is not a new lifecycle");
115 assert.deepEqual(expanded.entries.map(entry => entry.resource.path), reopened.entries.map(entry => entry.resource.path),
116 "expanding restores the committed previews");
117 assert.equal(expanded.selected?.resource.path, "h.txt", "expanding restores the committed selection");
118 assert.equal(expanded.navigation?.revision, reopened.navigation?.revision, "restoring never replays the command that opened it");
119
120 await act(async () => root.unmount());
121 dom.window.close();
122 console.log("PASS dock preview tabs: reuse, cap, source mode, reveal, lifecycle generations, collapse restore and first navigation per session");
123
123 lines Plain Text