返回 DeepSeek-Reasonix
workspace-preview-css.test.ts
根目录 / desktop / frontend / src / __tests__ / workspace-preview-css.test.ts
1 // Run: tsx src/__tests__/workspace-preview-css.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import { JSDOM } from "jsdom";
7
8 const testDir = dirname(fileURLToPath(import.meta.url));
9 const styles = readFileSync(resolve(testDir, "../styles.css"), "utf8");
10 const appGo = readFileSync(resolve(testDir, "../../../app.go"), "utf8");
11 const localeNotices = [
12 [readFileSync(resolve(testDir, "../locales/en.ts"), "utf8"), '"workspace.truncated": "Preview truncated to the first 2 MiB."'],
13 [readFileSync(resolve(testDir, "../locales/zh.ts"), "utf8"), '"workspace.truncated": "预览已截断到前 2 MiB。"'],
14 [readFileSync(resolve(testDir, "../locales/zh-TW.ts"), "utf8"), '"workspace.truncated": "預覽已截斷到前 2 MiB。"'],
15 ];
16 const localeSearchLabels = [
17 [readFileSync(resolve(testDir, "../locales/en.ts"), "utf8"), '"workspace.searchPlaceholder": "Find"'],
18 [readFileSync(resolve(testDir, "../locales/zh.ts"), "utf8"), '"workspace.searchPlaceholder": "查找"'],
19 [readFileSync(resolve(testDir, "../locales/zh-TW.ts"), "utf8"), '"workspace.searchPlaceholder": "尋找"'],
20 ];
21
22 let passed = 0;
23 let failed = 0;
24
25 function eq(a: unknown, b: unknown, label: string) {
26 if (a === b) {
27 process.stdout.write(` PASS ${label}\n`);
28 passed += 1;
29 } else {
30 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
31 failed += 1;
32 }
33 }
34
35 function matchingBlocks(selector: string): string[] {
36 const blocks: string[] = [];
37 const rule = /([^{}]+)\{([^{}]*)\}/g;
38 let match: RegExpExecArray | null;
39 while ((match = rule.exec(styles)) !== null) {
40 const selectors = match[1].split(",").map((part) => part.trim());
41 if (selectors.includes(selector)) blocks.push(match[2]);
42 }
43 return blocks;
44 }
45
46 function finalDeclaration(selector: string, property: string): string | undefined {
47 let value: string | undefined;
48 for (const block of matchingBlocks(selector)) {
49 const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g");
50 let match: RegExpExecArray | null;
51 while ((match = declaration.exec(block)) !== null) {
52 value = match[1].trim();
53 }
54 }
55 return value;
56 }
57
58 function computedDeclaration(html: string, selector: string, property: string): string {
59 const dom = new JSDOM(html);
60 const style = dom.window.document.createElement("style");
61 style.textContent = styles;
62 dom.window.document.head.append(style);
63 const element = dom.window.document.querySelector(selector);
64 if (!element) throw new Error(`Missing selector in test DOM: ${selector}`);
65 return dom.window.getComputedStyle(element).getPropertyValue(property).trim();
66 }
67
68 console.log("\nworkspace preview css");
69
70 eq(finalDeclaration(".workspace-preview__body--code", "overflow"), "hidden", "code preview body does not create a nested scroller");
71 eq(finalDeclaration(".workspace-preview__body--code", "display"), "flex", "code preview body hosts an editor-like viewport");
72 eq(finalDeclaration(".workspace-preview__body--code", "flex-direction"), "column", "truncated code notes stack above the code viewport");
73 eq(
74 computedDeclaration(
75 `<html data-theme-style="default"><head></head><body><aside class="workspace-panel workspace-panel--embedded"><div class="workspace-preview__body workspace-preview__body--code"></div></aside></body></html>`,
76 ".workspace-preview__body--code",
77 "padding",
78 ),
79 "0px",
80 "code preview body keeps zero padding under embedded and themed cascade",
81 );
82 eq(finalDeclaration(".workspace-preview__body--code .workspace-note", "flex"), "0 0 auto", "code truncation note keeps its own row");
83 eq(finalDeclaration(".workspace-preview__body--code .code-block", "display"), "flex", "code block fills the preview viewport");
84 eq(finalDeclaration(".workspace-preview__body--code .code-block__wrap", "display"), "flex", "code wrapper keeps the preview viewport height");
85 eq(finalDeclaration(".workspace-preview__body--code .code-block__wrap", "flex"), "1 1 auto", "code wrapper participates in the preview flex height");
86 eq(finalDeclaration(".workspace-preview__body--code .code-block__wrap", "flex-direction"), "column", "search toolbar stacks above the code viewport");
87 eq(finalDeclaration(".workspace-preview__body--code .code-block__wrap", "min-height"), "0", "code wrapper can shrink around the scrollable code viewport");
88 eq(finalDeclaration(".workspace-preview__body--code .code-search", "position"), "static", "workspace search toolbar participates in layout");
89 eq(finalDeclaration(".workspace-preview__body--code .code-search", "z-index"), "var(--z-local-handle)", "workspace search toolbar stays clickable above the copy affordance");
90 eq(finalDeclaration(".workspace-preview__body--code .code-search", "flex"), "0 0 auto", "workspace search toolbar keeps a dedicated row");
91 eq(finalDeclaration(".workspace-preview__body--code .code-search", "flex-wrap"), "wrap", "narrow workspace search toolbars can form a second control row");
92 eq(finalDeclaration(".workspace-preview__body--code .code-search", "width"), "100%", "workspace search toolbar spans the code viewport");
93 eq(finalDeclaration(".workspace-preview__body--code .code-search", "border-width"), "0 0 1px", "workspace search toolbar uses a divider instead of a floating card");
94 eq(finalDeclaration(".workspace-preview__body--code .code-search", "box-shadow"), "none", "workspace search toolbar does not retain popup elevation");
95 eq(finalDeclaration(".code-search__actions", "display"), "flex", "search actions remain one coherent wrapping group");
96 eq(finalDeclaration(".code-search__actions", "margin-left"), "auto", "wrapped search actions stay aligned to the toolbar edge");
97 eq(finalDeclaration(".workspace-preview__body--code .code", "overflow"), "auto", "code viewport owns horizontal and vertical scrolling");
98 eq(finalDeclaration(".workspace-preview__body--code .code", "min-height"), "0", "code viewport can shrink inside the preview pane");
99 eq(finalDeclaration(".workspace-preview__body--code .code", "margin"), "0", "code viewport scrollbar sits at the visible pane bottom");
100 eq(finalDeclaration(".code-search__input", "min-width"), "60px", "search input reserves a minimum readable width");
101 eq(
102 computedDeclaration(
103 `<html data-theme-style="default"><head></head><body><div class="workspace-preview__body workspace-preview__body--code"><div class="code code--lines"></div></div></body></html>`,
104 ".code--lines",
105 "padding",
106 ),
107 "0px",
108 "themed workspace code keeps the line-number gutter flush",
109 );
110 eq(
111 finalDeclaration(".workspace-panel--with-tree-rail:not(.workspace-panel--tree-hidden)", "grid-template-columns"),
112 "var(--workspace-tree-rail-width) var(--workspace-tree-width) minmax(var(--workspace-preview-min-width), 1fr)",
113 "split mode keeps a narrow tree toggle rail beside the file tree",
114 );
115 eq(finalDeclaration(".workspace-panel--with-tree-rail:not(.workspace-panel--tree-hidden) .workspace-files", "grid-column"), "2", "file tree sits beside the rail");
116 eq(finalDeclaration(".workspace-panel--with-tree-rail:not(.workspace-panel--tree-hidden) .workspace-preview", "grid-column"), "3", "preview sits after rail and tree");
117 eq(
118 finalDeclaration(".workspace-panel--with-tree-rail .workspace-tree-resizer", "left"),
119 "calc(var(--workspace-tree-rail-width) + var(--workspace-tree-width) - 4px)",
120 "tree resizer accounts for the persistent rail",
121 );
122 eq(
123 finalDeclaration(".workspace-panel--tree-hidden", "grid-template-columns"),
124 "var(--workspace-tree-rail-width) minmax(0, 1fr)",
125 "preview-only mode keeps a narrow tree toggle rail",
126 );
127 eq(finalDeclaration(".workspace-panel--tree-hidden .workspace-preview", "grid-column"), "2", "preview sits beside the rail");
128 eq(
129 /const filePreviewLimit = 2 \* 1024 \* 1024/.test(appGo),
130 true,
131 "backend workspace preview limit remains 2 MiB",
132 );
133 eq(
134 localeNotices.every(([source, expected]) => source.includes(expected)),
135 true,
136 "all locale truncation notices match the 2 MiB backend limit",
137 );
138 eq(
139 localeNotices.every(([source]) => !source.includes("256 KB")),
140 true,
141 "locale truncation notices do not retain the previous 256 KB limit",
142 );
143 eq(
144 localeSearchLabels.every(([source, expected]) => source.includes(expected)),
145 true,
146 "all locales provide workspace search labels",
147 );
148
149 console.log(`\n${passed} passed, ${failed} failed`);
150 if (failed > 0) process.exit(1);
151
151 lines TYPESCRIPT