1
19
20import { readFileSync } from "node:fs";
21import { resolve } from "node:path";
22import { describe, expect, it } from "vitest";
23
24const CORE_TOKENS = [
25 "background",
26 "foreground",
27 "card",
28 "card-foreground",
29 "popover",
30 "popover-foreground",
31 "primary",
32 "primary-foreground",
33 "secondary",
34 "secondary-foreground",
35 "muted",
36 "muted-foreground",
37 "accent",
38 "accent-foreground",
39 "destructive",
40 "destructive-foreground",
41 "border",
42 "input",
43 "ring",
44] as const;
45
46
47const RAW_TRIPLE = /^(\d+(?:\.\d+)?\s+){2}\d+(?:\.\d+)?$/;
48
49function blockBody(css: string, selector: string): string {
50 const i = css.indexOf(selector);
51 if (i === -1) throw new Error(`${selector} not found in globals.css`);
52 const open = css.indexOf("{", i);
53 const close = css.indexOf("}", open);
54 return css.slice(open + 1, close);
55}
56
57function tokenValues(body: string): Record<string, string> {
58 const out: Record<string, string> = {};
59 for (const name of CORE_TOKENS) {
60 const m = body.match(new RegExp(`--${name}:\\s*([^;]+);`));
61 const value = m?.[1];
62 if (!value) throw new Error(`--${name} missing in block`);
63 out[name] = value.trim();
64 }
65 return out;
66}
67
68describe("theme tokens (globals.css + tailwind.config.ts)", () => {
69 const css = readFileSync(resolve(__dirname, "../globals.css"), "utf8");
70 const config = readFileSync(
71 resolve(__dirname, "../../../tailwind.config.ts"),
72 "utf8",
73 );
74
75 const light = tokenValues(blockBody(css, ":root"));
76 const dark = tokenValues(blockBody(css, ".dark"));
77
78 it("stores every core token as a raw oklch channel triple in both themes", () => {
79 for (const [name, value] of Object.entries({ ...light, ...dark })) {
80 expect(value, `--${name} must be raw channels, got: ${value}`).toMatch(RAW_TRIPLE);
81 expect(
82 value.startsWith("oklch("),
83 `--${name} must NOT be a full oklch() color`,
84 ).toBe(false);
85 }
86 });
87
88 it("defines the same 19 core tokens in light and dark", () => {
89 expect(Object.keys(light).sort()).toEqual(Object.keys(dark).sort());
90 expect(CORE_TOKENS).toHaveLength(19);
91 });
92
93 it("wraps the tokens in the tailwind config as oklch(var(--x) / <alpha-value>)", () => {
94
95 for (const name of ["background", "foreground", "border", "ring", "primary", "muted", "card"]) {
96 expect(
97 config,
98 `tailwind.config must wrap --${name}`,
99 ).toContain(`oklch(var(--${name}) / <alpha-value>)`);
100 }
101
102 expect(config).not.toMatch(/"(?:var\(--(background|foreground|border|ring|primary)\))"/);
103 });
104});
105