返回 DeepSeek-Reasonix
workspace-tree-memory.test.ts
根目录 / desktop / frontend / src / __tests__ / workspace-tree-memory.test.ts
1 // Run: tsx src/__tests__/workspace-tree-memory.test.ts
2
3 import { JSDOM } from "jsdom";
4 import {
5 flushWorkspaceTreeMemory,
6 readWorkspaceTreeMemory,
7 rememberWorkspaceTreeScroll,
8 rememberWorkspaceTreeState,
9 resetWorkspaceTreeMemoryForTests,
10 } from "../lib/workspaceTreeMemory";
11 import {
12 createWorkspaceTreePersistenceScheduler,
13 type WorkspaceTreePersistenceClock,
14 } from "../lib/workspaceTreePersistence";
15
16 let passed = 0;
17 function ok(value: boolean, label: string): void {
18 if (!value) throw new Error(label);
19 passed += 1;
20 process.stdout.write(` PASS ${label}\n`);
21 }
22
23 const dom = new JSDOM("<!doctype html>", { url: "http://localhost/" });
24 globalThis.localStorage = dom.window.localStorage;
25
26 console.log("\nversioned per-project workspace memory");
27 resetWorkspaceTreeMemoryForTests();
28
29 rememberWorkspaceTreeState("project-a", {
30 openDirs: new Set(["", "src/"]),
31 selectedFilePath: "src/App.tsx",
32 selectedChangePath: "src/store.ts",
33 treeWidth: 276,
34 treeWidthMode: "even",
35 scrollTop: 144,
36 dockTreeWidth: 320,
37 dockPreviewWidth: 640,
38 });
39 rememberWorkspaceTreeState("project-b", { selectedFilePath: "README.md", treeWidth: 220 });
40
41 const projectA = readWorkspaceTreeMemory("project-a");
42 const projectB = readWorkspaceTreeMemory("project-b");
43 ok(projectA?.selectedFilePath === "src/App.tsx", "restores the file selection independently");
44 ok(projectA?.selectedChangePath === "src/store.ts", "restores the change selection independently");
45 ok(projectA?.openDirs.has("src/") === true && projectA.scrollTop === 144, "restores expanded directories and tree scroll");
46 ok(projectA?.dockTreeWidth === 320 && projectA.dockPreviewWidth === 640, "restores both outer dock widths");
47 ok(projectB?.selectedFilePath === "README.md" && projectB.treeWidth === 220, "keeps project state isolated by key");
48
49 const persisted = JSON.parse(localStorage.getItem("reasonix.workspaceState.v2") ?? "null") as { version?: number } | null;
50 ok(persisted?.version === 2, "writes an explicit schema version");
51
52 let synchronousWrites = 0;
53 const originalStorage = globalThis.localStorage;
54 const countingStorage: Storage = {
55 get length() { return originalStorage.length; },
56 clear: () => originalStorage.clear(),
57 getItem: (key) => originalStorage.getItem(key),
58 key: (index) => originalStorage.key(index),
59 removeItem: (key) => originalStorage.removeItem(key),
60 setItem: (key, value) => {
61 synchronousWrites += 1;
62 originalStorage.setItem(key, value);
63 },
64 };
65 globalThis.localStorage = countingStorage;
66 for (let index = 0; index < 120; index += 1) rememberWorkspaceTreeScroll("project-a", 200 + index);
67 ok(synchronousWrites === 0, "keeps high-frequency scroll updates off the synchronous storage path");
68 ok(readWorkspaceTreeMemory("project-a")?.scrollTop === 319, "updates the in-memory scroll position immediately");
69 flushWorkspaceTreeMemory();
70 ok(synchronousWrites === 1, "coalesces 120 scroll updates into one durable write");
71 globalThis.localStorage = originalStorage;
72
73 let nextHandle = 1;
74 const frames = new Map<number, () => void>();
75 const timers = new Map<number, () => void>();
76 const fakeClock: WorkspaceTreePersistenceClock = {
77 requestFrame(callback) {
78 const handle = nextHandle++;
79 frames.set(handle, callback);
80 return handle;
81 },
82 cancelFrame(handle) {
83 frames.delete(handle);
84 },
85 setTimer(callback) {
86 const handle = nextHandle++;
87 timers.set(handle, callback);
88 return handle as unknown as ReturnType<typeof setTimeout>;
89 },
90 clearTimer(handle) {
91 timers.delete(handle as unknown as number);
92 },
93 };
94 const persistedKeys: string[] = [];
95 const scheduler = createWorkspaceTreePersistenceScheduler((key) => persistedKeys.push(key), 200, fakeClock);
96 for (let index = 0; index < 120; index += 1) scheduler.schedule("project-a");
97 ok(frames.size === 1 && timers.size === 0, "allows at most one persistence scheduling frame");
98 for (const callback of Array.from(frames.values())) callback();
99 frames.clear();
100 ok(timers.size === 1 && persistedKeys.length === 0, "waits for the quiet-period timer after the frame");
101 for (const callback of Array.from(timers.values())) callback();
102 timers.clear();
103 ok(persistedKeys.join(",") === "project-a", "persists the final project once after the quiet period");
104
105 scheduler.schedule("project-b");
106 scheduler.flush();
107 ok(
108 frames.size === 0 && persistedKeys[persistedKeys.length - 1] === "project-b",
109 "flushes pending state before a scope or page exit",
110 );
111
112 resetWorkspaceTreeMemoryForTests();
113 localStorage.setItem("reasonix.workspaceState.v2", JSON.stringify({ version: 99, projects: [{ key: "future", state: {} }] }));
114 ok(readWorkspaceTreeMemory("future") === null, "safely ignores storage from an unsupported future schema");
115
116 resetWorkspaceTreeMemoryForTests();
117 localStorage.setItem("reasonix.workspaceState.v2", "{not-json");
118 ok(readWorkspaceTreeMemory("broken") === null, "a corrupt cache cannot prevent workspace startup");
119
120 dom.window.close();
121 console.log(`\n${passed} passed, 0 failed, ${passed} total`);
122
122 lines TYPESCRIPT