返回 DeepSeek-Reasonix
run-tests.mjs
根目录 / desktop / frontend / scripts / run-tests.mjs
1 #!/usr/bin/env node
2 // Discovery-based test runner: every src/__tests__/*.test.ts{,x} runs as its
3 // own tsx process (suites install their own jsdom/globals and exit non-zero
4 // on failure), so adding a test file needs no package.json registration.
5 // Suites owned by a dedicated pnpm script are excluded here, named with their
6 // owner, and their absence fails the run so a rename cannot silently drop
7 // coverage. Fail-fast by default; --keep-going runs everything and summarizes.
8 import { spawnSync } from "node:child_process";
9 import { readdirSync } from "node:fs";
10 import { createRequire } from "node:module";
11 import { join, resolve } from "node:path";
12 import { pathToFileURL } from "node:url";
13
14 // Resolve the local tsx entry directly so the runner works both under pnpm
15 // scripts and when invoked as plain `node scripts/run-tests.mjs`.
16 const tsxCli = createRequire(import.meta.url).resolve("tsx/cli");
17
18 const TESTS_DIR = "src/__tests__";
19 const SCRIPTS_DIR = "scripts";
20
21 const OWNED_ELSEWHERE = new Map(Object.entries({
22 "terminal-events.test.ts": "test:terminal",
23 "terminal-store.test.ts": "test:terminal",
24 "terminal-output.test.ts": "test:terminal",
25 "terminal-theme.test.ts": "test:terminal",
26 "task-monitor-navigation.test.ts": "test:task-monitor",
27 "workspace-refresh-store.test.ts": "test:workspace",
28 "workspace-selection-isolation.test.tsx": "test:workspace",
29 "workspace-changes-errors.test.tsx": "test:workspace",
30 "workspace-preview-css.test.ts": "test:workspace",
31 "workspace-context-menu.test.tsx": "test:workspace",
32 "workspace-resize-interaction.test.tsx": "test:workspace",
33 "rich-composer-selection.test.tsx": "pretest",
34 "context-center-contract.test.ts": "pretest",
35 "provider-model-cache.test.ts": "pretest",
36 "format-tokens.test.ts": "pretest",
37 "usage-stats-format.test.ts": "test:usage-stats",
38 "usage-stats-panel.test.tsx": "test:usage-stats",
39 "settings-responsive-layout.test.ts": "test:settings-responsive",
40 "raf-batch.test.ts": "test:stream",
41 "stream-delta-batch.test.ts": "test:stream",
42 "use-controller-stream-progress.test.ts": "test:stream",
43 "nested-scroll-handoff.test.ts": "test:transcript",
44 "typography-overflow-contract.test.ts": "test:transcript",
45 "composer-menu-viewport.test.ts": "test:composer-menu-viewport",
46 "virtual-menu-identity.test.tsx": "test:composer-menu-viewport",
47 "remote-workspace-launch.test.ts": "test:remote",
48 "remote-store.test.ts": "test:remote",
49 "remote-error-ux.test.tsx": "test:remote",
50 "remote-hosts-page.test.tsx": "test:remote",
51 "remote-connect-wizard.test.tsx": "test:remote (needs the css stub register)",
52 "add-project-entries.test.ts": "test:remote",
53 "remote-secret-dialog.test.tsx": "test:remote",
54 "remote-server-panel.test.tsx": "test:remote (needs the svg stub register)",
55 "remote-session-surface.test.tsx": "test:remote (needs the svg stub register)",
56 "remote-running-reconcile.test.ts": "test:remote",
57 "remote-project-tree.test.tsx": "test:remote",
58 "statusbar-workspace.test.tsx": "test:remote",
59 "updater-shared-state.test.tsx": "test:updater",
60 "window-state-ordering.test.ts": "test:window-state",
61 }));
62
63 const keepGoing = process.argv.includes("--keep-going");
64 const files = readdirSync(TESTS_DIR)
65 .filter((name) => /\.test\.tsx?$/.test(name))
66 .sort();
67
68 for (const [name, owner] of OWNED_ELSEWHERE) {
69 if (!files.includes(name)) {
70 console.error(`run-tests: excluded suite ${name} (owner: ${owner}) no longer exists — update OWNED_ELSEWHERE`);
71 process.exit(1);
72 }
73 }
74
75 // Browser assets are not executable Node code. Use the same SVG/image/CSS
76 // loader as dedicated component suites so discovered transitive imports work.
77 // Asset rendering remains covered by the browser and stylesheet gates.
78 const assetArgs = ["--import", pathToFileURL(resolve(SCRIPTS_DIR, "svg-stub-register.mjs")).href];
79
80 const suites = files.filter((name) => !OWNED_ELSEWHERE.has(name));
81 console.log(`run-tests: ${suites.length} discovered suites (${OWNED_ELSEWHERE.size} owned by dedicated scripts)`);
82
83 const failures = [];
84 for (const name of suites) {
85 const path = join(TESTS_DIR, name);
86 console.log(`\n▶ ${path}`);
87 // English locale mirrors the Go convention (LANG=en_US.UTF-8 go test):
88 // Node's built-in navigator.language follows the machine's ICU locale, and
89 // suites assert English UI strings.
90 const env = { ...process.env, LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8" };
91 const result = spawnSync(process.execPath, [tsxCli, ...assetArgs, path], { stdio: "inherit", env });
92 if (result.error) console.error(`run-tests: spawn failed for ${path}: ${result.error.message}`);
93 if (result.status !== 0) {
94 if (!keepGoing) {
95 console.error(`\nrun-tests: FAILED at ${path}`);
96 process.exit(result.status ?? 1);
97 }
98 failures.push(name);
99 }
100 }
101
102 if (failures.length > 0) {
103 console.error(`\nrun-tests: ${failures.length}/${suites.length} suites failed:`);
104 for (const name of failures) console.error(` FAIL ${name}`);
105 process.exit(1);
106 }
107 console.log(`\nrun-tests: all ${suites.length} suites passed`);
108
108 lines Plain Text