返回 DeepSeek-Reasonix
code-theme.mjs
根目录 / desktop / frontend / bench / code-theme.mjs
1 import assert from "node:assert/strict";
2 import { mkdtemp, rm } from "node:fs/promises";
3 import { tmpdir } from "node:os";
4 import path from "node:path";
5 import { fileURLToPath } from "node:url";
6 import { build, preview, loadConfigFromFile } from "vite";
7
8 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9 process.env.PLAYWRIGHT_BROWSERS_PATH ||= path.join(root, ".pw-browsers");
10 const { chromium } = await import("playwright");
11 const outDir = await mkdtemp(path.join(tmpdir(), "reasonix-code-theme-"));
12 const { config } = await loadConfigFromFile({ command: "build", mode: "production" }, path.join(root, "vite.config.ts"));
13 let server, browser;
14 try {
15 await build({ ...config, configFile: false, root, logLevel: "error",
16 plugins: config.plugins.filter(plugin => !["archive-hidden-sourcemaps", "keep-dist-placeholder"].includes(plugin?.name)),
17 build: { ...config.build, outDir, sourcemap: false,
18 rolldownOptions: { ...config.build.rolldownOptions, input: path.join(root, "bench/code-theme.html") } } });
19 server = await preview({ configFile: false, root, logLevel: "error", build: { outDir }, preview: { host: "127.0.0.1", port: 0 } });
20 browser = await chromium.launch({ headless: true });
21 const page = await browser.newPage({ viewport: { width: 1100, height: 900 }, permissions: ["clipboard-read", "clipboard-write"] });
22 const errors = [];
23 page.on("pageerror", error => errors.push(error.message));
24 await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/bench/code-theme.html?mock=1`);
25 const live = page.getByRole("region", { name: "Streaming code" });
26 await live.locator(".hljs-keyword").first().waitFor();
27 const prefix = await live.locator("pre").textContent();
28 await live.locator(".code-block__line").first().evaluate(el => {
29 el.dataset.retained = "yes";
30 const range = document.createRange(); range.selectNodeContents(el);
31 window.getSelection().removeAllRanges(); window.getSelection().addRange(range);
32 });
33 await page.getByRole("button", { name: "追加代码" }).click();
34 await live.getByText("count", { exact: true }).count();
35 await page.waitForFunction(() => document.querySelector('[aria-label="Streaming code"] pre').textContent.includes("42"));
36 assert.equal(await live.locator(".code-block__line").first().getAttribute("data-retained"), "yes");
37 assert.ok((await live.locator("pre").textContent()).startsWith(prefix));
38 await page.getByRole("button", { name: "追加代码" }).click();
39 await live.getByRole("button", { name: /Copy|复制/, exact: false }).click();
40 const complete = await live.locator("pre").textContent();
41 assert.equal(await page.evaluate(() => navigator.clipboard.readText()), complete, "copy excludes header and fence markers");
42 await page.getByRole("button", { name: "追加代码" }).click();
43 await page.waitForFunction(() => document.querySelector('[aria-label="Streaming code"] .hljs-keyword'));
44 assert.equal(await live.locator("pre").textContent(), complete, "settlement preserves code text");
45
46 async function checkContrast(label) {
47 await page.locator('[aria-label="Completed code"] .hljs-string').first().waitFor();
48 const colors = await page.locator('[aria-label="Completed code"] [class^="hljs-"]').evaluateAll(tokens =>
49 [...new Set(tokens.map(token => getComputedStyle(token).color))]);
50 assert.ok(colors.length >= 4, `${label}: syntax roles must actually have distinct colors, got ${colors}`);
51 const measurements = await page.evaluate(() => {
52 const luminance = color => {
53 const rgb = color.match(/[\d.]+/g).slice(0, 3).map(Number).map(v => {
54 v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
55 });
56 return rgb[0] * .2126 + rgb[1] * .7152 + rgb[2] * .0722;
57 };
58 const ratio = (a, b) => (Math.max(a, b) + .05) / (Math.min(a, b) + .05);
59 return [...document.querySelectorAll(".code-block--framed")].flatMap(block => {
60 const pre = block.querySelector("pre");
61 const header = block.querySelector(".code-block__header");
62 return [...pre.querySelectorAll('[class^="hljs-"]'), pre, header, header.querySelector("button")].map(el => {
63 const bg = getComputedStyle(el.closest("pre") ?? el).backgroundColor;
64 return { token: el.className, bg, ratio: ratio(luminance(getComputedStyle(el).color), luminance(bg)) };
65 });
66 });
67 });
68 for (const value of measurements) {
69 assert.ok(!value.bg.startsWith("rgba"), `${label} ${value.token}: code surface must be opaque`);
70 assert.ok(value.ratio >= 4.49, `${label} ${value.token}: contrast ${value.ratio}`);
71 }
72 return Math.min(...measurements.map(value => value.ratio));
73 }
74 let minimum = Infinity;
75 let cases = 0;
76 for (const style of ["graphite", "aurora", "slate", "carbon", "nocturne", "amber"]) {
77 await page.getByLabel("Style", { exact: true }).selectOption(style);
78 for (const [mode, system] of [["light", "dark"], ["dark", "light"], ["auto", "light"], ["auto", "dark"]]) {
79 await page.emulateMedia({ colorScheme: system });
80 await page.getByLabel("Mode", { exact: true }).selectOption(mode);
81 minimum = Math.min(minimum, await checkContrast(`${style}/${mode}/${system}`)); cases++;
82 }
83 }
84 for (const pack of ["inverted", "midtone", "translucent", "wallpaper"]) {
85 await page.getByLabel("Palette", { exact: true }).selectOption(pack);
86 for (const mode of ["light", "dark"]) {
87 await page.getByLabel("Mode", { exact: true }).selectOption(mode);
88 minimum = Math.min(minimum, await checkContrast(`${pack}/${mode}`)); cases++;
89 }
90 }
91 await page.getByLabel("Scene", { exact: true }).selectOption("home");
92 minimum = Math.min(minimum, await checkContrast("wallpaper/home")); cases++;
93 const spacing = await page.locator(".code-block--framed pre").first().evaluate(el => getComputedStyle(el).margin);
94 assert.equal(spacing, "0px", "Markdown default pre margins cannot split the framed surface");
95 await page.setViewportSize({ width: 390, height: 844 });
96 const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth);
97 assert.equal(overflow, false, "narrow viewport does not overflow horizontally");
98 assert.deepEqual(errors, []);
99 console.log(JSON.stringify({ cases, minimumContrast: minimum, streaming: "passed", copy: "passed", narrow: "passed", errors }));
100 } finally {
101 await browser?.close();
102 await new Promise(resolve => server ? server.httpServer.close(resolve) : resolve());
103 await rm(outDir, { recursive: true, force: true });
104 }
105
105 lines Plain Text