返回 DeepSeek-Reasonix
diagnostics-settings.test.tsx
根目录 / desktop / frontend / src / __tests__ / diagnostics-settings.test.tsx
1 import { JSDOM } from "jsdom";
2 import React from "react";
3 import { act } from "react";
4 import { createRoot } from "react-dom/client";
5 import { DiagnosticsSettingsPage } from "../components/DiagnosticsSettingsPage";
6 import type { AppBindings } from "../lib/bridge";
7 import { LocaleProvider } from "../lib/i18n";
8 import type { CapabilityDiagnosticsReport, SettingsTab } from "../lib/types";
9
10 function ok(value: unknown, message: string) {
11 if (!value) throw new Error(message);
12 }
13
14 function flush(): Promise<void> {
15 return new Promise((resolve) => setTimeout(resolve, 0));
16 }
17
18 async function waitFor(label: string, predicate: () => boolean) {
19 for (let attempt = 0; attempt < 40; attempt += 1) {
20 await act(async () => {
21 await flush();
22 });
23 if (predicate()) return;
24 }
25 throw new Error(`timed out waiting for ${label}: ${document.body?.textContent?.slice(0, 400) ?? ""}`);
26 }
27
28 function installDom() {
29 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
30 pretendToBeVisual: true,
31 url: "http://localhost/",
32 });
33 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
34 globalThis.window = dom.window as unknown as Window & typeof globalThis;
35 globalThis.document = dom.window.document;
36 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
37 globalThis.Node = dom.window.Node;
38 globalThis.HTMLElement = dom.window.HTMLElement;
39 globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
40 globalThis.HTMLInputElement = dom.window.HTMLInputElement;
41 globalThis.Event = dom.window.Event;
42 globalThis.MouseEvent = dom.window.MouseEvent;
43 globalThis.localStorage = dom.window.localStorage;
44 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
45 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
46 Object.defineProperty(window, "matchMedia", {
47 configurable: true,
48 value: () => ({
49 matches: true,
50 media: "(prefers-reduced-motion: reduce)",
51 onchange: null,
52 addEventListener() {},
53 removeEventListener() {},
54 addListener() {},
55 removeListener() {},
56 dispatchEvent: () => false,
57 }),
58 });
59 return dom;
60 }
61
62 function baseReport(runtime: boolean): CapabilityDiagnosticsReport {
63 return {
64 schema_version: 1,
65 root: "<workspace>",
66 live: false,
67 summary: {
68 errors: 1,
69 warnings: 1,
70 infos: runtime ? 1 : 0,
71 instructions: 1,
72 skills: 1,
73 commands: 0,
74 hooks: 0,
75 plugins: 0,
76 mcp_servers: 1,
77 },
78 instructions: { docs: [{ path: "<workspace>/AGENTS.md", scope: "project", directory: "<workspace>", depth: 0, order: 1 }] },
79 skills: {
80 roots: [{ path: "<workspace>/.reasonix/skills", scope: "project", status: "ok" }],
81 entries: [{ name: "demo", path: "<workspace>/.reasonix/skills/demo/SKILL.md", status: "winner" }],
82 winners: 1,
83 shadowed: 0,
84 },
85 commands: { roots: [], entries: [], winners: 0, shadowed: 0 },
86 hooks: { trusted_project: false, project_defines_hooks: true, sources: [], entries: [] },
87 plugins: { packages: [] },
88 mcp: {
89 servers: [{
90 name: "demo-mcp",
91 transport: "stdio",
92 start_intent: "automatic",
93 source: "toml",
94 runtime_status: runtime ? "connected" : undefined,
95 tool_count: runtime ? 2 : undefined,
96 }],
97 },
98 issues: [
99 {
100 severity: "error",
101 code: "mcp.command_not_found",
102 subsystem: "mcp",
103 name: "demo-mcp",
104 message: "command missing",
105 remediation: "install binary",
106 settings_tab: "mcp",
107 },
108 {
109 severity: "warning",
110 code: "skill.missing_description",
111 subsystem: "skills",
112 name: "nodesc",
113 message: "no description",
114 settings_tab: "skills",
115 },
116 ],
117 };
118 }
119
120 console.log("diagnostics settings page");
121
122 {
123 const calls: boolean[] = [];
124 const navigations: SettingsTab[] = [];
125 installDom();
126 // Prefer English labels for stable button text assertions.
127 window.localStorage.setItem("reasonix-lang", "en");
128
129 window.go = {
130 main: {
131 App: {
132 CapabilityDiagnostics: async (includeSessionRuntime: boolean) => {
133 calls.push(includeSessionRuntime);
134 return baseReport(includeSessionRuntime);
135 },
136 } as Partial<AppBindings> as AppBindings,
137 },
138 };
139
140 const rootEl = document.getElementById("root");
141 if (!rootEl) throw new Error("missing root");
142 const root = createRoot(rootEl);
143
144 await act(async () => {
145 root.render(
146 React.createElement(
147 LocaleProvider,
148 null,
149 React.createElement(DiagnosticsSettingsPage, {
150 onNavigate: (tab: SettingsTab) => {
151 navigations.push(tab);
152 },
153 }),
154 ),
155 );
156 await flush();
157 });
158
159 await waitFor("static report", () => (rootEl.textContent || "").includes("mcp.command_not_found"));
160 ok(calls[0] === false, "initial load must request static report (includeSessionRuntime=false)");
161 ok((rootEl.textContent || "").includes("skill.missing_description"), "warnings must render");
162 ok(rootEl.querySelector(".diag-summary"), "health summary must render");
163
164 const runtimeToggle = rootEl.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
165 ok(runtimeToggle, "runtime toggle must exist");
166 ok(runtimeToggle!.checked === false, "runtime toggle starts unchecked");
167 await act(async () => {
168 // Use click so React's controlled onChange sees the flipped checked state.
169 runtimeToggle!.click();
170 await flush();
171 });
172 await waitFor("runtime reload", () => calls.length >= 2 && calls[calls.length - 1] === true);
173
174 const gotoBtn = Array.from(rootEl.querySelectorAll("button")).find((b) =>
175 (b.textContent || "").includes("Open settings"),
176 );
177 ok(gotoBtn, "goto settings button must exist on issue with settings_tab");
178 await act(async () => {
179 gotoBtn!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
180 await flush();
181 });
182 ok(navigations.includes("mcp"), `goto settings must navigate to mcp, got ${JSON.stringify(navigations)}`);
183
184 const paths = rootEl.querySelectorAll(".diag-path");
185 ok(paths.length > 0, "redacted paths must render");
186
187 await act(async () => {
188 root.unmount();
189 });
190 }
191
192 {
193 installDom();
194 window.localStorage.setItem("reasonix-lang", "en");
195
196 const nullArrays = baseReport(false) as unknown as Record<string, unknown>;
197 nullArrays.summary = {
198 errors: 0,
199 warnings: 0,
200 infos: 0,
201 instructions: 0,
202 skills: 0,
203 commands: 0,
204 hooks: 0,
205 plugins: 0,
206 mcp_servers: 0,
207 };
208 nullArrays.issues = null;
209 nullArrays.instructions = { docs: null };
210 nullArrays.skills = { roots: null, entries: null, winners: 0, shadowed: 0 };
211 nullArrays.commands = { roots: null, entries: null, winners: 0, shadowed: 0 };
212 nullArrays.hooks = { trusted_project: false, project_defines_hooks: false, sources: null, entries: null };
213 nullArrays.plugins = { packages: null };
214 nullArrays.mcp = { servers: null };
215
216 window.go = {
217 main: {
218 App: {
219 CapabilityDiagnostics: async () => nullArrays as unknown as CapabilityDiagnosticsReport,
220 } as Partial<AppBindings> as AppBindings,
221 },
222 };
223
224 const rootEl = document.getElementById("root");
225 if (!rootEl) throw new Error("missing root");
226 const root = createRoot(rootEl);
227
228 await act(async () => {
229 root.render(
230 React.createElement(
231 LocaleProvider,
232 null,
233 React.createElement(DiagnosticsSettingsPage),
234 ),
235 );
236 await flush();
237 });
238
239 await waitFor("null arrays to render as empty", () => (rootEl.textContent || "").includes("No issues found"));
240 ok(rootEl.querySelector(".diag-summary"), "null arrays must not crash the diagnostics page");
241
242 await act(async () => {
243 root.unmount();
244 });
245 }
246
247 console.log("diagnostics-settings: ok");
248
248 lines Plain Text