返回 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", "max-height"), "none", "workspace code clears the shared scroll-y height cap");
100 eq(finalDeclaration(".workspace-preview__body--code .code", "margin"), "0", "code viewport scrollbar sits at the visible pane bottom");
101 eq(
102 computedDeclaration(
103 `<html><head></head><body><div class="workspace-preview__body workspace-preview__body--code"><pre class="code code--scroll-y"></pre></div></body></html>`,
104 ".code--scroll-y",
105 "max-height",
106 ),
107 "none",
108 "workspace selector wins over the shared virtual-scroll cap",
109 );
110 eq(finalDeclaration(".code-search__input", "min-width"), "60px", "search input reserves a minimum readable width");
111 eq(
112 computedDeclaration(
113 `<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>`,
114 ".code--lines",
115 "padding",
116 ),
117 "0px",
118 "themed workspace code keeps the line-number gutter flush",
119 );
120 const splitDom = `<html><head></head><body><aside class="workspace-panel"><section class="workspace-files"></section><section class="workspace-preview"></section><button class="workspace-tree-resizer"></button></aside></body></html>`;
121 eq(
122 computedDeclaration(splitDom, ".workspace-panel", "grid-template-columns"),
123 "minmax(var(--workspace-preview-min-width), 1fr) var(--workspace-tree-width)",
124 "split mode is a two-column layout: preview on the left, file tree on the right (the tree toggle rail is gone)",
125 );
126 eq(computedDeclaration(splitDom, ".workspace-files", "grid-column"), "2", "file tree sits at the far right after the preview");
127 eq(computedDeclaration(splitDom, ".workspace-preview", "grid-column"), "1", "preview sits to the left of the tree");
128 eq(
129 computedDeclaration(splitDom, ".workspace-tree-resizer", "left"),
130 "calc(100% - var(--workspace-tree-width) - 4px)",
131 "tree resizer sits at the tree's left edge (tree is on the right)",
132 );
133 eq(
134 finalDeclaration(".workspace-panel--tree-hidden", "grid-template-columns"),
135 "minmax(0, 1fr)",
136 "preview-only mode drops the file tree",
137 );
138 eq(finalDeclaration(".workspace-panel--tree-hidden .workspace-preview", "grid-column"), "1", "preview fills the panel when the tree is hidden");
139 eq(
140 /const filePreviewLimit = 2 \* 1024 \* 1024/.test(appGo),
141 true,
142 "backend workspace preview limit remains 2 MiB",
143 );
144 eq(
145 localeNotices.every(([source, expected]) => source.includes(expected)),
146 true,
147 "all locale truncation notices match the 2 MiB backend limit",
148 );
149 eq(
150 localeNotices.every(([source]) => !source.includes("256 KB")),
151 true,
152 "locale truncation notices do not retain the previous 256 KB limit",
153 );
154 eq(
155 localeSearchLabels.every(([source, expected]) => source.includes(expected)),
156 true,
157 "all locales provide workspace search labels",
158 );
159
160 console.log(`\n${passed} passed, ${failed} failed`);
161 if (failed > 0) process.exit(1);
162
162 lines TYPESCRIPT