返回 CodeWhale
docs-theme-contract.test.ts
根目录 / web / lib / docs-theme-contract.test.ts
1 import { readFileSync } from "node:fs";
2 import { describe, expect, it } from "vitest";
3
4 const CSS = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
5
6 function selectorBlock(selector: string): string {
7 const match = CSS.match(new RegExp(`${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`, "s"));
8 if (!match) throw new Error(`Missing CSS selector: ${selector}`);
9 return match[1];
10 }
11
12 function customProperty(block: string, name: string): string {
13 const match = block.match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
14 if (!match) throw new Error(`Missing custom property: --${name}`);
15 return match[1];
16 }
17
18 function relativeLuminance(hex: string): number {
19 const channels = hex
20 .slice(1)
21 .match(/.{2}/g)!
22 .map((value) => Number.parseInt(value, 16) / 255)
23 .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4));
24
25 return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
26 }
27
28 function contrastRatio(foreground: string, background: string): number {
29 const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
30 const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
31 return (lighter + 0.05) / (darker + 0.05);
32 }
33
34 describe("docs dark-theme contrast contract", () => {
35 it("keeps current and hover sidebar text at WCAG AA contrast", () => {
36 const darkThemes = [
37 selectorBlock('html:not([data-theme="light"]) .docs-theme'),
38 selectorBlock('[data-theme="dark"] .docs-theme'),
39 ];
40
41 for (const dark of darkThemes) {
42 const accent = customProperty(dark, "docs-accent");
43 const background = customProperty(dark, "paper");
44 expect(contrastRatio(accent, background)).toBeGreaterThanOrEqual(4.5);
45 }
46 expect(CSS).toMatch(/\.docs-sidebar-link:hover,\s*\.docs-sidebar-link-current\s*{[^}]*color:\s*var\(--docs-accent\)/s);
47 });
48
49 it("keeps secondary button text at WCAG AA contrast", () => {
50 const darkThemes = [
51 selectorBlock('html:not([data-theme="light"]) .docs-theme'),
52 selectorBlock('[data-theme="dark"] .docs-theme'),
53 ];
54
55 for (const dark of darkThemes) {
56 const text = customProperty(dark, "docs-button-text");
57 const background = customProperty(dark, "docs-button-bg");
58 expect(contrastRatio(text, background)).toBeGreaterThanOrEqual(4.5);
59 }
60 expect(selectorBlock(".docs-theme .portal-button-secondary")).toContain(
61 "color: var(--docs-button-text)",
62 );
63 });
64 });
65
65 lines TYPESCRIPT