sdlc-hub/web/src/styles/tokens.test.ts
2026-10-24 20:02:12 +04:00

92 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/);
});
});