wren/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files wren/5-detail
111 lines · 4.3 KB · ~1.3k tok Raw History
1
/**
2
 * The look holds: the tokens compile to both themes, every pair of colours
3
 * a person must read meets WCAG AA in both, and no colour is spelled out
4
 * anywhere but design/tokens.json.
5
 */
6
import { expect, test } from "bun:test";
7
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
8
import { tmpdir } from "node:os";
9
import { join } from "node:path";
10
import { lint, rawColours, unknownTokens } from "../scripts/lint-design.ts";
11
import { compile, flatten, token, tokens, tokensCss } from "./tokens.ts";
12
13
test("the tokens compile: light on :root, dark for the system and for data-theme", () => {
14
  expect(tokensCss).toContain(`--color-bg: ${token("color-bg")};`);
15
  expect(tokensCss).toContain("--space-4: 1rem;");
16
  expect(tokensCss).toContain(
17
    '--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;',
18
  );
19
  expect(tokensCss).toContain("@media (prefers-color-scheme: dark)");
20
  expect(tokensCss).toContain(':root:not([data-theme="light"])');
21
  expect(tokensCss).toContain(':root[data-theme="dark"]');
22
  // Every colour has a dark value, or the dark theme would inherit a light one.
23
  for (const t of tokens.filter((t) => t.type === "color"))
24
    expect([t.name, t.dark !== undefined]).toEqual([t.name, true]);
25
  expect(
26
    tokensCss.split(`--color-bg: ${token("color-bg", "dark")};`).length,
27
  ).toBe(3);
28
});
29
30
test("tokens that do not read stop the app, with the path named", () => {
31
  expect(() => flatten({ color: { bg: { $value: "#fff" } } })).toThrow(
32
    "color.bg: no $type",
33
  );
34
  expect(() =>
35
    flatten({
36
      color: { $type: "color", bg: { $value: "red; } body { display: none" } },
37
    }),
38
  ).toThrow("color.bg: $value is not a CSS value");
39
  expect(() =>
40
    flatten({ color: { $type: "color", "b g": { $value: "#fff" } } }),
41
  ).toThrow("not a name CSS can carry");
42
  expect(
43
    compile(flatten({ size: { $type: "dimension", tap: { $value: "44px" } } })),
44
  ).toContain("--size-tap: 44px;");
45
});
46
47
// WCAG 2 relative luminance and contrast, from a #rrggbb.
48
const luminance = (hex: string): number => {
49
  const [r, g, b] = [1, 3, 5].map((i) => {
50
    const c = parseInt(hex.slice(i, i + 2), 16) / 255;
51
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
52
  }) as [number, number, number];
53
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
54
};
55
const contrast = (a: string, b: string): number => {
56
  const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p) as [
57
    number,
58
    number,
59
  ];
60
  return (x + 0.05) / (y + 0.05);
61
};
62
63
test("every pair a person reads is at least 4.5:1, in both themes", () => {
64
  const pairs = [
65
    ["color-fg", "color-bg"],
66
    ["color-fg", "color-surface"],
67
    ["color-muted", "color-bg"],
68
    ["color-muted", "color-surface"],
69
    ["color-accent", "color-bg"],
70
    ["color-accent", "color-surface"],
71
    ["color-accent-fg", "color-accent"],
72
    ["color-toast-fg", "color-toast-bg"],
73
  ] as const;
74
  for (const mode of ["light", "dark"] as const)
75
    for (const [fg, bg] of pairs) {
76
      const ratio = contrast(token(fg, mode), token(bg, mode));
77
      expect([mode, fg, bg, ratio >= 4.5]).toEqual([mode, fg, bg, true]);
78
    }
79
});
80
81
test("the app spells out no colour but in its tokens", () => {
82
  expect(lint(join(import.meta.dir, ".."))).toEqual([]);
83
});
84
85
test("the lint refuses a raw colour and a token that is not there", () => {
86
  expect(
87
    rawColours(
88
      "a.css",
89
      ".x { color: #ff0000; }\n.y { background: rgb(0 0 0); }",
90
    ),
91
  ).toEqual([
92
    "a.css:1: raw colour #ff0000 — name it in design/tokens.json and use var(--…)",
93
    "a.css:2: raw colour rgb( — name it in design/tokens.json and use var(--…)",
94
  ]);
95
  // Not colours: an entity, an id selector's word, a pull number.
96
  expect(rawColours("a.ts", "`&#39;` #notes pull #12 #abcdefg")).toEqual([]);
97
  expect(
98
    unknownTokens(
99
      "a.css",
100
      ".x { color: var(--color-fg); gap: var(--spcae-2); }",
101
    ),
102
  ).toEqual(["a.css:1: --spcae-2 is not a token in design/tokens.json"]);
103
  const root = mkdtempSync(join(tmpdir(), "app-lint-"));
104
  mkdirSync(join(root, "static"));
105
  mkdirSync(join(root, "src"));
106
  writeFileSync(join(root, "static", "extra.css"), "a {\n  color: #fff;\n}\n");
107
  writeFileSync(join(root, "src", "ok.test.ts"), "const red = '#f00';\n");
108
  expect(lint(root)).toEqual([
109
    "static/extra.css:2: raw colour #fff — name it in design/tokens.json and use var(--…)",
110
  ]);
111
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts