返回 DeepSeek-Reasonix
settings-responsive-layout.test.ts
根目录 / desktop / frontend / src / __tests__ / settings-responsive-layout.test.ts
1 // Run: tsx src/__tests__/settings-responsive-layout.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6
7 const testDir = dirname(fileURLToPath(import.meta.url));
8 const styles = readFileSync(resolve(testDir, "../styles.css"), "utf8");
9 const managementStyles = readFileSync(resolve(testDir, "../components/ManagementPageShell.css"), "utf8");
10 const panelStyles = readFileSync(resolve(testDir, "../components/SettingsPanel.css"), "utf8");
11
12 let passed = 0;
13 let failed = 0;
14
15 function eq(actual: unknown, expected: unknown, label: string) {
16 if (actual === expected) {
17 process.stdout.write(` PASS ${label}\n`);
18 passed += 1;
19 } else {
20 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`);
21 failed += 1;
22 }
23 }
24
25 function ruleBlock(source: string, selector: string): string {
26 const start = source.indexOf(`${selector} {`);
27 if (start < 0) return "";
28 const bodyStart = source.indexOf("{", start) + 1;
29 const end = source.indexOf("}", bodyStart);
30 return source.slice(bodyStart, end);
31 }
32
33 function declaration(block: string, property: string): string | undefined {
34 const match = block.match(new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`));
35 return match?.[1].trim();
36 }
37
38 console.log("\nsettings responsive layout contract");
39
40 const settingsScreen = ruleBlock(managementStyles, ".management-screen");
41 eq(declaration(settingsScreen, "position"), "fixed", "settings owns the viewport without resizing the workspace");
42 eq(declaration(settingsScreen, "inset"), "0", "settings fills the entire viewport");
43 eq(declaration(settingsScreen, "background"), "var(--bg-soft)", "settings has an opaque theme surface");
44 const windowsSettings = ruleBlock(managementStyles, ".app--windows-frameless:has(.management-screen:not([hidden]))");
45 const caption = ruleBlock(managementStyles, ":root[data-theme-style] .app--windows-frameless:has(.management-screen:not([hidden])) > .windows-window-controls");
46 eq(declaration(windowsSettings, "--management-titlebar-height"), "48px", "Windows settings owns a caption height independent of workspace style");
47 eq(declaration(ruleBlock(managementStyles, ".management-screen__chrome"), "flex"), "0 0 var(--management-titlebar-height, 44px)", "drag strip shares caption geometry while retaining macOS spacing");
48 eq(declaration(caption, "--windows-window-controls-height"), "var(--management-titlebar-height)", "Windows caption buttons match the settings drag strip");
49 eq(declaration(caption, "background"), "var(--bg-soft)", "caption controls blend with settings in every theme");
50 eq(declaration(caption, "border"), "0", "workspace borders do not leak into the settings caption");
51 const settingsCenter = ruleBlock(panelStyles, ".settings-center");
52 eq(declaration(settingsCenter, "grid-template-columns"), "clamp(220px, 20.5vw, 304px) minmax(0, 1fr)", "settings navigation remains readable without consuming the content pane");
53
54 const generalPage = ruleBlock(panelStyles, ".settings-page--general");
55 eq(declaration(generalPage, "container"), "settings-general / inline-size", "general settings respond to their available content width");
56
57 const generalContainerStart = panelStyles.indexOf("@container settings-general (max-width: 620px)");
58 const generalFallbackStart = panelStyles.indexOf("@media (max-width: 980px)", generalContainerStart);
59 const generalContainer = panelStyles.slice(generalContainerStart, generalFallbackStart);
60 const compactGeneralField = ruleBlock(generalContainer, ".settings-page--general .settings-field");
61 eq(declaration(compactGeneralField, "grid-template-columns"), "1fr", "general settings stack before controls overflow their content pane");
62 eq(declaration(compactGeneralField, "gap"), "10px", "stacked general settings keep compact vertical spacing");
63 const compactGeneralControl = ruleBlock(generalContainer, ".settings-page--general .settings-field__control");
64 eq(declaration(compactGeneralControl, "min-width"), "0", "compact general controls may shrink within the content pane");
65
66 const soundContainerStart = panelStyles.indexOf("@container settings-general (max-width: 440px)");
67 const soundContainerEnd = panelStyles.indexOf("@media (max-width: 980px)", soundContainerStart);
68 const compactSound = panelStyles.slice(soundContainerStart, soundContainerEnd);
69 const compactSoundRow = ruleBlock(compactSound, ".settings-sound-row");
70 eq(declaration(compactSoundRow, "grid-template-columns"), "minmax(0, 1fr)", "narrow sound settings stack labels above their controls");
71 eq(
72 compactSound.includes(".settings-sound-row .notification-volume-control") && compactSound.includes("width: 100%"),
73 true,
74 "notification volume expands without overflowing a narrow settings pane",
75 );
76
77 const fallbackPanel = panelStyles.slice(generalFallbackStart, panelStyles.indexOf("@media (max-width: 760px)", generalFallbackStart));
78 const fallbackGeneralField = ruleBlock(fallbackPanel, ".settings-page--general .settings-field");
79 eq(declaration(fallbackGeneralField, "grid-template-columns"), "1fr", "980px fallback stacks general settings without container queries");
80
81 const generalSectionBoundary = ruleBlock(panelStyles, ".settings-page--general > .settings-section:not(:last-child) .settings-section__body");
82 eq(
83 declaration(generalSectionBoundary, "border-bottom"),
84 "1px solid var(--border-soft)",
85 "general settings end intermediate sections with a subtle divider",
86 );
87
88 const wideTail = ruleBlock(styles, ".memory-tabs-row__tail");
89 eq(declaration(wideTail, "display"), "contents", "wide memory controls preserve the original row flex items");
90 eq(declaration(wideTail, "flex"), undefined, "wide memory wrapper does not compete in flex sizing");
91
92 const settingsResponsiveStart = styles.indexOf("/* ── responsive: compact content on narrow screens");
93 const narrowStart = styles.indexOf("@media (max-width: 900px)", settingsResponsiveStart);
94 const narrowEnd = styles.indexOf("@media (max-width: 760px)", narrowStart);
95 const narrowSettings = styles.slice(narrowStart, narrowEnd);
96 const narrowTail = ruleBlock(narrowSettings, ".memory-tabs-row__tail");
97
98 eq(declaration(narrowTail, "display"), "flex", "narrow memory controls form a flex group");
99 eq(declaration(narrowTail, "flex"), "0 1 100%", "narrow memory controls stay on their own row");
100 eq(declaration(narrowTail, "min-width"), "0", "narrow memory controls may shrink without overflowing");
101 eq(declaration(narrowTail, "gap"), "12px", "narrow memory controls retain their compact spacing");
102
103 const compactPanelStart = panelStyles.indexOf("@media (max-width: 760px)");
104 const compactPanel = panelStyles.slice(compactPanelStart);
105 const compactCenter = ruleBlock(compactPanel, ".settings-center,\n :root[data-theme-style] .settings-center");
106 eq(declaration(compactCenter, "grid-template-columns"), "minmax(0, 1fr)", "minimum-width settings use one content column");
107 eq(declaration(compactCenter, "grid-template-rows"), "auto minmax(0, 1fr)", "minimum-width navigation occupies its own top row");
108 const compactNav = ruleBlock(compactPanel, ".settings-center__nav,\n :root[data-theme-style] .settings-center__nav");
109 eq(declaration(compactNav, "width"), "100%", "minimum-width navigation spans the settings panel");
110 eq(declaration(compactNav, "overflow-x"), "auto", "minimum-width navigation scrolls horizontally");
111 eq(declaration(compactNav, "overflow-y"), "hidden", "minimum-width navigation does not create a second vertical scroller");
112
113 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
114 if (failed > 0) process.exit(1);
115
115 lines TYPESCRIPT