返回 DeepSeek-Reasonix
subagent-progress-card.test.tsx
根目录 / desktop / frontend / src / __tests__ / subagent-progress-card.test.tsx
1 // Run: tsx src/__tests__/subagent-progress-card.test.tsx
2 //
3 // Verifies the ToolCard rendering of the sub-agent progress chip (phase +
4 // elapsed + recent activity) and the expanded preview body (reasoning /
5 // response preview / notices), including the terminal phase visuals.
6
7 import { JSDOM } from "jsdom";
8 import React from "react";
9 import { act } from "react";
10 import { createRoot } from "react-dom/client";
11 import gsap from "gsap";
12 import { ToolCard } from "../components/ToolCard";
13 import { LocaleProvider } from "../lib/i18n";
14 import type { Item, SubagentProgress } from "../lib/useController";
15
16 type ToolItem = Extract<Item, { kind: "tool" }>;
17
18 // jsdom has no layout engine: stub the GSAP tween surface the collapse hook
19 // touches so layout effects complete synchronously. Under tsx the imported
20 // binding is a CJS interop object, so the stubs must go onto that object
21 // itself (the hook imports the same binding).
22 type GsapToOptions = { onComplete?: () => void };
23 const gsapForTests = gsap as unknown as {
24 to: (target: unknown, vars: GsapToOptions) => unknown;
25 fromTo: (target: unknown, from: unknown, vars: GsapToOptions) => unknown;
26 set: (target: unknown, vars: unknown) => unknown;
27 killTweensOf: (target: unknown) => void;
28 };
29 gsapForTests.to = (_target: unknown, vars: GsapToOptions) => {
30 vars.onComplete?.();
31 return {};
32 };
33 gsapForTests.fromTo = (_target: unknown, _from: unknown, vars: GsapToOptions) => {
34 vars.onComplete?.();
35 return {};
36 };
37 gsapForTests.set = () => ({});
38 gsapForTests.killTweensOf = () => {};
39
40 let passed = 0;
41 let failed = 0;
42
43 function ok(value: unknown, label: string) {
44 if (value) {
45 process.stdout.write(` PASS ${label}\n`);
46 passed += 1;
47 } else {
48 process.stdout.write(` FAIL ${label}\n`);
49 failed += 1;
50 }
51 }
52
53 function flushTimers(): Promise<void> {
54 return new Promise((resolve) => setTimeout(resolve, 0));
55 }
56
57 function installDom() {
58 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
59 pretendToBeVisual: true,
60 url: "http://localhost/",
61 });
62 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
63 globalThis.window = dom.window as unknown as Window & typeof globalThis;
64 globalThis.document = dom.window.document;
65 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
66 globalThis.Node = dom.window.Node;
67 globalThis.Element = dom.window.Element;
68 globalThis.HTMLElement = dom.window.HTMLElement;
69 globalThis.Event = dom.window.Event;
70 globalThis.MouseEvent = dom.window.MouseEvent;
71 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
72 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
73 dom.window.matchMedia = () => ({
74 matches: true,
75 media: "(prefers-reduced-motion: reduce)",
76 onchange: null,
77 addListener: () => undefined,
78 removeListener: () => undefined,
79 addEventListener: () => undefined,
80 removeEventListener: () => undefined,
81 dispatchEvent: () => false,
82 });
83 return dom;
84 }
85
86 function makeItem(phase: SubagentProgress["phase"], over: Partial<SubagentProgress> = {}): ToolItem {
87 const now = Date.now();
88 return {
89 kind: "tool",
90 id: `task-${phase}`,
91 name: "task",
92 args: "{}",
93 readOnly: true,
94 status: phase === "completed" || phase === "failed" ? "done" : phase === "cancelled" ? "stopped" : "running",
95 subagentProgress: {
96 phase,
97 reasoning: "thinking step by step",
98 text: "draft answer preview",
99 notice: "heads up",
100 lastActivityAt: now - 3_000,
101 startedAt: now - 12_000,
102 truncated: false,
103 ...over,
104 },
105 };
106 }
107
108 console.log("\nsubagent progress card");
109
110 {
111 const dom = installDom();
112 const rootEl = document.getElementById("root");
113 if (!rootEl) throw new Error("missing root");
114 const root = createRoot(rootEl);
115
116 // Running card: chip shows phase, live elapsed and recent activity.
117 const running = makeItem("reasoning");
118 await act(async () => {
119 root.render(
120 React.createElement(LocaleProvider, null, React.createElement(ToolCard, { item: running })),
121 );
122 await flushTimers();
123 });
124 const chip = document.querySelector(".tool__subagent-chip");
125 ok(!!chip, "running card renders the progress chip");
126 ok(chip?.textContent?.includes("reasoning"), "chip shows the phase label");
127 ok(chip?.textContent?.includes("12s"), "chip shows the running elapsed");
128 ok(chip?.textContent?.includes("3s ago"), "chip shows recent activity");
129 ok(chip?.getAttribute("data-phase") === "reasoning", "chip carries the phase attribute");
130
131 // Expanded body shows reasoning / response / notices without ordinary output.
132 const head = document.querySelector(".tool__head") as HTMLButtonElement | null;
133 ok(!!head, "card head renders");
134 await act(async () => {
135 head?.click();
136 await flushTimers();
137 });
138 ok(!!document.querySelector(".tool__subagent-preview"), "expanded body renders the preview block");
139 ok(document.querySelector(".tool__subagent-preview-label")?.textContent === "Reasoning", "reasoning section label");
140 ok(document.body.textContent?.includes("thinking step by step"), "reasoning preview text visible");
141 ok(document.body.textContent?.includes("draft answer preview"), "response preview text visible");
142 ok(document.body.textContent?.includes("heads up"), "notice preview text visible");
143
144 await act(async () => {
145 root.unmount();
146 });
147 dom.window.close();
148 }
149
150 {
151 const dom = installDom();
152 const rootEl = document.getElementById("root");
153 if (!rootEl) throw new Error("missing root");
154 const root = createRoot(rootEl);
155
156 // Terminal chips: completed/failed/cancelled show the final duration, no
157 // recent-activity suffix, and the existing status visuals.
158 const completed = makeItem("completed", { durationMs: 42_000 });
159 await act(async () => {
160 root.render(
161 React.createElement(LocaleProvider, null, React.createElement(ToolCard, { item: completed })),
162 );
163 await flushTimers();
164 });
165 const chip = document.querySelector(".tool__subagent-chip");
166 ok(chip?.textContent?.includes("completed"), "completed chip label");
167 ok(chip?.textContent?.includes("42s"), "completed chip shows the terminal duration");
168 ok(!chip?.textContent?.includes("ago"), "terminal chip drops the recent-activity suffix");
169 ok(!!document.querySelector(".tool__status-icon--ok"), "completed card shows the done icon");
170
171 await act(async () => {
172 root.unmount();
173 });
174 dom.window.close();
175 }
176
177 {
178 const dom = installDom();
179 const rootEl = document.getElementById("root");
180 if (!rootEl) throw new Error("missing root");
181 const root = createRoot(rootEl);
182
183 const cancelled = makeItem("cancelled", { durationMs: 500 });
184 await act(async () => {
185 root.render(
186 React.createElement(LocaleProvider, null, React.createElement(ToolCard, { item: cancelled })),
187 );
188 await flushTimers();
189 });
190 ok(document.querySelector(".tool__subagent-chip")?.textContent?.includes("cancelled"), "cancelled chip label");
191 ok(!!document.querySelector(".tool__status-icon--stopped"), "cancelled card shows the stopped icon");
192 ok(document.querySelector(".tool__subagent-chip")?.classList.contains("tool__subagent-chip--cancelled"), "chip carries the cancelled modifier class");
193
194 await act(async () => {
195 root.unmount();
196 });
197 dom.window.close();
198 }
199
200 console.log(`\nsubagent progress card: ${passed} passed, ${failed} failed`);
201 if (failed > 0) process.exit(1);
202
202 lines Plain Text