Skip to content
87 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync } from "node:fs";
3import { test } from "node:test";
4
5// The shared tokens every site draws from (packages/theme/tokens.css).
6const css = readFileSync(new URL("../../../../packages/theme/tokens.css", import.meta.url), "utf8");
7
8function token(name: string): string {
9 const found = css.match(new RegExp(`--g1t-${name}:\\s*(#[0-9a-f]{6});`, "i"));
10 assert.ok(found, `--g1t-${name} is a six-digit hex colour`);
11 return found[1].toLowerCase();
12}
13
14function luminance(hex: string): number {
15 const [r, g, b] = [1, 3, 5]
16 .map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
17 .map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
18 return 0.2126 * r + 0.7152 * g + 0.0722 * b;
19}
20
21function contrast(a: string, b: string): number {
22 const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
23 return (hi + 0.05) / (lo + 0.05);
24}
25
26test("the accent is the logo's lavender, and mint means success", () => {
27 assert.equal(token("accent"), "#b6a8ff");
28 assert.equal(token("success"), "#86efc4");
29});
30
31test("merged reads apart from the accent", () => {
32 assert.notEqual(token("merged"), token("accent"));
33 assert.notEqual(token("merged"), token("success"));
34});
35
36test("text colours meet WCAG AA on every surface", () => {
37 for (const surface of ["bg", "surface", "raised"]) {
38 for (const ink of ["fg", "muted", "accent", "success", "merged", "info", "warn", "danger"]) {
39 const ratio = contrast(token(ink), token(surface));
40 assert.ok(ratio >= 4.5, `--g1t-${ink} on --g1t-${surface} is ${ratio.toFixed(2)}:1`);
41 }
42 }
43});
44
45test("the dark base reads on an accent or success button", () => {
46 for (const fill of ["accent", "accent-hover", "success"]) {
47 const ratio = contrast(token("bg"), token(fill));
48 assert.ok(ratio >= 4.5, `--g1t-bg on --g1t-${fill} is ${ratio.toFixed(2)}:1`);
49 }
50});
51
52test("focus borders in the dim accent stand out from the page", () => {
53 assert.ok(contrast(token("accent-dim"), token("bg")) >= 3);
54});
55
56// The light palette (Appearance, lib/theme.ts): the same names after `light-`.
57
58test("the light accent is a deeper lavender, and light success is green", () => {
59 assert.equal(token("light-accent"), "#6b52c8");
60 assert.equal(token("light-success"), "#0d7a52");
61});
62
63test("every dark token has a light one", () => {
64 const dark = [...css.matchAll(/--g1t-(?!light-|font-)([a-z-]+):/g)].map((match) => match[1]);
65 assert.ok(dark.length > 10);
66 for (const name of dark) token(`light-${name}`);
67});
68
69test("light text colours meet WCAG AA on every light surface", () => {
70 for (const surface of ["bg", "surface", "raised"]) {
71 for (const ink of ["fg", "fg-soft", "muted", "faint", "accent", "accent-hover", "success", "merged", "info", "warn", "danger"]) {
72 const ratio = contrast(token(`light-${ink}`), token(`light-${surface}`));
73 assert.ok(ratio >= 4.5, `--g1t-light-${ink} on --g1t-light-${surface} is ${ratio.toFixed(2)}:1`);
74 }
75 }
76});
77
78test("the light page reads on an accent or success button", () => {
79 for (const fill of ["accent", "accent-hover", "success"]) {
80 const ratio = contrast(token("light-bg"), token(`light-${fill}`));
81 assert.ok(ratio >= 4.5, `--g1t-light-bg on --g1t-light-${fill} is ${ratio.toFixed(2)}:1`);
82 }
83});
84
85test("light focus borders in the dim accent stand out from the page", () => {
86 assert.ok(contrast(token("light-accent-dim"), token("light-bg")) >= 3);
87});