返回 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
4 const CSS = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
5 const TUI_TOKENS = readFileSync(
6 new URL("../../crates/tui/src/palette/tokens.rs", import.meta.url),
7 "utf8",
8 );
9
10 function rustRgb(name: string): string {
11 const match = TUI_TOKENS.match(
12 new RegExp(`pub const ${name}_RGB:[^=]+\\= \\((\\d+), (\\d+), (\\d+)\\)`),
13 );
14 if (!match) throw new Error(`Missing Rust RGB token: ${name}_RGB`);
15 return `#${match
16 .slice(1)
17 .map((channel) => Number(channel).toString(16).padStart(2, "0"))
18 .join("")}`;
19 }
20
21 function cssHex(name: string): string {
22 const match = CSS.match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
23 if (!match) throw new Error(`Missing CSS token: --${name}`);
24 return match[1].toLowerCase();
25 }
26
27 describe("Blue Stage public-surface contract", () => {
28 it("shares the TUI stage, action, human, and structural dark tokens", () => {
29 expect(cssHex("ocean-deep")).toBe(rustRgb("WHALE_BG"));
30 expect(cssHex("action-on-dark")).toBe(rustRgb("WHALE_ACTION"));
31 expect(cssHex("signal-gold")).toBe(rustRgb("WHALE_HUMAN"));
32 expect(cssHex("ocean-current")).toBe(rustRgb("WHALE_ICE"));
33 });
34
35 it("shares the TUI light surface, text, and interaction tokens", () => {
36 expect(cssHex("paper")).toBe(rustRgb("LIGHT_SURFACE"));
37 expect(cssHex("paper-deep")).toBe(rustRgb("LIGHT_ELEVATED"));
38 expect(cssHex("ink")).toBe(rustRgb("LIGHT_TEXT_BODY"));
39 expect(cssHex("indigo")).toBe(rustRgb("LIGHT_ACTION"));
40 });
41
42 it("reserves Signal Gold for the whale while controls use action blue", () => {
43 expect(CSS).toMatch(/\.codewhale-mark-primary \{ fill: var\(--signal-gold\); \}/);
44 expect(CSS).toMatch(/\.portal-button-primary[\s\S]*background: var\(--indigo-deep\)/);
45 expect(CSS).toMatch(/\.nav-link::after[\s\S]*background: var\(--indigo\)/);
46 });
47
48 it("keeps the shared mobile navigation inside a 390px viewport", () => {
49 const mobile = CSS.split("@media (max-width: 520px)")[1];
50
51 expect(mobile).toMatch(/\.site-nav-inner\s*\{\s*gap:\s*0\.5rem/);
52 expect(mobile).toMatch(/\.site-nav-actions\s*\{[\s\S]*?min-width:\s*0/);
53 expect(mobile).toMatch(
54 /\.site-nav-actions select\s*\{[\s\S]*?width:\s*8rem;[\s\S]*?min-width:\s*0/,
55 );
56 expect(mobile).not.toMatch(/body:has\(\.product-home\) \.site-nav-actions select/);
57 });
58 });
59
59 lines TYPESCRIPT