返回 DeepSeek-Reasonix
composer-workspace-frame-css.test.ts
根目录 / desktop / frontend / src / __tests__ / composer-workspace-frame-css.test.ts
1 // Run: tsx src/__tests__/composer-workspace-frame-css.test.ts
2 //
3 // Contract: the new-session workspace selector and composer share one outline.
4 // The child surfaces provide only the internal separator so focus/running
5 // accents cannot begin halfway down the combined control.
6
7 import { readFileSync } from "node:fs";
8 import { dirname, resolve } from "node:path";
9 import { fileURLToPath } from "node:url";
10
11 const testDir = dirname(fileURLToPath(import.meta.url));
12 const styles = [
13 readFileSync(resolve(testDir, "../styles.css"), "utf8"),
14 readFileSync(resolve(testDir, "../components/ComposerWorkspaceContextBar.css"), "utf8"),
15 ].join("\n").replace(/\/\*[\s\S]*?\*\//g, "");
16 const composerSource = readFileSync(resolve(testDir, "../components/Composer.tsx"), "utf8");
17
18 let passed = 0;
19 let failed = 0;
20
21 function ok(value: unknown, label: string) {
22 if (value) {
23 process.stdout.write(` PASS ${label}\n`);
24 passed += 1;
25 } else {
26 process.stdout.write(` FAIL ${label}\n`);
27 failed += 1;
28 }
29 }
30
31 function matchingBlocks(selector: string): string[] {
32 const blocks: string[] = [];
33 const rule = /([^{}]+)\{([^{}]*)\}/g;
34 let match: RegExpExecArray | null;
35 while ((match = rule.exec(styles)) !== null) {
36 const selectors = match[1].split(",").map((part) => part.trim());
37 if (selectors.includes(selector)) blocks.push(match[2]);
38 }
39 return blocks;
40 }
41
42 function finalDeclaration(selector: string, property: string): string | undefined {
43 let value: string | undefined;
44 for (const block of matchingBlocks(selector)) {
45 const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g");
46 let match: RegExpExecArray | null;
47 while ((match = declaration.exec(block)) !== null) value = match[1].trim();
48 }
49 return value;
50 }
51
52 console.log("\ncomposer workspace frame css");
53
54 ok(composerSource.includes("composer-workspace-frame--context"), "workspace context mounts a shared frame");
55 ok(composerSource.includes("!workspaceContext && running"), "plain composers keep ownership of their existing running accent");
56 ok(finalDeclaration(".composer-workspace-frame--context", "border-radius") === "16px", "shared frame owns the complete rounded outline");
57 ok(finalDeclaration(".composer-workspace-frame--context .composer-workspace-bar", "border") === "0", "workspace bar does not draw a competing outer border");
58 ok(finalDeclaration(".composer-workspace-frame--context .composer-workspace-bar", "border-bottom") === "1px solid var(--border-soft)", "workspace bar keeps only the internal separator");
59 ok(finalDeclaration(":root[data-theme-style] .composer-workspace-frame--context .composer-card", "border") === "0", "theme cascade cannot restore the composer card outer border");
60 ok(finalDeclaration(":root[data-theme-style] .composer-workspace-frame--context .composer-card", "border-radius") === "0 0 15px 15px", "theme cascade cannot restore the composer card top corners");
61 ok(finalDeclaration(":root[data-theme-style] .composer-workspace-frame--context .composer-card::before", "display") === "none", "composer card focus outline is handed to the shared frame");
62 ok(finalDeclaration(":root[data-theme-style] .composer-workspace-frame--context .composer-card::after", "display") === "none", "composer card animated outline is handed to the shared frame");
63
64 console.log(`\n${passed} passed, ${failed} failed`);
65 if (failed > 0) process.exit(1);
66
66 lines TYPESCRIPT