import { describe, expect, it } from "vitest"; import { eslint } from "./lintProbe.testkit"; // Three audit rules in eslint.config.js, pinned so a selector that stops matching cannot pass for // a clean codebase: a hover that swaps a colour instead of the B3 state layer, an that holds only a // glyph (DESIGN.md "A link that looks like a button" is IconLink), and a palette colour (F16). async function messages(source: string, starts: string): Promise { const [result] = await eslint.lintText(source, { filePath: "src/components/__probe.tsx" }); return (result?.messages ?? []).filter((m) => m.message.startsWith(starts)).length; } const swaps = (src: string) => messages(src, "hover by a colour swap"); const glyphs = (src: string) => messages(src, "a glyph-only "); const palette = (src: string) => messages(src, "a palette colour"); describe("a hover by a colour swap", () => { // Assembled, so this file does not trip the very rule it probes. const h = (v: string) => `hover:${v}`; it("is refused for bg-card, bg-border, a hand-written layer and an opacity dim", async () => { expect(await swaps(`export const a = "px-2 ${h("bg-card")}";\n`)).toBe(1); expect(await swaps(`export const b = "${h("bg-border/40")}";\n`)).toBe(1); expect(await swaps("export const c = `x ${1} " + h("opacity-80") + "`;\n")).toBe(1); expect(await swaps(`export const d = "enabled:${h("bg-(--hover)")}";\n`)).toBe(1); }); it("leaves the state layer, a parent's reveal and an edged pill's accent hover alone", async () => { expect(await swaps(`export const e = "state-layer group-${h("opacity-100")}";\n`)).toBe(0); expect(await swaps(`export const f = "${h("border-accent")} ${h("bg-accent/20")}";\n`)).toBe(0); expect(await swaps(`export const g = "${h("bg-cardinal")}";\n`)).toBe(0); }); }); describe("a glyph-only link", () => { it("is refused, whatever its attributes hold", async () => { const src = 'import { X } from "lucide-react";\n' + "export const A = ({ u }: { u: string }) => (\n" + ' \n \n \n);\n'; expect(await glyphs(src)).toBe(1); }); it("passes a text link, a card with content, and an i18n Trans slot", async () => { const src = 'import { X } from "lucide-react";\n' + "export const B = ({ u, n }: { u: string; n: string }) => (\n" + " <>\n" + " \n Open\n \n" + " \n {n}\n \n" + ' \n' + " \n);\n"; expect(await glyphs(src)).toBe(0); }); }); // F16: DESIGN.md Tokens "only semantic tokens". A palette class is measured by nothing, so a gold that // passes on dark ships at 2:1 on light. Black and white stay: they are the media scrims and their text. describe("a palette colour", () => { const c = (v: string) => `${v}-500`; // assembled, as above it("is refused as a text, fill, edge, gradient stop or ring", async () => { expect(await palette(`export const a = "${c("text-amber")}";\n`)).toBe(1); expect(await palette(`export const b = "x ${c("bg-red")}/90";\n`)).toBe(1); expect(await palette("export const d = `${1} " + c("from-slate") + "`;\n")).toBe(1); expect(await palette(`export const e = "hover:${c("ring-neutral")}";\n`)).toBe(1); }); it("leaves the tokens, black and white alone", async () => { expect( await palette( `export const f = "text-rating bg-live text-live-fg bg-black/80 text-white/75";\n`, ), ).toBe(0); expect(await palette(`export const g = "bg-accent text-danger border-border";\n`)).toBe(0); }); });