返回 DeepSeek-Reasonix
bundle-contract.test.ts
根目录 / desktop / frontend / src / __tests__ / bundle-contract.test.ts
1 // Run: tsx src/__tests__/bundle-contract.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6
7 let passed = 0;
8 let failed = 0;
9
10 function ok(cond: boolean, label: string) {
11 if (cond) {
12 process.stdout.write(` PASS ${label}\n`);
13 passed += 1;
14 } else {
15 process.stdout.write(` FAIL ${label}\n`);
16 failed += 1;
17 }
18 }
19
20 const here = dirname(fileURLToPath(import.meta.url));
21 const appSource = readFileSync(resolve(here, "../App.tsx"), "utf8");
22 const projectTreeSource = readFileSync(resolve(here, "../components/ProjectTree.tsx"), "utf8");
23 const settingsEntrySource = readFileSync(resolve(here, "../components/SettingsPanelEntry.tsx"), "utf8");
24 const settingsSource = readFileSync(resolve(here, "../components/SettingsPanel.tsx"), "utf8");
25 const markdownSource = readFileSync(resolve(here, "../components/Markdown.tsx"), "utf8");
26 const localPathLinksSource = readFileSync(resolve(here, "../lib/localPathLinks.ts"), "utf8");
27 const i18nSource = readFileSync(resolve(here, "../lib/i18n.tsx"), "utf8");
28 const mainSource = readFileSync(resolve(here, "../main.tsx"), "utf8");
29 const stylesSource = readFileSync(resolve(here, "../styles.css"), "utf8");
30
31 console.log("\nbundle contract");
32
33 ok(
34 !/import\s+\{[^}]*\}\s+from\s+["']\.\/lib\/sessionExport["']/.test(appSource),
35 "App keeps session export code out of the initial chunk",
36 );
37 ok(
38 appSource.includes('import("./lib/sessionExport")'),
39 "App loads session export code on demand",
40 );
41 ok(
42 !/import\s+\{[^}]*\}\s+from\s+["']\.\/components\/SettingsPanel["']/.test(appSource) &&
43 !/import\s+\{[^}]*\}\s+from\s+["']\.\/components\/HistoryPanel["']/.test(appSource),
44 "App keeps secondary drawers out of the initial chunk",
45 );
46 ok(
47 appSource.includes('import("./components/SettingsPanelEntry")') &&
48 appSource.includes('import("./components/HistoryPanel")'),
49 "App loads secondary drawers on demand",
50 );
51 ok(
52 settingsEntrySource.includes('import "./CompactRatioSettings.css"') &&
53 settingsEntrySource.includes('from "./SettingsPanel"') &&
54 !settingsSource.includes('import "./CompactRatioSettings.css"'),
55 "Settings CSS stays in the lazy production entry without breaking direct module tests",
56 );
57 ok(
58 !appSource.includes("openAllHistory") &&
59 !appSource.includes("cmd-history") &&
60 !appSource.includes("sidebar.allHistory") &&
61 !projectTreeSource.includes("onOpenProjectHistory") &&
62 !projectTreeSource.includes("project-history"),
63 "App has no dedicated history-page entry points",
64 );
65 ok(
66 appSource.includes('id: "cmd-trash"') &&
67 appSource.includes("openTrash") &&
68 appSource.includes("paletteSessions.slice(0, 12)") &&
69 projectTreeSource.includes('t("projectTree.searchPlaceholder")'),
70 "Trash and existing session search remain available",
71 );
72 ok(
73 /\.sidebar--workbench\s+\.sidebar__utility-row\s*\{[^}]*grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/s.test(stylesSource),
74 "Workbench footer distributes its three utility actions evenly",
75 );
76 ok(
77 /\.app--creation\s+\.sidebar__nav,\s*:root\[data-theme-style\]\s+\.app--creation\s+\.sidebar__nav\s*\{[^}]*grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/s.test(stylesSource),
78 "Creation footer distributes search, trash, and settings evenly",
79 );
80 ok(
81 !/import\s+\{[^}]*\b(?:MCPServersSettingsPage|SkillsSettingsPage|PluginsSettingsPage)\b[^}]*\}\s+from\s+["']\.\/CapabilitiesPanel["']/.test(settingsSource) &&
82 !/import\s+\{[^}]*\bMemorySettingsPage\b[^}]*\}\s+from\s+["']\.\/MemoryPanel["']/.test(settingsSource),
83 "SettingsPanel keeps secondary settings pages out of the first settings chunk",
84 );
85 ok(
86 settingsSource.includes('import("./CapabilitiesPanel")') &&
87 settingsSource.includes('import("./MemoryPanel")'),
88 "SettingsPanel loads secondary settings pages on demand",
89 );
90 ok(
91 !/from\s+["']qrcode\.react["']/.test(settingsSource),
92 "SettingsPanel keeps QR rendering code out of the first settings chunk",
93 );
94 ok(
95 settingsSource.includes('import("qrcode.react")'),
96 "SettingsPanel loads QR rendering code on demand",
97 );
98 ok(
99 !/from\s+["']react-markdown["']/.test(markdownSource) &&
100 !/from\s+["']remark-gfm["']/.test(markdownSource) &&
101 !/from\s+["']remark-math["']/.test(markdownSource) &&
102 !/from\s+["']rehype-katex["']/.test(markdownSource) &&
103 !/katex\/dist\/katex\.min\.css/.test(markdownSource),
104 "Markdown wrapper keeps markdown/math vendor code out of the initial chunk",
105 );
106 ok(
107 markdownSource.includes('import("./MarkdownRenderer")'),
108 "Markdown wrapper loads markdown renderer on demand",
109 );
110 ok(
111 !/String\.raw`[^`]*\(\?<!/s.test(localPathLinksSource),
112 "Markdown local-path support avoids RegExp lookbehind required by newer WebKit",
113 );
114 ok(
115 !/import\s+\{\s*zh\s*\}\s+from\s+["']\.\.\/locales\/zh["']/.test(i18nSource) &&
116 !/import\s+\{\s*zhTW\s*\}\s+from\s+["']\.\.\/locales\/zh-TW["']/.test(i18nSource),
117 "i18n keeps Chinese dictionaries out of the unconditional initial chunk",
118 );
119 ok(
120 i18nSource.includes('import("../locales/zh")') &&
121 i18nSource.includes('import("../locales/zh-TW")'),
122 "i18n loads Chinese dictionaries on demand",
123 );
124 ok(
125 mainSource.includes("await preloadDetectedLocale()"),
126 "main preloads the detected locale before mounting React",
127 );
128
129 console.log(`\n${passed} passed, ${failed} failed`);
130 if (failed > 0) process.exit(1);
131
131 lines TYPESCRIPT