返回 DeepSeek-Reasonix
transcript-fold-preference.test.tsx
根目录 / desktop / frontend / src / __tests__ / transcript-fold-preference.test.tsx
1 // Run: tsx src/__tests__/transcript-fold-preference.test.tsx
2 //
3 // Pins that switching the work-process-fold preference applies to folds
4 // already on screen through the live event bus — not only to folds mounted
5 // afterwards.
6
7 import { JSDOM } from "jsdom";
8 import React from "react";
9 import { act } from "react";
10 import { createRoot } from "react-dom/client";
11
12 let passed = 0;
13 let failed = 0;
14
15 function ok(value: unknown, label: string) {
16 if (value) {
17 process.stdout.write(` PASS ${label}\n`);
18 passed += 1;
19 } else {
20 process.stdout.write(` FAIL ${label}\n`);
21 failed += 1;
22 }
23 }
24
25 console.log("\ntranscript fold preference sync");
26
27 const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
28 pretendToBeVisual: true,
29 url: "http://localhost/",
30 });
31 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
32 globalThis.window = dom.window as unknown as Window & typeof globalThis;
33 globalThis.document = dom.window.document;
34 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
35 globalThis.Node = dom.window.Node;
36 globalThis.HTMLElement = dom.window.HTMLElement;
37 globalThis.Event = dom.window.Event;
38 globalThis.CustomEvent = dom.window.CustomEvent;
39 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
40 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
41 Object.defineProperty(dom.window, "matchMedia", {
42 configurable: true,
43 value: () => ({
44 matches: true,
45 media: "(prefers-reduced-motion: reduce)",
46 onchange: null,
47 addEventListener() {},
48 removeEventListener() {},
49 addListener() {},
50 removeListener() {},
51 dispatchEvent: () => false,
52 }),
53 });
54 const storage = new Map<string, string>();
55 Object.defineProperty(globalThis, "localStorage", {
56 configurable: true,
57 value: {
58 getItem: (key: string) => storage.get(key) ?? null,
59 setItem: (key: string, value: string) => void storage.set(key, value),
60 removeItem: (key: string) => void storage.delete(key),
61 clear: () => storage.clear(),
62 key: () => null,
63 length: 0,
64 },
65 });
66
67 const { createServer } = await import("vite");
68 // GSAP's CSS plugin cannot run against jsdom; the assertions here are about
69 // state-driven class names, so the animation hooks are stubbed out.
70 const server = await createServer({
71 appType: "custom",
72 logLevel: "silent",
73 server: { middlewareMode: true },
74 plugins: [
75 {
76 name: "stub-animation-hooks",
77 enforce: "pre",
78 load(id) {
79 if (id.endsWith("/src/lib/useGSAPCollapse.ts")) {
80 return "export function useGSAPCollapse() {}";
81 }
82 if (id.endsWith("/src/lib/useEntranceAnimation.ts")) {
83 return "export function useEntranceAnimation() { return { current: null }; }";
84 }
85 return undefined;
86 },
87 },
88 ],
89 });
90 const { Transcript } = await server.ssrLoadModule("/src/components/Transcript.tsx");
91 const { LocaleProvider } = await server.ssrLoadModule("/src/lib/i18n.tsx");
92 const { setProcessFoldPreference } = await server.ssrLoadModule("/src/lib/processFoldPreference.ts");
93
94 const items = [
95 { kind: "user", id: "u1", text: "ask" },
96 { kind: "assistant", id: "a1", text: "answered", reasoning: "quick thought", streaming: false, workDurationMs: 3_000 },
97 {
98 kind: "notice",
99 id: "decision-1",
100 level: "info",
101 text: "Decision recorded: answered",
102 decisionReceipt: {
103 id: "ask-1",
104 kind: "ask",
105 subject: "Choose a model: DeepSeek V4",
106 outcome: "answered",
107 },
108 },
109 ];
110
111 const container = dom.window.document.getElementById("root")!;
112 const root = createRoot(container);
113 try {
114 await act(async () => {
115 root.render(
116 React.createElement(
117 LocaleProvider,
118 null,
119 React.createElement(Transcript, { items, onPrompt: () => {}, questionNavigator: false, running: false }),
120 ),
121 );
122 });
123
124 ok(container.querySelector(".turn-collapse"), "completed turn renders its work fold");
125 ok(!container.querySelector(".turn-collapse--open"), "auto preference keeps the completed fold collapsed");
126 ok(container.textContent?.includes("Question answered"), "Ask receipt keeps its completed title");
127 ok(!container.querySelector(".notice-line__decision-outcome"), "Ask receipt does not repeat the answered outcome");
128
129 await act(async () => {
130 setProcessFoldPreference("expanded");
131 });
132 ok(container.querySelector(".turn-collapse--open"), "switching to keep-expanded opens folds already on screen");
133
134 await act(async () => {
135 setProcessFoldPreference("auto");
136 });
137 ok(!container.querySelector(".turn-collapse--open"), "switching back to auto collapses completed folds again");
138 } finally {
139 await act(async () => {
140 root.unmount();
141 });
142 await server.close();
143 }
144
145 console.log(`\n${passed} passed, ${failed} failed`);
146 if (failed > 0) process.exit(1);
147
147 lines Plain Text