返回 DeepSeek-Reasonix
file-navigation-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / file-navigation-lifecycle.test.tsx
1 // Run: node --import ./scripts/svg-stub-register.mjs --import tsx src/__tests__/file-navigation-lifecycle.test.tsx
2 //
3 // The historical defect was a request object rebuilt during render and merged
4 // into the panel's props: every render produced a new object, the panel wrote
5 // state from it, and the pair looped until React reported #301. This drives the
6 // real dock chain and asserts the replacement behaves: a navigation happens
7 // once, re-renders neither navigate nor read, and a StrictMode mount does not
8 // double a read.
9
10 import assert from "node:assert/strict";
11 import React, { StrictMode, act } from "react";
12 import { renderFilesWorkspace, waitFor, flushPromises } from "./workspace-panel-test-harness";
13 import { WorkspaceDockRegion, type WorkspaceDockRegionProps } from "../app-shell/WorkspaceDockRegion";
14 import { PresentedFiles } from "../components/PresentedFiles";
15 import { LocaleProvider } from "../lib/i18n";
16 import { performResourceAction } from "../lib/fileNavigationCommands";
17 import { fileNavigationOwner } from "../lib/fileNavigationCommands";
18 import { fileNavigationKey } from "../lib/fileNavigationOwner";
19 import { useActivityBarStore } from "../store/activityBar";
20
21 const reads: string[] = [];
22 const preview = (path: string) => {
23 reads.push(path);
24 return { path, body: `CONTENT ${path}`, size: 12, truncated: false, binary: false };
25 };
26 const { dom, root, dockTabId } = await renderFilesWorkspace({
27 ReadFileForTab: async (_tab, path) => preview(path),
28 ReadPresentedFileForTab: async (_tab, _tool, path) => preview(path),
29 ReadPresentedFileSourceForTab: async (_tab, _tool, path) => preview(path),
30 });
31 const props: WorkspaceDockRegionProps = {
32 visible: true, overlay: false, mode: "files", showContext: false,
33 t: key => key, onPickEntry: () => {}, remote: { onClose: () => {} }, context: {} as WorkspaceDockRegionProps["context"],
34 workspace: { open: true, tabId: "tab-a", cwd: "/repo", maximized: false, onClose: () => {}, onToggleMaximized: () => {} },
35 workspaceKey: "lifecycle", workspaceRoot: "/repo", fileNavigation: fileNavigationOwner(),
36 };
37 let paints = 0;
38 function Lifecycle({ generation }: { generation: number }) {
39 paints += 1;
40 return <>
41 <PresentedFiles tabId="tab-a" hostId="local" files={[{ path: "app.ts", toolCallId: "call", description: "d" }]} />
42 <span data-generation={generation} />
43 <WorkspaceDockRegion {...props} />
44 </>;
45 }
46 const record = () => fileNavigationOwner().getSnapshot(fileNavigationKey({ sessionTabId: "tab-a", dockTabId }));
47 const paint = (generation: number) => act(async () => {
48 root.render(<StrictMode><LocaleProvider><Lifecycle generation={generation} /></LocaleProvider></StrictMode>);
49 await flushPromises();
50 });
51
52 // StrictMode replays mount effects; the dock must still open the file once.
53 await paint(0);
54 await act(async () => {
55 document.querySelector<HTMLButtonElement>(".presented-file__main")
56 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
57 await flushPromises();
58 });
59 await waitFor("presented preview", () => document.body.textContent?.includes("CONTENT app.ts") === true);
60 const afterOpen = record()!;
61 assert.equal(afterOpen.selected?.resource.path, "app.ts");
62 assert.equal(reads.length, 1, `one command reads the file once, got ${JSON.stringify(reads)}`);
63 assert.equal(afterOpen.navigation?.revision, 1, "one command is one navigation revision");
64
65 // Repeated parent renders must not navigate, read again, or loop. StrictMode
66 // renders each commit twice, so the bound is two paints per driven render; a
67 // render loop would overshoot it by orders of magnitude.
68 const readsAfterOpen = reads.length;
69 const paintsAfterOpen = paints;
70 for (let generation = 1; generation <= 12; generation += 1) await paint(generation);
71 assert.equal(reads.length, readsAfterOpen, `parent re-renders must not read again, got ${JSON.stringify(reads)}`);
72 assert.equal(record()!.navigation?.revision, 1, "parent re-renders never advance a navigation revision");
73 assert(paints - paintsAfterOpen <= 24, `twelve driven renders paint at most twice each, got ${paints - paintsAfterOpen}`);
74
75 // A second click on the same row re-delivers the navigation but not the read.
76 await act(async () => {
77 document.querySelector<HTMLButtonElement>(".presented-file__main")
78 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
79 await flushPromises();
80 });
81 await waitFor("repeat preview", () => record()!.navigation!.revision === 2);
82 assert.equal(reads.length, readsAfterOpen, "reopening the same file in the same mode keeps the valid read");
83
84 // A remount restores the committed selection without replaying the command.
85 await act(async () => { root.render(<StrictMode><LocaleProvider><span /></LocaleProvider></StrictMode>); await flushPromises(); });
86 const readsAfterUnmount = reads.length;
87 await paint(13);
88 await waitFor("restored preview", () => document.body.textContent?.includes("CONTENT app.ts") === true);
89 assert.equal(record()!.navigation?.revision, 2, "a remount restores the record instead of issuing a command");
90 assert.equal(reads.length, readsAfterUnmount + 1, "a remount reads the restored selection exactly once");
91 assert.equal(record()!.dockTabId, useActivityBarStore.getState().activeTabId);
92
93 await act(async () => root.unmount());
94 dom.window.close();
95 console.log("PASS dock navigation is command-driven: no render-time requests, no re-render reads, no StrictMode replay");
96
96 lines Plain Text