| 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 |