import { describe, expect, it } from "vitest"; import { BLOCK_COLORS, STATUS_COLORS } from "@factory/shared/constants"; import { BLOCK_COLOR_KEYS, DARK_TOKENS, LIGHT_TOKENS, STATUS_COLOR_KEYS, THEME_TOKENS, TOKEN_NAMES, cssTokensFile, cssVariables, } from "./tokens.js"; describe("tokens", () => { it("emits both themes with equal key sets", () => { expect(Object.keys(THEME_TOKENS.light).sort()).toEqual(Object.keys(THEME_TOKENS.dark).sort()); expect(THEME_TOKENS.light).not.toHaveProperty("undefined"); }); it("covers every semantic token in both themes", () => { for (const name of TOKEN_NAMES) { expect(THEME_TOKENS.light[name], `light missing ${name}`).toBeTruthy(); expect(THEME_TOKENS.dark[name], `dark missing ${name}`).toBeTruthy(); } }); it("uses Notion ink for light primary", () => { expect(THEME_TOKENS.light["primary"]).toBe("#37352F"); expect(THEME_TOKENS.light["foreground"]).toBe("#37352F"); }); it("maps all 18 shared block colors to CSS vars in both themes", () => { expect(BLOCK_COLOR_KEYS).toHaveLength(18); for (const c of BLOCK_COLORS) { const key = `block-color-${c.token}`; expect(BLOCK_COLOR_KEYS).toContain(key); expect(THEME_TOKENS.light[key]).toBe(c.hex); expect(THEME_TOKENS.dark[key]).toBe(c.hex); } }); it("maps all 9 shared status colors to CSS vars", () => { expect(STATUS_COLOR_KEYS).toHaveLength(9); for (const s of STATUS_COLORS) { expect(THEME_TOKENS.light[`status-${s}`]).toBeTruthy(); expect(THEME_TOKENS.dark[`status-${s}`]).toBeTruthy(); } }); it("cssVariables(light) renders :root with all keys, deterministic order", () => { const css = cssVariables("light"); expect(css.startsWith(":root {")).toBe(true); expect(css.endsWith("}")).toBe(true); for (const key of [...TOKEN_NAMES, ...BLOCK_COLOR_KEYS, ...STATUS_COLOR_KEYS]) { expect(css).toContain(`--${key}:`); } // deterministic: first declared key comes first expect(css.indexOf("--background:")).toBeGreaterThan(-1); expect(css.indexOf("--background:")).toBeLessThan(css.indexOf("--focus-ring:")); expect(css).toBe(cssVariables("light")); // stable across calls }); it('cssVariables(dark) renders [data-theme="dark"]', () => { const css = cssVariables("dark"); expect(css.startsWith('[data-theme="dark"] {')).toBe(true); for (const key of TOKEN_NAMES) { expect(css).toContain(`--${key}:`); } }); it("cssTokensFile contains both themes + block/status vars", () => { const file = cssTokensFile(); expect(file).toContain(":root {"); expect(file).toContain('[data-theme="dark"] {'); // var names derive 1:1 from the shared `token` field (underscore kept — // see README "Recorded decisions"; shared's own doc uses the same style) expect(file).toContain("--block-color-blue:"); expect(file).toContain("--block-color-blue_bg:"); expect(file).toContain("--status-red:"); expect(file.endsWith("\n")).toBe(true); // light and dark sections have the same count of declarations const light = file.slice(file.indexOf(":root {"), file.indexOf('[data-theme="dark"]')); const dark = file.slice(file.indexOf('[data-theme="dark"]')); expect((light.match(/--[\w-]+:/g) ?? []).length).toBe((dark.match(/--[\w-]+:/g) ?? []).length); }); it("light/dark token maps are plain string records", () => { expect(LIGHT_TOKENS.background).toBe("#FFFFFF"); expect(DARK_TOKENS.background).toBe("#191919"); }); });