返回 DeepSeek-Reasonix
composer-autosize-scrollbar-css.test.ts
根目录 / desktop / frontend / src / __tests__ / composer-autosize-scrollbar-css.test.ts
1 // Run: tsx src/__tests__/composer-autosize-scrollbar-css.test.ts
2 //
3 // Contract: in autosize mode the composer textarea hides its scrollbar while
4 // the content fits, but once the content exceeds the max height (the JS
5 // inline overflow-y flips to auto and the card gains
6 // `.composer-card--auto-overflow`) the thin scrollbar reappears so long
7 // drafts expose their scrollability (#8494/#8742/#9019). The hero (creation)
8 // input cap is min(30vh, 160px), not the old 96px hard clip.
9
10 import { readFileSync } from "node:fs";
11 import { dirname, resolve } from "node:path";
12 import { fileURLToPath } from "node:url";
13
14 const testDir = dirname(fileURLToPath(import.meta.url));
15 // Strip comments so declaration parsing never matches prose inside them.
16 const styles = readFileSync(resolve(testDir, "../styles.css"), "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
17 const composerSource = readFileSync(resolve(testDir, "../components/Composer.tsx"), "utf8");
18
19 let passed = 0;
20 let failed = 0;
21
22 function ok(value: boolean, label: string) {
23 if (value) {
24 process.stdout.write(` PASS ${label}\n`);
25 passed += 1;
26 } else {
27 process.stdout.write(` FAIL ${label}\n`);
28 failed += 1;
29 }
30 }
31
32 function eq(a: unknown, b: unknown, label: string) {
33 if (a === b) {
34 ok(true, label);
35 } else {
36 ok(false, `${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
37 }
38 }
39
40 function matchingBlocks(selector: string): string[] {
41 const blocks: string[] = [];
42 const rule = /([^{}]+)\{([^{}]*)\}/g;
43 let match: RegExpExecArray | null;
44 while ((match = rule.exec(styles)) !== null) {
45 const selectors = match[1].split(",").map((part) => part.trim());
46 if (selectors.includes(selector)) blocks.push(match[2]);
47 }
48 return blocks;
49 }
50
51 function finalDeclaration(selector: string, property: string): string | undefined {
52 let value: string | undefined;
53 for (const block of matchingBlocks(selector)) {
54 const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g");
55 let match: RegExpExecArray | null;
56 while ((match = declaration.exec(block)) !== null) {
57 value = match[1].trim();
58 }
59 }
60 return value;
61 }
62
63 console.log("\ncomposer autosize scrollbar css");
64
65 eq(finalDeclaration(".composer__input", "scrollbar-width"), "none", "autosize textarea hides the scrollbar while content fits");
66 eq(finalDeclaration(".composer__input::-webkit-scrollbar", "display"), "none", "autosize textarea hides the webkit scrollbar while content fits");
67 eq(finalDeclaration(".composer-card--auto-overflow .composer__input", "scrollbar-width"), "thin", "overflowing autosize textarea restores the thin scrollbar");
68 eq(finalDeclaration(".composer-card--auto-overflow .composer__input::-webkit-scrollbar", "display"), "block", "overflowing autosize textarea restores the webkit scrollbar");
69 eq(finalDeclaration(".composer-card--auto-overflow .composer__input::-webkit-scrollbar", "width"), "6px", "overflowing autosize scrollbar matches the resized-card width");
70 eq(
71 finalDeclaration(".app--creation .composer-wrap--hero .composer__input", "max-height"),
72 "min(30vh, 160px) !important",
73 "hero input cap is min(30vh, 160px) instead of the 96px hard clip",
74 );
75 eq(
76 finalDeclaration(".composer__input.composer__input--measure", "position"),
77 "absolute",
78 "autosize mirror stays outside the composer layout flow",
79 );
80 eq(
81 finalDeclaration(".composer__input.composer__input--measure", "visibility"),
82 "hidden",
83 "autosize mirror is never visible to the user",
84 );
85
86 ok(
87 /composerAutoOverflow = composerHeight === null && textareaAutoOverflow/.test(composerSource)
88 && /composerAutoOverflow \? " composer-card--auto-overflow" : ""/.test(composerSource),
89 "Composer wires the auto-overflow modifier to the autosize overflow state",
90 );
91 ok(
92 /Math\.min\(Math\.floor\(window\.innerHeight \* 0\.3\), 160\)/.test(composerSource),
93 "hero input JS cap is min(30vh, 160px)",
94 );
95 ok(
96 /const measureTaRef = useRef<HTMLTextAreaElement>\(null\)/.test(composerSource)
97 && /ref=\{measureTaRef\}[\s\S]*?value=\{text\}[\s\S]*?aria-hidden="true"/.test(composerSource),
98 "Composer renders a text-synchronized, accessibility-hidden measurement mirror",
99 );
100 ok(
101 !/\.style\.height\s*=\s*"auto"/.test(composerSource),
102 "autosize measurement never collapses the live textarea",
103 );
104
105 console.log(`\n${passed} passed, ${failed} failed`);
106 if (failed > 0) process.exit(1);
107
107 lines TYPESCRIPT