返回 DeepSeek-Reasonix
theme-surface-contract.mjs
根目录 / desktop / frontend / bench / theme-surface-contract.mjs
1 #!/usr/bin/env node
2
3 import fs from "node:fs";
4 import path from "node:path";
5 import { fileURLToPath } from "node:url";
6
7 const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8 process.env.PLAYWRIGHT_BROWSERS_PATH = !process.env.PLAYWRIGHT_BROWSERS_PATH || process.env.PLAYWRIGHT_BROWSERS_PATH === ".pw-browsers"
9 ? path.join(frontendDir, ".pw-browsers")
10 : process.env.PLAYWRIGHT_BROWSERS_PATH;
11 const { chromium } = await import("playwright");
12
13 const styles = fs.readFileSync(path.join(frontendDir, "src", "styles.css"), "utf8");
14 const themeStyles = ["graphite", "aurora", "slate", "carbon", "nocturne", "amber"];
15 const modes = [
16 { name: "forced dark", attribute: "dark", media: "dark" },
17 { name: "forced light", attribute: "light", media: "dark" },
18 { name: "auto dark", attribute: null, media: "dark" },
19 { name: "auto light", attribute: null, media: "light" },
20 ];
21 const overlaySelectors = [
22 ".anchored-popover",
23 ".slashmenu",
24 ".composer-access-menu",
25 ".jobs-popover",
26 ".modelsw__menu",
27 ".workspace-recent-menu",
28 ".floating-menu",
29 ".mem-ws-select__menu",
30 ".menu",
31 ".settings-select-menu",
32 ".external-opener__menu",
33 ".context-menu",
34 ".theme-gallery__menu",
35 ".theme-gallery__editor",
36 ".theme-editor__header",
37 ".theme-editor__live",
38 ".provider-access-more__menu",
39 ".topicbar__export-menu",
40 ".heartbeat-filter-menu",
41 ".heartbeat-project-menu",
42 ".heartbeat-task-menu",
43 ];
44 const surfaceSelectors = [
45 ".msg-pasted-block",
46 ".composer__pasted-block",
47 ".mem-tab-select",
48 ".mermaid-diagram__error",
49 ];
50 const fixtures = [...new Set([...overlaySelectors, ...surfaceSelectors])]
51 .map((selector) => `<div class="${selector.slice(1)}">surface</div>`)
52 .join("");
53
54 const amberExpected = {
55 dark: {
56 "--bg": "#090a0c",
57 "--bg-soft": "#111319",
58 "--panel": "#191b22",
59 "--sidebar-bg": "#0c0e12",
60 "--fg": "#f4f5f7",
61 "--fg-dim": "#c0c4cc",
62 "--accent": "#d4632f",
63 "--border": "#343945",
64 },
65 light: {
66 "--bg": "#f7f8fb",
67 "--bg-soft": "#eef2f7",
68 "--panel": "#ffffff",
69 "--sidebar-bg": "#f3f6fa",
70 "--fg": "#111827",
71 "--fg-dim": "#4b5563",
72 "--accent": "#dd5b28",
73 "--border": "#d8dee8",
74 },
75 };
76
77 function assert(condition, message) {
78 if (!condition) throw new Error(message);
79 process.stdout.write(` PASS ${message}\n`);
80 }
81
82 let browser;
83 try {
84 browser = await chromium.launch({ headless: true });
85 const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
86 await page.setContent(`<!doctype html><html><head><style>${styles}</style></head><body>${fixtures}<div class="transcript__loading">muted</div></body></html>`);
87
88 for (const style of themeStyles) {
89 for (const mode of modes) {
90 await page.emulateMedia({ colorScheme: mode.media });
91 await page.evaluate(({ style, attribute }) => {
92 const root = document.documentElement;
93 root.setAttribute("data-theme-style", style);
94 if (attribute) root.setAttribute("data-theme", attribute);
95 else root.removeAttribute("data-theme");
96 root.removeAttribute("data-theme-pack");
97 root.removeAttribute("data-theme-has-bg");
98 root.removeAttribute("data-theme-scene");
99 root.style.removeProperty("--theme-pane-overlay-pct");
100 root.style.removeProperty("--theme-pane-task-overlay-pct");
101 }, { style, attribute: mode.attribute });
102
103 const result = await page.evaluate(({ overlaySelectors, surfaceSelectors }) => {
104 function colorAlpha(value) {
105 const canvas = document.createElement("canvas");
106 canvas.width = 1;
107 canvas.height = 1;
108 const context = canvas.getContext("2d");
109 context.clearRect(0, 0, 1, 1);
110 context.fillStyle = value;
111 context.fillRect(0, 0, 1, 1);
112 return context.getImageData(0, 0, 1, 1).data[3] / 255;
113 }
114 const alpha = (selector) => colorAlpha(getComputedStyle(document.querySelector(selector)).backgroundColor);
115 const muted = document.querySelector(".transcript__loading");
116 const probe = document.createElement("span");
117 probe.style.color = "var(--fg-faint)";
118 document.body.append(probe);
119 const mutedColor = getComputedStyle(muted).color;
120 const expectedMutedColor = getComputedStyle(probe).color;
121 probe.remove();
122 return {
123 overlays: Object.fromEntries(overlaySelectors.map((selector) => [selector, alpha(selector)])),
124 surfaces: Object.fromEntries(surfaceSelectors.map((selector) => [selector, alpha(selector)])),
125 mutedColor,
126 expectedMutedColor,
127 };
128 }, { overlaySelectors, surfaceSelectors });
129
130 for (const [selector, alpha] of Object.entries(result.overlays)) {
131 assert(alpha >= 0.999, `${style} ${mode.name} keeps ${selector} opaque (${alpha})`);
132 }
133 for (const [selector, alpha] of Object.entries(result.surfaces)) {
134 assert(alpha > 0, `${style} ${mode.name} resolves ${selector} background (${alpha})`);
135 }
136 assert(result.mutedColor === result.expectedMutedColor, `${style} ${mode.name} resolves muted text to --fg-faint`);
137
138 if (style === "amber") {
139 const expected = amberExpected[mode.media === "light" || mode.attribute === "light" ? "light" : "dark"];
140 const colors = await page.evaluate((entries) => {
141 const result = {};
142 for (const [token] of entries) {
143 const probe = document.createElement("span");
144 probe.style.backgroundColor = `var(${token})`;
145 document.body.append(probe);
146 result[token] = getComputedStyle(probe).backgroundColor;
147 probe.remove();
148 }
149 return result;
150 }, Object.entries(expected));
151 const expectedColors = await page.evaluate((entries) => {
152 const result = {};
153 for (const [token, value] of entries) {
154 const probe = document.createElement("span");
155 probe.style.backgroundColor = value;
156 document.body.append(probe);
157 result[token] = getComputedStyle(probe).backgroundColor;
158 probe.remove();
159 }
160 return result;
161 }, Object.entries(expected));
162 for (const token of Object.keys(expected)) {
163 assert(colors[token] === expectedColors[token], `Amber ${mode.name} ${token} matches preview (${colors[token]})`);
164 }
165 }
166 }
167 }
168
169 await page.emulateMedia({ colorScheme: "dark" });
170 for (const [paneOpacity, expectedAlpha] of [[0, 0.4], [0.5, 0.9], [1, 1]]) {
171 for (const scene of ["home", "task"]) {
172 const alphas = await page.evaluate(({ overlaySelectors, paneOpacity, scene }) => {
173 const root = document.documentElement;
174 const percentage = `${Math.min((paneOpacity + 0.4) * 100, 100)}%`;
175 root.setAttribute("data-theme-style", "graphite");
176 root.setAttribute("data-theme", "dark");
177 root.setAttribute("data-theme-pack", "surface-contract");
178 root.setAttribute("data-theme-has-bg", "true");
179 root.setAttribute("data-theme-scene", scene);
180 root.style.setProperty("--theme-pane-overlay-pct", percentage);
181 root.style.setProperty("--theme-pane-task-overlay-pct", percentage);
182 return Object.fromEntries(overlaySelectors.map((selector) => {
183 const value = getComputedStyle(document.querySelector(selector)).backgroundColor;
184 const canvas = document.createElement("canvas");
185 canvas.width = 1;
186 canvas.height = 1;
187 const context = canvas.getContext("2d");
188 context.clearRect(0, 0, 1, 1);
189 context.fillStyle = value;
190 context.fillRect(0, 0, 1, 1);
191 return [selector, context.getImageData(0, 0, 1, 1).data[3] / 255];
192 }));
193 }, { overlaySelectors, paneOpacity, scene });
194 for (const [selector, alpha] of Object.entries(alphas)) {
195 assert(
196 Math.abs(alpha - expectedAlpha) <= 1 / 255,
197 `${scene} ${selector} follows paneOpacity ${paneOpacity} (${alpha})`,
198 );
199 }
200 }
201 }
202
203 process.stdout.write("\nTheme surface browser contract passed.\n");
204 } finally {
205 await browser?.close();
206 }
207
207 lines Plain Text