import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { describe, expect, it } from "vitest"; const SRC = resolve(__dirname, ".."); const TOKENS = resolve(__dirname, "tokens.css"); function cssFiles(dir: string): string[] { return readdirSync(dir).flatMap((name) => { const p = join(dir, name); if (statSync(p).isDirectory()) return cssFiles(p); return p.endsWith(".css") ? [p] : []; }); } const stripComments = (s: string) => s.replace(/\/\*[\s\S]*?\*\//g, ""); const LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\(|:\s*(?:white|black|red|green|blue|gray|grey|orange|yellow|purple)\b/g; /** Тело блока, начинающегося с селектора `head` (по балансу скобок). */ function block(css: string, head: string): string { const at = css.indexOf(head); if (at < 0) throw new Error(`нет блока ${head}`); let i = css.indexOf("{", at); const start = i + 1; let depth = 0; for (; i < css.length; i++) { if (css[i] === "{") depth++; if (css[i] === "}" && --depth === 0) return css.slice(start, i); } throw new Error(`блок ${head} не закрыт`); } const names = (body: string) => [...body.matchAll(/(--color-[\w-]+)\s*:/g)].map((m) => m[1]).sort(); describe("токены AURA", () => { it("вне tokens.css нет литералов цвета", () => { const found: string[] = []; for (const f of cssFiles(SRC)) { if (f === TOKENS) continue; for (const m of stripComments(readFileSync(f, "utf8")).matchAll(LITERAL)) { found.push(`${relative(SRC, f)}: ${m[0]}`); } } expect(found).toEqual([]); }); const css = stripComments(readFileSync(TOKENS, "utf8")); it("обе темы задают одни имена", () => { const l = names(block(css, ":root {")); const media = names(block(css, ':root:not([data-theme="light"])')); const dark = names(block(css, ':root[data-theme="dark"]')); expect(l.length).toBeGreaterThan(20); expect(media).toEqual(l); expect(dark).toEqual(l); }); it("тёмные блоки и светлый :root задают color-scheme", () => { expect(block(css, ":root {")).toContain("color-scheme: light"); expect(block(css, ':root:not([data-theme="light"])')).toContain("color-scheme: dark"); expect(block(css, ':root[data-theme="dark"]')).toContain("color-scheme: dark"); }); }); describe("использованные токены определены", () => { const defs = (css: string) => new Set([...stripComments(css).matchAll(/(--[\w-]+)\s*:/g)].map((m) => m[1])); // переменные, которые выставляет код страницы (setProperty), а не css const SET_BY_JS = new Set(["--head-h"]); const tokenDefs = defs(readFileSync(TOKENS, "utf8")); it("каждый var(--…) в css определён в tokens.css или в том же файле", () => { const missing: string[] = []; for (const f of cssFiles(SRC)) { if (f === TOKENS) continue; const css = readFileSync(f, "utf8"); const local = defs(css); for (const m of stripComments(css).matchAll(/var\(\s*(--[\w-]+)/g)) { if (!tokenDefs.has(m[1]) && !local.has(m[1]) && !SET_BY_JS.has(m[1])) missing.push(`${relative(SRC, f)}: ${m[1]}`); } } expect([...new Set(missing)]).toEqual([]); }); }); describe("доска без чередования дорожек (#69, design 9)", () => { it("токена --lane-alt нет, правила чётной дорожки нет", () => { expect(readFileSync(TOKENS, "utf8")).not.toContain("--lane-alt"); expect(readFileSync(resolve(SRC, "modules/jira/board/board.css"), "utf8")).not.toMatch(/\.lane:nth-child/); }); });