/** * The design-system contract. * * `theme.test.ts` checks the visibility of document marks. These tests check * the system those marks sit inside: resolved colours, scale membership and * whether every consumer has left the vocabulary this system replaces. */ import { describe, expect, it } from "bun:test"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; type Oklch = { l: number; c: number; h: number }; const ROOT = join(import.meta.dir, "../../.."); const SHARED_THEME = readFileSync(join(ROOT, "packages/visuals/theme.css"), "utf8"); const WEB_THEME = readFileSync(join(import.meta.dir, "theme.css"), "utf8"); const THEME = `${SHARED_THEME}\n${WEB_THEME}`; const EDITOR_STYLES = readFileSync(join(ROOT, "packages/editor/src/styles.css"), "utf8"); describe("theme ownership", () => { it("loads the design tokens from the shared visuals package", () => { let sharedTheme = join(ROOT, "packages/visuals/theme.css"); expect(existsSync(sharedTheme)).toBe(true); expect(SHARED_THEME).toContain("@theme static"); expect(WEB_THEME).toContain('@import "@chopin/visuals/theme.css";'); expect(WEB_THEME).not.toContain("@theme static"); }); }); function declared(name: string): string { let found = new RegExp(`\\n\\s*${name}:\\s*([^;]+);`).exec(THEME); if (!found) throw new Error(`no ${name} in the theme`); return found[1]!.trim().replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")"); } function resolved(name: string): string { let value = declared(name); let alias = /^var\((--[\w-]+)\)$/.exec(value); return alias ? resolved(alias[1]!) : value; } function colour(name: string): Oklch { let found = /^oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)\)$/.exec(resolved(name)); if (!found) throw new Error(`${name} is not an opaque oklch colour`); return { l: Number(found[1]), c: Number(found[2]), h: Number(found[3]) }; } function linearChannels(value: Oklch): number[] { let radians = (value.h * Math.PI) / 180; let a = value.c * Math.cos(radians); let b = value.c * Math.sin(radians); let long = (value.l + 0.3963377774 * a + 0.2158037573 * b) ** 3; let medium = (value.l - 0.1055613458 * a - 0.0638541728 * b) ** 3; let short = (value.l - 0.0894841775 * a - 1.291485548 * b) ** 3; return [ 4.0767416621 * long - 3.3077115913 * medium + 0.2309699292 * short, -1.2684380046 * long + 2.6097574011 * medium - 0.3413193965 * short, -0.0041960863 * long - 0.7034186147 * medium + 1.707614701 * short, ]; } function hex(name: string): string { let channels = linearChannels(colour(name)); return `#${ channels.map(channel => { let encoded = channel <= 0.0031308 ? 12.92 * channel : 1.055 * Math.pow(Math.max(channel, 0), 1 / 2.4) - 0.055; return Math.round(Math.min(1, Math.max(0, encoded)) * 255) .toString(16) .padStart(2, "0"); }).join("") }`; } function luminance(name: string): number { let [red, green, blue] = linearChannels(colour(name)); return 0.2126 * red! + 0.7152 * green! + 0.0722 * blue!; } function contrast(foreground: string, background: string): number { let lighter = Math.max(luminance(foreground), luminance(background)); let darker = Math.min(luminance(foreground), luminance(background)); return (lighter + 0.05) / (darker + 0.05); } function sources(dir: string, found: string[] = []): string[] { for (let entry of readdirSync(dir)) { if (entry === "node_modules" || entry === "dist" || entry.startsWith(".")) continue; let path = join(dir, entry); if (statSync(path).isDirectory()) sources(path, found); else if (entry.endsWith(".tsx") || entry.endsWith(".css")) found.push(path); } return found; } function withoutComments(source: string): string { return source.replace(/\/\*[\s\S]*?\*\//g, ""); } function utility(name: string): string { let start = THEME.indexOf(`@utility ${name} {`); if (start === -1) throw new Error(`no ${name} utility in the theme`); let depth = 0; for (let index = THEME.indexOf("{", start); index < THEME.length; index++) { if (THEME[index] === "{") depth++; if (THEME[index] === "}") depth--; if (depth === 0) return THEME.slice(start, index + 1); } throw new Error(`unterminated ${name} utility in the theme`); } function sizes(name: string): { height: number; width?: number } { let result: { height: number; width?: number } = { height: 0 }; for (let property of ["height", "width"] as const) { let found = new RegExp(`\\n\\s*${property}:\\s*([^;]+);`).exec(utility(name)); if (!found) continue; let value = found[1]!.replace(/var\((--[\w-]+)\)/g, (_, token) => resolved(token)); let length = /^([\d.]+)rem$/.exec(value); let scaled = /^calc\(var\(--spacing\) \* ([\d.]+)\)$/.exec(value); if (length) result[property] = Number(length[1]) * 16; else if (scaled) { result[property] = Number.parseFloat(resolved("--spacing")) * 16 * Number(scaled[1]); } else throw new Error(`unsupported ${name} ${property}: ${value}`); } return result; } function inlinePadding(name: string): string | undefined { let found = /\n\s*padding-inline:\s*([^;]+);/.exec(utility(name)); return found?.[1]?.trim(); } describe("palette", () => { let steps = [50, 100, 150, 200, 300, 400, 450, 500, 600, 700, 750, 800, 900]; it("keeps all thirteen neutral steps on the warm olive axis", () => { for (let step of steps) expect(colour(`--color-gray-${step}`).h).toBe(95); }); it("resolves the settled specimens exactly", () => { expect(hex("--color-gray-100")).toBe("#f8f8f6"); expect(hex("--color-gray-150")).toBe("#f4f4f1"); expect(hex("--color-brand")).toBe("#06707e"); expect(hex("--color-destructive")).toBe("#d54d4c"); }); it("keeps the Chat body ink distinct from the general secondary text", () => { expect(declared("--color-chat-body")).toBe("oklch(0.38064 0.01903 136.21)"); }); it("washes the Chat pane with thirty percent of the app shell", () => { expect(declared("--color-chat-pane")).toBe( "color-mix(in srgb, var(--color-ground) 30%, var(--color-page))", ); }); it("tunes the light semantic washes independently", () => { expect(declared("--color-brand-wash")).toBe("oklch(0.925 0.02 210)"); expect(declared("--color-success-wash")).toBe("oklch(0.93 0.026 145)"); expect(declared("--color-warning-wash")).toBe("oklch(0.95 0.032 75)"); expect(declared("--color-destructive-wash")).toBe( "color-mix(in srgb, var(--color-destructive) 20%, var(--color-page))", ); }); it("keeps the Chat pane edge subtly stronger than the frame hairline", () => { expect(THEME).toMatch( /\.workspace-frame \.workspace-chat-panel\s*\{\s*border-color:\s*var\(--color-divider\);/, ); expect(declared("--color-divider")).toBe("rgb(0 0 0 / 9%)"); }); it("gives every text level at least AA contrast on the page", () => { for (let level of ["primary", "secondary", "tertiary", "quaternary"]) { let ratio = contrast(`--color-text-${level}`, "--color-page"); expect({ level, passes: ratio >= 4.5 }).toEqual({ level, passes: true }); } }); it("keeps text used on tinted surfaces at AA contrast", () => { for (let surface of ["ground", "hover", "selected"]) { let ratio = contrast("--color-text-tertiary", `--color-${surface}`); expect({ surface, passes: ratio >= 4.5 }).toEqual({ surface, passes: true }); } }); it("uses selected as the single passive control fill", () => { expect(THEME).not.toMatch(/\n\s*--color-control(?:-hover)?:/); expect(EDITOR_STYLES).not.toContain("var(--color-control)"); for ( let source of sources(join(ROOT, "apps/web/src")).concat(sources(join(ROOT, "packages"))) ) { expect(withoutComments(readFileSync(source, "utf8"))).not.toMatch(/\bbg-control\b/); } }); it("keeps petrol as the only blue family", () => { let blue = [...THEME.matchAll(/\n\s*(--color-[\w-]+):\s*oklch\([\d.]+\s+[\d.]+\s+([\d.]+)/g)] .filter(match => Number(match[2]) >= 180 && Number(match[2]) <= 270) .map(match => match[1]); expect(blue).toEqual([ "--color-brand", "--color-brand-hover", "--color-brand-active", "--color-brand-wash", "--color-brand-ink", ]); }); }); describe("type", () => { it("uses the designed fluid modular steps at both viewport endpoints", () => { let steps = [ ["2xs", -2.5], ["xs", -2], ["sm", -1], ["base", 0], ["lg", 2], ["xl", 3], ["2xl", 5], ] as const; let found = [...SHARED_THEME.matchAll(/\n\s*(--text-(?![\w-]*--line-height)[\w-]+):/g)] .map(match => match[1]); expect(found).toEqual(steps.map(([name]) => `--text-${name}`)); for (let [name, step] of steps) { let value = declared(`--text-${name}`); let match = /^clamp\(([\d.]+)rem, ([\d.]+)rem \+ ([\d.]+)vw, ([\d.]+)rem\)$/.exec(value); expect(match).not.toBeNull(); let [, low, intercept, slope, high] = match!; let expectedMin = 15 * 1.12 ** step; let expectedMax = 16 * 1.14 ** step; expect(Number(low) * 16).toBeCloseTo(expectedMin, 3); expect(Number(intercept) * 16 + Number(slope) * 3.6).toBeCloseTo(expectedMin, 3); expect(Number(intercept) * 16 + Number(slope) * 14.4).toBeCloseTo(expectedMax, 3); expect(Number(high) * 16).toBeCloseTo(expectedMax, 3); } }); it("pairs every rung with its designed line height", () => { expect( ["2xs", "xs", "sm", "base", "lg", "xl", "2xl"].map(name => declared(`--text-${name}--line-height`) ), ) .toEqual(["1.35", "1.35", "1.5", "1.5", "1.4", "1.25", "1.15"]); }); it("restores fluid type inside MDXEditor's fixed variable scope", () => { for (let name of ["xs", "sm"] as const) { expect(EDITOR_STYLES).toContain(`--plan-text-${name}: var(--text-${name});`); expect(EDITOR_STYLES).toContain(`--text-${name}: var(--plan-text-${name});`); } expect(EDITOR_STYLES).toContain("--plan-body: var(--text-base);"); expect(EDITOR_STYLES).toContain("--text-base: var(--plan-body);"); }); }); describe("edges and depth", () => { it("keeps neutral edges and the approved danger edge distinct", () => { let edges = [...THEME.matchAll(/\n\s*(--color-[\w-]*edge):/g)].map(match => match[1]); expect(edges.sort()).toEqual([ "--color-button-outline-edge", "--color-control-edge", "--color-danger-edge", "--color-edge", ]); expect(declared("--color-edge")).toBe("oklch(0 0 0 / 7%)"); expect(declared("--color-control-edge")).toBe("oklch(0 0 0 / 15%)"); expect(declared("--color-button-outline-edge")).toBe("oklch(0 0 0 / 12%)"); }); it("keeps the control boundary visible on every surface", () => { for (let surface of ["page", "ground", "hover", "selected"]) { let ratio = contrast("--color-control-boundary", `--color-${surface}`); expect({ surface, passes: ratio >= 3 }).toEqual({ surface, passes: true }); } }); it("halves the hairline on retina displays", () => { expect(declared("--edge-width")).toBe("1px"); expect(THEME).toMatch(/@media \(min-resolution: 2dppx\)[\s\S]+--edge-width:\s*0\.5px/); }); it("provides exactly the four designed shadows", () => { let found = [...THEME.matchAll(/\n\s*(--shadow-(?!color)[\w-]+):/g)].map(match => match[1]); expect(found).toEqual([ "--shadow-resting", "--shadow-resting-strong", "--shadow-raised", "--shadow-overlay", ]); expect(declared("--shadow-color")).toBe("0.15908 0.00637 95"); expect(EDITOR_STYLES).toMatch( /\[data-research-ready\]\s*\{\s*box-shadow:\s*var\(--shadow-resting-strong\)/, ); }); }); describe("controls", () => { it("exposes the settled button sizes and destructive states", () => { expect(sizes("btn-md")).toEqual({ height: 32 }); expect(sizes("btn-sm")).toEqual({ height: 24 }); expect(sizes("btn-compact")).toEqual({ height: 24 }); expect(sizes("btn-icon")).toEqual({ height: 28, width: 28 }); expect(inlinePadding("btn-md")).toBe("calc(var(--spacing) * 3)"); expect(inlinePadding("btn-sm")).toBe("var(--control-padding-inline)"); expect(resolved("--control-padding-inline")).toBe("calc(var(--spacing) * 2)"); expect(utility("btn-icon")).toContain("padding: var(--icon-button-padding);"); expect(declared("--icon-button-padding")).toBe( "calc((var(--icon-button-size) - var(--icon-size-default)) / 2)", ); expect(hex("--color-destructive-hover")).toBe("#c44746"); expect(hex("--color-destructive-active")).toBe("#b34140"); }); it("gives every button tier guarded states without a rest border", () => { for (let tier of ["primary", "secondary", "ghost", "destructive"]) { let rule = utility(`btn-${tier}`); expect(rule).toMatch(/&:hover:not\(:disabled\)/); expect(rule).toMatch(/&:active:not\(:disabled\)/); expect(rule.slice(0, rule.indexOf("&:"))).not.toMatch(/\bborder(?:-\w+)?:/); } }); it("gives filled buttons their designed hairlines", () => { let primary = utility("btn-primary").split("&:")[0]!; let secondary = utility("btn-secondary").split("&:")[0]!; let destructive = utility("btn-destructive").split("&:")[0]!; expect(declared("--button-edge-width")).toBe("1px"); expect(primary).toContain("outline: var(--button-edge-width) solid var(--color-control-edge)"); expect(secondary).toContain( "outline: var(--button-edge-width) solid var(--color-edge)", ); expect(destructive).toContain( "outline: var(--button-edge-width) solid var(--color-control-edge)", ); for (let rest of [primary, secondary, destructive]) { expect(rest).toContain("outline-offset: calc(-1 * var(--button-edge-width))"); } }); it("spaces button icons and gives busy actions a readable disabled state", () => { let button = utility("btn"); expect(button).toContain("gap: calc(var(--spacing) * 2)"); expect(button).toMatch(/&\[aria-busy="true"\]:disabled\s*\{[\s\S]*opacity:\s*0\.65/); expect(button).toMatch( /&\[aria-busy="true"\] \[data-button-loader\]\s*\{[\s\S]*animation:\s*button-loader-spin/, ); expect(THEME).toMatch( new RegExp( String.raw`@media \(prefers-reduced-motion: reduce\)\s*\{` + String.raw`[\s\S]*\.btn\[aria-busy="true"\] \[data-button-loader\]\s*\{` + String.raw`[\s\S]*animation:\s*none`, ), ); }); it("raises primary buttons with the resting shadow", () => { let primary = utility("btn-primary").split("&:")[0]!; expect(primary).toContain("box-shadow: var(--shadow-resting)"); }); it("raises fields subtly and strengthens selected radio boundaries", () => { expect(utility("field")).toContain("box-shadow: var(--shadow-resting)"); expect(utility("choice-control")).toMatch( /&\[type="radio"\]:checked\s*\{[\s\S]*border-width:\s*calc\(var\(--edge-width\) \* 2\)/, ); }); it("dims each button's default style when disabled", () => { let rule = utility("btn"); let disabled = /&:disabled\s*\{([^}]*)\}/.exec(rule)?.[1] ?? ""; expect(disabled).toContain("opacity: 0.4"); expect(disabled).not.toMatch(/background-color|color:/); }); it("requires every shared button consumer to choose a size", () => { let offenders: string[] = []; for (let file of [...sources(join(ROOT, "apps")), ...sources(join(ROOT, "packages"))]) { if (!file.endsWith(".tsx")) continue; let lines = readFileSync(file, "utf8").split("\n"); for (let [index, line] of lines.entries()) { if (!line.includes("className=") || !/(? { expect(utility("btn")).toMatch(/white-space:\s*nowrap/); }); it("keeps consumer classes from resizing standard icon buttons", () => { expect(EDITOR_STYLES).not.toMatch(/\.plan-research-dismiss\s*\{[^}]*(?:width|height):/s); }); it("distinguishes the active compact workspace destination", () => { expect(THEME).toMatch( /\.workspace-navigation \[aria-current="page"\]\s*\{[\s\S]*background-color:\s*var\(--color-page\);[\s\S]*color:\s*var\(--color-text-primary\)/, ); }); it("keeps focus and invalid outlines visible above their surface", () => { expect(declared("--focus-ring-color")).toBe("var(--color-brand)"); expect(declared("--focus-ring-width")).toBe("2px"); expect(declared("--focus-ring-offset")).toBe("2px"); expect(THEME).toMatch( /\[data-focus-boundary\]\s*\{[\s\S]*--focus-ring-offset:\s*-2px/, ); expect(THEME).toMatch( /:focus-visible\s*\{[\s\S]*outline:\s*var\(--focus-ring-width\) solid var\(--focus-ring-color\);[\s\S]*outline-offset:\s*var\(--focus-ring-offset\)/, ); expect(utility("field")).toMatch( /&\[aria-invalid="true"\]\s*\{[\s\S]*outline:\s*2px solid var\(--color-destructive\);[\s\S]*outline-offset:\s*2px/, ); }); it("uses the two designed control edges across fields and choices", () => { for (let name of ["field", "choice-control"]) { let rule = utility(name); expect(rule).toMatch(/border:\s*var\(--edge-width\) solid var\(--color-control-edge\)/); expect(rule).toMatch(/&:disabled\s*\{[\s\S]*border-color:\s*var\(--color-edge\)/); } expect(utility("field")).toMatch( /&:disabled\s*\{[\s\S]*background-color:\s*var\(--color-gray-200\)/, ); expect(utility("choice-control")).toMatch( /&:disabled\s*\{[\s\S]*color:\s*var\(--color-gray-600\)/, ); }); it("keeps the checked checkbox glyph", () => { expect(utility("choice-control")).toContain("d='m3 7 2.5 2.5L11 4'"); }); it("keeps a disabled checked checkbox's fill distinct from its glyph", () => { let rule = utility("choice-control"); let checked = rule.indexOf("&:disabled:checked"); let checkbox = rule.indexOf('&[type="checkbox"]:disabled:checked'); expect(checkbox).toBeGreaterThan(checked); expect(rule.slice(checkbox)).toMatch( /background-color:\s*var\(--color-gray-200\);[\s\S]*stroke='%23605e56'/, ); }); it("keeps ghost fields transparent until hovered", () => { let rule = utility("field-ghost"); expect(rule).toMatch(/background-color:\s*transparent/); expect(rule).toMatch( /&:hover:not\(:disabled\)\s*\{[\s\S]*border-color:\s*var\(--color-control-edge\)/, ); }); }); describe("consumer roles", () => { it("uses the shared icon library instead of literal interface artwork", () => { let offenders: string[] = []; for (let file of [...sources(join(ROOT, "apps")), ...sources(join(ROOT, "packages"))]) { if (file.startsWith(join(ROOT, "packages/icons/"))) continue; // The diagram viewer owns an SVG canvas, not interface icon artwork. if (file === join(ROOT, "packages/diagrams/src/diagram.tsx")) continue; let content = withoutComments(readFileSync(file, "utf8")); if (/ { for (let file of [...sources(join(ROOT, "apps")), ...sources(join(ROOT, "packages"))]) { expect(withoutComments(readFileSync(file, "utf8"))).not.toMatch( /(?:^|\n)\s*color:\s*var\(--color-gray-400\)|text-gray-400/, ); } }); it("keeps placeholders on an opaque AA text role", () => { for (let file of [...sources(join(ROOT, "apps")), ...sources(join(ROOT, "packages"))]) { let content = withoutComments(readFileSync(file, "utf8")); expect(content).not.toMatch(/placeholder:text-text-quaternary(?:\/\d+)?/); } }); }); type StandardAction = { count?: number; action: string; marker: string; size: "btn-sm" | "btn-md" | "btn-icon"; tiers: readonly ("btn-primary" | "btn-secondary" | "btn-outline" | "btn-ghost" | "btn-destructive")[]; }; function classLists(button: string): string[][] { let attribute = button.slice(button.indexOf("className=")); let staticClass = /^className="([^"]+)"/.exec(attribute)?.[1]; if (staticClass) return [staticClass.split(/\s+/)]; let templateClass = /^className=\{`([^`]*)`\}/.exec(attribute)?.[1]; if (templateClass) return [templateClass.split(/\s+/)]; let expression = /^className=\{([^}]*)\}/.exec(attribute)?.[1]; if (!expression) return []; if (expression.startsWith("cn(")) { return [[...expression.matchAll(/"([^"]+)"/g)].flatMap(match => match[1]!.split(/\s+/))]; } let conditional = /\?\s*"([^"]+)"\s*:\s*"([^"]+)"/.exec(expression); return conditional ? [conditional[1]!.split(/\s+/), conditional[2]!.split(/\s+/)] : []; } function standardButtonOffenders(source: string, file: string, action: StandardAction): string[] { let buttons = [...source.matchAll(//g)] .filter(button => button[0].includes(action.marker)); let count = action.count ?? 1; if (buttons.length !== count) { let expected = count === 1 ? "one button" : `${count} buttons`; return [`${file}:1 ${action.action}: expected ${expected}, found ${buttons.length}`]; } let offenders: string[] = []; for (let button of buttons) { let line = source.slice(0, button.index).split("\n").length; let classes = classLists(button[0]); if (classes.length === 0) { offenders.push(`${file}:${line} ${action.action}: no static class list`); continue; } let tiers: string[] = []; for (let list of classes) { let sizes = list.filter(name => /^(btn-sm|btn-md|btn-icon)$/.test(name)); let currentTiers = list.filter(name => /^(btn-primary|btn-secondary|btn-outline|btn-ghost|btn-destructive)$/.test(name) ); let legacy = list.filter(name => /^(bg|px|py)-/.test(name)); tiers.push(...currentTiers); if (!list.includes("btn") || sizes.length !== 1 || sizes[0] !== action.size) { offenders.push( `${file}:${line} ${action.action}: ${list.join(" ") || "no static class list"}`, ); } if (currentTiers.length !== 1 || legacy.length > 0) { offenders.push( `${file}:${line} ${action.action}: ${ legacy.length > 0 ? `conflicting rest utilities ${legacy.join(" ")}` : list.join(" ") }`, ); } } if (tiers.sort().join(" ") !== [...action.tiers].sort().join(" ")) { offenders.push(`${file}:${line} ${action.action}: expected ${action.tiers.join(" or ")}`); } } return offenders; } type StandardControl = { file: string; marker: string; name: string; tag: "div" | "input" | "select" | "textarea"; utility: "choice-control" | "field" | "field-ghost"; }; function controlBlock(source: string, control: StandardControl): string { let first = source.indexOf(control.marker); let second = source.indexOf(control.marker, first + control.marker.length); if (first === -1 || second !== -1) { throw new Error(`${control.name}: expected one marker, found ${first === -1 ? 0 : 2}`); } let start = source.lastIndexOf(`<${control.tag}`, first); let closing = control.tag === "select" ? "" : "/>"; let end = source.indexOf(closing, first); if (start === -1 || end === -1) { throw new Error(`${control.name}: expected a complete ${control.tag}`); } return source.slice(start, end + closing.length); } function controlOffenders(source: string, control: StandardControl): string[] { let block = controlBlock(source, control); let classes = /className\s*=\s*"([^"]*)"/.exec(block)?.[1]?.split(/\s+/) ?? []; let offenders = classes.includes(control.utility) ? [] : [`${control.name}: missing ${control.utility} in ${block}`]; for (let name of classes) { if (/^(?:disabled|aria-disabled):opacity-.+$/.test(name)) { offenders.push(`${control.name}: ${name}`); } } return offenders; } describe("migration", () => { it("puts each listed standard control on its shared utility", () => { let controls: StandardControl[] = [ { file: "apps/web/src/chat/chat.tsx", marker: 'className="composer-surface field"', name: "chat composer", tag: "div", utility: "field", }, { file: "packages/editor/src/comments.tsx", marker: "maxLength={limits.MAX_NOTE}", name: "comment composer", tag: "textarea", utility: "field", }, ]; let offenders = controls.flatMap(control => controlOffenders( readFileSync(join(ROOT, control.file), "utf8"), control, ) ); expect(offenders).toEqual([]); }); it("rejects disabled opacity only inside a listed migrated control block", () => { let control: StandardControl = { file: "fixture.tsx", marker: "data-migrated", name: "fixture field", tag: "textarea", utility: "field", }; let source = `