返回 DeepSeek-Reasonix
history-recovery-copies.test.tsx
根目录 / desktop / frontend / src / __tests__ / history-recovery-copies.test.tsx
1 // Run: tsx src/__tests__/history-recovery-copies.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import { registerHooks } from "node:module";
5 import React, { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import type { SessionMeta } from "../lib/types";
8
9 registerHooks({
10 resolve(specifier, context, nextResolve) {
11 if (specifier.endsWith(".svg")) return nextResolve("./asset-stub-for-tests.ts", { ...context, parentURL: import.meta.url });
12 return nextResolve(specifier, context);
13 },
14 });
15
16 let passed = 0;
17 let failed = 0;
18
19 function ok(value: boolean, label: string) {
20 if (value) {
21 process.stdout.write(` PASS ${label}\n`);
22 passed += 1;
23 } else {
24 process.stdout.write(` FAIL ${label}\n`);
25 failed += 1;
26 }
27 }
28
29 function eq(actual: unknown, expected: unknown, label: string) {
30 ok(JSON.stringify(actual) === JSON.stringify(expected), `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
31 }
32
33 function installDom() {
34 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", { pretendToBeVisual: true, url: "http://localhost/" });
35 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
36 globalThis.window = dom.window as unknown as Window & typeof globalThis;
37 globalThis.document = dom.window.document;
38 Object.defineProperty(dom.window.navigator, "language", { configurable: true, value: "en-US" });
39 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
40 globalThis.Node = dom.window.Node;
41 globalThis.Element = dom.window.Element;
42 globalThis.HTMLElement = dom.window.HTMLElement;
43 globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
44 globalThis.Event = dom.window.Event;
45 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
46 globalThis.MouseEvent = dom.window.MouseEvent;
47 globalThis.localStorage = dom.window.localStorage;
48 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
49 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
50 globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
51 return dom;
52 }
53
54 const now = 1_750_000_000_000;
55
56 function session(overrides: Partial<SessionMeta> & { path: string }): SessionMeta {
57 return {
58 preview: "session preview",
59 turns: 3,
60 createdAt: now - 3_600_000,
61 lastActivityAt: now,
62 modTime: now,
63 current: false,
64 open: false,
65 ...overrides,
66 };
67 }
68
69 async function renderPanel(props: Record<string, unknown>) {
70 const { HistoryPanel } = await import("../components/HistoryPanel");
71 const { LocaleProvider } = await import("../lib/i18n");
72 const root = createRoot(document.getElementById("root")!);
73 await act(async () => {
74 root.render(
75 <LocaleProvider>
76 <HistoryPanel
77 running={false}
78 onResume={() => {}}
79 onPreview={async () => []}
80 onDelete={() => {}}
81 onRename={() => {}}
82 onClose={() => {}}
83 sessions={[]}
84 {...props}
85 />
86 </LocaleProvider>,
87 );
88 await new Promise((resolve) => setTimeout(resolve, 30));
89 });
90 return root;
91 }
92
93 function findButton(text: string): HTMLButtonElement | undefined {
94 return Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.trim() === text) as HTMLButtonElement | undefined;
95 }
96
97 async function click(button: HTMLButtonElement) {
98 await act(async () => {
99 button.click();
100 await new Promise((resolve) => setTimeout(resolve, 20));
101 });
102 }
103
104 console.log("\nhistory recovery data visibility");
105
106 {
107 const dom = installDom();
108 const root = await renderPanel({
109 kind: "history",
110 sessions: [
111 session({ path: "/s/normal.jsonl", title: "normal session" }),
112 session({ path: "/s/continued.jsonl", title: "continued session", recovered: true }),
113 session({ path: "/s/covered.jsonl", title: "covered copy", recovered: true, recoveryCopy: true }),
114 ],
115 });
116
117 ok(document.body.textContent?.includes("normal session") === true, "ordinary history keeps normal sessions");
118 ok(document.body.textContent?.includes("continued session") === true, "unique recovered content remains ordinary history");
119 ok(document.body.textContent?.includes("covered copy") === false, "covered copies are hidden from ordinary history");
120 ok(!document.body.textContent?.includes("Recovery copies"), "ordinary history exposes no recovery-copy group");
121 ok(!findButton("Trash recovery copies"), "ordinary history exposes no copy cleanup action");
122 await act(async () => root.unmount());
123 dom.window.close();
124 }
125
126 {
127 const dom = installDom();
128 const emptied: string[][] = [];
129 const restored: string[] = [];
130 const purged: string[] = [];
131 const root = await renderPanel({
132 kind: "trash",
133 sessions: [
134 session({ path: "/t/normal.jsonl", title: "deleted session", deletedAt: now }),
135 session({ path: "/t/covered.jsonl", title: "system copy", deletedAt: now, recovered: true, recoveryCopy: true }),
136 ],
137 onRestore: async (path: string) => { restored.push(path); },
138 onPurge: async (path: string) => { purged.push(path); },
139 onPurgeAll: async (paths: string[]) => { emptied.push(paths); },
140 });
141
142 ok(document.body.textContent?.includes("system copy") === false, "trash collapses system recovery data by default");
143 const disclosure = document.querySelector<HTMLButtonElement>(".history-system-recovery__toggle") ?? undefined;
144 ok(Boolean(disclosure), "trash provides a collapsed advanced recovery-data disclosure");
145 if (disclosure) await click(disclosure);
146 ok(document.body.textContent?.includes("system copy") === true, "advanced disclosure keeps recovery data restorable");
147 const systemCopy = Array.from(document.querySelectorAll<HTMLButtonElement>(".hist-item__main"))
148 .find((button) => button.textContent?.includes("system copy"));
149 if (systemCopy) await click(systemCopy);
150 const restore = findButton("Restore");
151 ok(Boolean(restore) && !restore?.disabled, "system recovery data retains its restore action");
152 if (restore) await click(restore);
153 eq(restored, ["/t/covered.jsonl"], "restore targets the selected system recovery item");
154 const purge = findButton("Delete permanently");
155 ok(Boolean(purge) && !purge?.disabled, "system recovery data retains permanent delete control");
156 if (purge) {
157 await click(purge);
158 const confirmPurge = findButton("Confirm permanent delete");
159 if (confirmPurge) await click(confirmPurge);
160 }
161 eq(purged, ["/t/covered.jsonl"], "permanent delete targets the selected system recovery item");
162
163 const clear = findButton("Empty trash");
164 ok(Boolean(clear), "ordinary trash still exposes its normal empty action");
165 if (clear) {
166 await click(clear);
167 const confirm = findButton("Confirm empty trash");
168 if (confirm) await click(confirm);
169 }
170 eq(emptied, [["/t/normal.jsonl"]], "empty trash excludes protected system recovery data");
171 await act(async () => root.unmount());
172 dom.window.close();
173 }
174
175 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
176 if (failed > 0) process.exit(1);
177
177 lines Plain Text