返回 CodeWhale
blue-stage-contract.test.ts
根目录 / web / lib / blue-stage-contract.test.ts
1 import { readFileSync } from "node:fs";
2 import { describe, expect, it } from "vitest";
3 import { resolveWhale } from "./whale-tokens";
4
5 const CSS = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
6
7 function selectorBlock(selector: string): string {
8 const match = CSS.match(
9 new RegExp(`(?:^|\n)${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`, "s"),
10 );
11 if (!match) throw new Error(`Missing CSS selector: ${selector}`);
12 return match[1];
13 }
14
15 // globals.css names the palette token (`--paper: var(--gpui-paper)`)
16 // rather than repeating its hex; resolve one hop through the generated
17 // app/tokens.css plus the hand-kept --gpui-* block.
18 function cssHexIn(block: string, name: string): string {
19 const match = block.match(new RegExp(`--${name}:\\s*([^;]+);`, "i"));
20 if (!match) throw new Error(`Missing CSS token in block: --${name}`);
21 const value = resolveWhale(match[1].trim());
22 if (!/^#[0-9a-f]{6}$/i.test(value)) {
23 throw new Error(`--${name} is not a hex color: ${value}`);
24 }
25 return value.toLowerCase();
26 }
27
28 const ROOT = selectorBlock(":root");
29 const BELOW_WATERLINE = selectorBlock(
30 '.ocean-column,\n.site-footer,\nhtml[data-theme="dark"] .docs-portal',
31 );
32
33 describe("GPUI public-surface contract", () => {
34 it("grounds the paper sheet in the GPUI light theme's warm paper and inks", () => {
35 // Above the waterline the field is the GPUI light background — warm
36 // paper — and the ink is its plum-charcoal foreground. The literals are
37 // the Shoreline theme constants in crates/palette/src/tokens.rs, reached
38 // through the generated tokens in app/tokens.css.
39 expect(cssHexIn(ROOT, "paper")).toBe("#f5f0e9");
40 expect(cssHexIn(ROOT, "paper-deep")).toBe("#ece5e0");
41 expect(cssHexIn(ROOT, "paper-edge")).toBe("#d7ced5");
42 expect(cssHexIn(ROOT, "paper-card")).toBe("#fffcf7");
43 expect(cssHexIn(ROOT, "ink")).toBe("#302832");
44 expect(cssHexIn(ROOT, "ink-soft")).toBe("#4a414c");
45 expect(cssHexIn(ROOT, "ink-mute")).toBe("#6b606e");
46 // Action on paper is the GPUI light primary; hover sinks to its hover.
47 expect(cssHexIn(ROOT, "indigo")).toBe("#006684");
48 expect(cssHexIn(ROOT, "indigo-deep")).toBe("#00536d");
49 expect(cssHexIn(ROOT, "mark-ink")).toBe("#302832");
50 // The deep field is always the stage's darkest, and code plates keep the
51 // stage deep on either side of the waterline.
52 expect(cssHexIn(ROOT, "ocean-deep")).toBe("#1a181c");
53 expect(cssHexIn(ROOT, "action-on-dark")).toBe("#67b8d6");
54 expect(cssHexIn(ROOT, "ocean-current")).toBe("#67b8d6");
55 expect(cssHexIn(ROOT, "code-bg")).toBe("#1a181c");
56 });
57
58 it("re-inks every dark subtree with the GPUI charcoal tokens through one rule", () => {
59 // The ocean column, the footer seabed, and the opt-in docs dark sheet
60 // share one below-the-waterline rule, so a component never needs to know
61 // which side of the surface it is on.
62 expect(CSS).toMatch(/\.ocean-column,\s*\.site-footer,\s*html\[data-theme="dark"\] \.docs-portal\s*\{/);
63 expect(cssHexIn(BELOW_WATERLINE, "paper")).toBe("#211f23");
64 expect(cssHexIn(BELOW_WATERLINE, "paper-deep")).toBe("#2b282e");
65 expect(cssHexIn(BELOW_WATERLINE, "paper-edge")).toBe("#49424d");
66 expect(cssHexIn(BELOW_WATERLINE, "ink")).toBe("#f2ece5");
67 expect(cssHexIn(BELOW_WATERLINE, "ink-soft")).toBe("#b0a7b2");
68 expect(cssHexIn(BELOW_WATERLINE, "ink-mute")).toBe("#7e7583");
69 expect(cssHexIn(BELOW_WATERLINE, "indigo")).toBe("#67b8d6");
70 expect(cssHexIn(BELOW_WATERLINE, "jade")).toBe("#9ec7b2");
71 expect(cssHexIn(BELOW_WATERLINE, "signal-gold")).toBe("#d6c78f");
72 });
73
74 it("draws the water from palette tokens only, never a hex of its own", () => {
75 const strata = readFileSync(new URL("../components/strata.tsx", import.meta.url), "utf8");
76 expect(strata).not.toMatch(/#[0-9a-f]{3,8}\b/i);
77 for (const token of ["--gpui-primary-dark", "--gpui-stage-raised", "--gpui-stage-muted", "--gpui-stage-deep", "--gpui-tan", "--ink"]) {
78 expect(strata, token).toContain(`var(${token})`);
79 }
80 // Static and decorative: no animation, hidden from assistive technology.
81 expect(strata).not.toMatch(/animate|@keyframes/);
82 expect(strata).toContain('aria-hidden="true"');
83 });
84
85 it("renders the whale mark in the sheet's mark ink while controls use action blue", () => {
86 expect(CSS).toMatch(/\.codewhale-mark-primary \{ fill: var\(--mark-ink\); \}/);
87 expect(CSS).toMatch(/\.portal-button-primary[\s\S]*background: var\(--indigo\)/);
88 expect(CSS).toMatch(/\.nav-link::after[\s\S]*background: var\(--indigo\)/);
89 });
90
91 it("keeps localized navigation controls inside compact viewports", () => {
92 const mobile = CSS.split("@media (max-width: 520px)")[1];
93
94 expect(mobile).toMatch(/\.site-nav-inner\s*\{\s*gap:\s*0\.5rem/);
95 expect(mobile).toMatch(/\.site-nav-actions\s*\{[\s\S]*?min-width:\s*0/);
96 expect(mobile).toMatch(
97 /\.site-nav-actions select\s*\{[\s\S]*?width:\s*6\.75rem;[\s\S]*?min-width:\s*0/,
98 );
99 expect(CSS).toMatch(/\.paper-wordmark-mark\s*\{[^}]*height:\s*22px;/);
100 expect(CSS).toMatch(/\.paper-wordmark-logo\s*\{[^}]*height:\s*20px;/);
101 expect(CSS).toMatch(/\.site-nav-actions\s*\{[\s\S]*?flex-shrink:\s*0/);
102 expect(CSS).toMatch(/\.site-nav-actions\s*>\s*\*\s*\{\s*flex-shrink:\s*0/);
103 expect(CSS).toMatch(/@media \(max-width: 900px\)[\s\S]*?\.site-github-link\s*\{\s*display:\s*none/);
104 expect(mobile).not.toMatch(/body:has\(\.product-home\) \.site-nav-actions select/);
105 // The locale <select> and the home wordmark must keep a usable hit
106 // target on every viewport, not only below 520px. Long native option
107 // labels and 2xl companion text used to collapse the wordmark to 0.
108 expect(CSS).toMatch(
109 /\.site-nav-actions select\s*\{\s*width:\s*6\.75rem;\s*max-width:\s*6\.75rem;\s*min-width:\s*0;/,
110 );
111 // `min-width` is the floor that keeps the wordmark clickable; the shrink
112 // factor stays at 1 so the compact controls are never the ones pushed
113 // past `overflow-x: clip` when the row is over budget.
114 expect(CSS).toMatch(/\.paper-wordmark\s*\{[^}]*flex:\s*0 1 auto;[^}]*min-width:\s*8\.75rem;/);
115 expect(CSS).not.toMatch(/\.paper-wordmark\s*\{[^}]*flex:\s*0 0 auto;/);
116 });
117 });
118
118 lines TYPESCRIPT