check-tokens.test.mjs

Design token check tests

Tier 2engineering

Test that each of the five token rules convicts its target and leaves its near miss alone, so an edit to the checker cannot quietly move the line.

CHECK-TOKENS.TEST.MJS TOKEN CHECK NEAR MISSES TESTS PASS
Add this to your project
Add "Design token check tests" from ntent to this repo.

Fetch https://ntent.app/r/f/check-tokens-test as plain text. Write it verbatim to scripts/check-tokens.test.mjs.

It needs check-tokens.mjs. If this repo does not have it, stop and tell me.

Then check it: node --test passes. Stage an edit to check-tokens.mjs that stops z-[999] being caught and the commit is refused, naming this file.

Reads https://ntent.app/r/f/check-tokens-test

Read the code
check-tokens.test.mjsscripts/check-tokens.test.mjs
348 lines
/**
 * Tests for the token checker, run against a real stylesheet and real source
 * files on disk.
 *
 * Five rules, and each one is tested as a pair: the value it must convict and
 * the near miss it must leave alone. The near misses are the half that keeps
 * this check alive. shadcn ships every overlay on z-50, and a layer rule that
 * convicts z-50 gets waived in bulk on the first day; a length rule that
 * convicts 1px gets the same treatment. What this pins is where each rule draws
 * its line, so a change that moves the line has to move a test with it.
 *
 * It pins the SHIPPED defaults: the layer scale, the structural lengths, an
 * empty ownedFamilies. A repo that changes one of those on purpose will see a
 * test here go red, and the fix is to change the test to match, in the same
 * commit. That is the pre-commit self-test doing its job, not a false alarm.
 *
 * Every case writes a throwaway repo in a temp directory, with its own
 * stylesheet passed by --css, so whichever stylesheet your CONFIG points at is
 * never read. Nothing here touches the repository you are running from.
 *
 * Run:  node --test scripts/check-tokens.test.mjs
 */

import test, { after } from 'node:test'
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

// Every temp dir this file makes. They are removed when the file's tests end, pass or fail.
const made = []
const tmp = (prefix) => {
  const dir = mkdtempSync(join(tmpdir(), prefix))
  made.push(dir)
  return dir
}
after(() => {
  for (const dir of made) rmSync(dir, { recursive: true, force: true })
})

const CHECK = join(dirname(fileURLToPath(import.meta.url)), 'check-tokens.mjs')

// A Tailwind v4 token build, small enough to read: two colours with a root the
// unknown-class rule can own, a spacing step to name in a hint, one duration
// token and one layer token above the framework's scale.
const TOKENS = `@theme {
  --color-fg-muted: oklch(0.55 0 0);
  --color-fg-subtle: oklch(0.7 0 0);
  --spacing-gutter: 24px;
  --dur-state: 180ms;
  --z-modal: 60;
}
`

/** Write { 'src/a.tsx': source, ... } plus the stylesheet, and check src. */
function check(tree, { css = TOKENS, args = ['--check'] } = {}) {
  const dir = tmp('check-tokens-')
  if (css !== null) writeFileSync(join(dir, 'tokens.css'), css)
  for (const [path, source] of Object.entries(tree)) {
    mkdirSync(dirname(join(dir, path)), { recursive: true })
    writeFileSync(join(dir, path), source)
  }
  const cssArgs = css === null ? [] : ['--css', 'tokens.css']
  try {
    const out = execFileSync('node', [CHECK, ...cssArgs, ...args, 'src'], { cwd: dir, encoding: 'utf8' })
    return { code: 0, out, findings: findings(out) }
  } catch (e) {
    const out = (e.stdout ?? '') + (e.stderr ?? '')
    return { code: e.status, out, findings: findings(out) }
  }
}

/** Every reported finding as `<line> <rule> <text>`, and the hint under it. */
function findings(out) {
  const rows = []
  const lines = out.split('\n')
  lines.forEach((l, i) => {
    const m = /^\s+(\d+):\s{2}(raw-color|magic-length|unknown-class|raw-duration|raw-z-index)\s+(.+)$/.exec(l)
    if (m) rows.push({ at: `${m[1]} ${m[2]} ${m[3].trim()}`, hint: (lines[i + 1] ?? '').trim() })
  })
  return rows
}
const at = (run) => run.findings.map((f) => f.at)

// ------------------------------------------------------------ raw-color ---

test('raw-color: a hex or a colour function, in CSS and in a component', () => {
  const run = check({
    'src/card.css': '.card { color: #ff0055; background: rgb(0 0 0 / 50%); }\n',
    'src/Card.tsx': `export const Card = () => <div style={{ color: '#334155' }} className="text-fg-muted" />\n`,
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 raw-color #ff0055', '1 raw-color rgb(...)', '1 raw-color #334155'])
})

test('raw-color: a token, color-mix of tokens, an id selector and a comment all pass', () => {
  const run = check({
    'src/card.css': `/* was #ff0055 before the brand refresh */
#main { color: var(--color-fg-muted); }
.b { border-color: color-mix(in oklab, var(--color-fg-muted), transparent); }
`,
  })
  assert.equal(run.code, 0, run.out)
})

// --------------------------------------------------------- magic-length ---

test('magic-length: every unit, and every value hidden behind an underscore', () => {
  // Tailwind spells a space inside an arbitrary value as _, so the second and
  // third lengths in a shadow sit behind one. A rule that only saw the first
  // would pass a hand-rolled shadow on its blur.
  const run = check({
    'src/Panel.tsx': `export const Panel = () => <div className="max-w-[87rem] shadow-[0_4px_12px]" />\n`,
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 magic-length 87rem', '1 magic-length 4px', '1 magic-length 12px'])
  assert.match(run.findings[0].hint, /\(1392px\)/, 'a rem length says what it is in px')
})

test('magic-length: names the token that carries the value, and leaves structure alone', () => {
  const run = check({
    'src/layout.css': `.page { padding-inline: 24px; border-width: 1px; height: 100dvh; width: 50%; }
@media (min-width: 768px) { .page { display: grid; } }
`,
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 magic-length 24px'], '1px, 100dvh, 50% and the breakpoint are not design decisions')
  assert.match(run.findings[0].hint, /use var\(--spacing-gutter\)/)
})

test('magic-length: the 3px focus ring that shadcn writes into every Button is not a finding', () => {
  const run = check({
    'src/Button.tsx': `export const Button = () => <button className="outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" />\n`,
  })
  assert.equal(run.code, 0, run.out)
})

// -------------------------------------------------------- unknown-class ---

test('unknown-class: a member of a family you define that does not exist', () => {
  const run = check({
    'src/Label.tsx': `export const Label = () => <span className="text-fg-nope" />\n`,
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 unknown-class text-fg-nope'])
  assert.match(run.findings[0].hint, /resolves to nothing/)
  assert.match(run.findings[0].hint, /Nearest: fg-muted, fg-subtle/)
})

test('unknown-class: real tokens and the framework palette pass', () => {
  // bg-red-500 is the framework's business: no token here has a red root.
  const run = check({
    'src/Label.tsx': `export const Label = () => <span className="text-fg-muted hover:text-fg-subtle bg-red-500 text-sm" />\n`,
  })
  assert.equal(run.code, 0, run.out)
})

// --------------------------------------------------------- raw-duration ---

test('raw-duration: both spellings, and the token that carries the value', () => {
  const run = check({
    'src/motion.css': `.a { transition: color 0.18s ease; }
.b { transition: opacity 420ms; }
.c { transition-duration: 0s; }
`,
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 raw-duration 0.18s', '2 raw-duration 420ms'], '0s is structural')
  assert.match(run.findings[0].hint, /use var\(--dur-state\) \(180ms\)/)
  assert.match(run.findings[1].hint, /no token carries this duration/)
})

// ---------------------------------------------------------- raw-z-index ---

test('raw-z-index: the framework steps, the structural nudges and a --z token pass', () => {
  const run = check({
    'src/Layers.tsx': `export const A = () => <div className="z-50" />
export const B = () => <div className="-z-1" />
export const C = () => <div className="z-60" />
export const D = () => <div style={{ zIndex: 10 }} />
`,
    'src/layers.css': '.modal { z-index: 60; }\n',
  })
  assert.equal(run.code, 0, run.out)
})

test('raw-z-index: a layer off the scale, however it is spelled', () => {
  const run = check({
    'src/Layers.tsx': `export const A = () => <div className="z-[999]" />
export const B = () => <div style={{ zIndex: 100 }} />
export const C = () => <div className="-z-10" />
`,
    'src/layers.css': '.toast { z-index: 9999; }\n',
  })
  assert.equal(run.code, 1, run.out)
  // Reported by file, and the stylesheet sorts first.
  assert.deepEqual(at(run), ['1 raw-z-index z-index: 9999', '1 raw-z-index z-[999]', '2 raw-z-index zIndex: 100', '3 raw-z-index -z-10'])
  assert.match(run.findings[0].hint, /not on the layer scale \(-?\d.*60\)/, 'the hint lists the scale, token included')
})

// -------------------------------------------------------------- waivers ---

test('a waiver with a reason clears the finding and the run prints the reason', () => {
  const run = check({
    'src/Embed.tsx': `/* project-check-ignore -- the vendor widget only takes a hex */
export const Embed = () => <Widget brandColor="#0000ff" />
`,
  })
  assert.equal(run.code, 0, run.out)
  assert.match(run.out, /1 finding waived by comment/)
  assert.match(run.out, /reason: the vendor widget only takes a hex/)
})

// ---------------------------------------------------------- cannot check ---

test('no stylesheet is exit 2, never a pass', () => {
  const dir = tmp('check-tokens-')
  mkdirSync(join(dir, 'src'))
  writeFileSync(join(dir, 'src', 'Card.tsx'), 'export const Card = () => null\n')
  let code = 0
  let out = ''
  try {
    out = execFileSync('node', [CHECK, '--css', 'missing.css', '--check', 'src'], { cwd: dir, encoding: 'utf8' })
  } catch (e) {
    code = e.status
    out = (e.stdout ?? '') + (e.stderr ?? '')
  }
  assert.equal(code, 2, out)
  assert.match(out, /no generated stylesheet found/)
})

test('an empty scan under --check is exit 2, and says it checked nothing', () => {
  const run = check({ 'src/notes.md': '# nothing scannable\n' })
  assert.equal(run.code, 2, run.out)
  assert.match(run.out, /scanned 0 files/)
  assert.match(run.out, /This is not a pass/)
})

// ------------------------------------------------ stock Tailwind v3 ---

// What the shadcn CLI writes for Tailwind v3: HSL triplets under bare names,
// no @theme, and a --radius that is a length. 14 repos in the field study had
// this file and the checker exited 2 on every one of them.
const SHADCN_V3 = `@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    --chart-1: 12 76% 61%;
    --radius: 0.5rem;
  }
  .dark {
    --background: 222.2 84% 4.9%;
  }
}
`

test('a stock shadcn v3 stylesheet is readable: its names are the colour tokens', () => {
  const ok = check(
    { 'src/Card.tsx': 'export const A = () => <div className="bg-background text-foreground border-primary text-card-foreground" />\n' },
    { css: SHADCN_V3 },
  )
  assert.equal(ok.code, 0, ok.out)
  const typo = check(
    { 'src/Card.tsx': 'export const A = () => <div className="text-primary-foregroud" />\n' },
    { css: SHADCN_V3 },
  )
  assert.equal(typo.code, 1, typo.out)
  assert.deepEqual(at(typo), ['1 unknown-class text-primary-foregroud'])
  const raw = check({ 'src/Card.tsx': "export const A = () => <div style={{ color: '#ff0044' }} />\n" }, { css: SHADCN_V3 })
  assert.equal(raw.code, 1, raw.out)
})

test('an unknown namespace is a note about that namespace, not a refusal to run', () => {
  const css = `@theme {\n  --color-fg: oklch(0.5 0 0);\n  --widget-gap: 4px;\n  --lanes-count: 3;\n}\n`
  const run = check({ 'src/a.tsx': "export const A = () => <div style={{ color: '#ff0044' }} />\n" }, { css })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(at(run), ['1 raw-color #ff0044'])
  assert.match(run.out, /not checked, no rule for these namespaces: --widget-\*, --lanes-\*/)
  const clean = check({ 'src/a.tsx': 'export const A = () => null\n' }, { css })
  assert.equal(clean.code, 0, clean.out)
  assert.match(clean.out, /--widget-\*/)
})

test('a minified stylesheet is read', () => {
  const css = '@theme{--color-fg-muted:oklch(.55 0 0);--spacing-gutter:24px;--dur-state:180ms;--z-modal:60}'
  const run = check(
    { 'src/a.tsx': 'export const A = () => <div className="text-fg-nope z-[70]" style={{ transition: "all 180ms" }} />\n' },
    { css },
  )
  assert.equal(run.code, 1, run.out)
  const rows = at(run)
  assert.ok(rows.includes('1 unknown-class text-fg-nope'), run.out)
  assert.ok(rows.includes('1 raw-z-index z-[70]'), run.out)
  const duration = run.findings.find((f) => /raw-duration 180ms/.test(f.at))
  assert.ok(duration, run.out)
  assert.match(duration.hint, /--dur-state/)
})

test('.jsx screens are read', () => {
  const run = check({ 'src/Screen.jsx': "export const A = () => <div style={{ color: '#ff0044' }} />\n" })
  assert.equal(run.code, 1, run.out)
})

// ---------------------------------------------- why it read nothing, for the hook ---

test('every exit 2 writes its own reason to the file the hook names, and nothing else does', () => {
  // The hook repeats the reason in its note. It used to say "nothing in this
  // commit" for all of them, including a stylesheet it could not read.
  const run = (css, files, args = []) => {
    const dir = tmp('check-tokens-note-')
    const note = join(dir, 'unchecked')
    writeFileSync(note, '')
    if (css !== null) writeFileSync(join(dir, 'tokens.css'), css)
    for (const [path, source] of Object.entries(files)) {
      mkdirSync(dirname(join(dir, path)), { recursive: true })
      writeFileSync(join(dir, path), source)
    }
    let code = 0
    try {
      execFileSync('node', [CHECK, ...(css === null ? [] : ['--css', 'tokens.css']), '--check', ...args, 'src'], {
        cwd: dir,
        encoding: 'utf8',
        stdio: 'pipe',
        env: { ...process.env, NTENT_UNCHECKED_FILE: note },
      })
    } catch (e) {
      code = e.status
    }
    return { code, note: readFileSync(note, 'utf8').trim() }
  }
  const tsx = { 'src/a.tsx': 'export const a = 1\n' }
  assert.deepEqual(run(null, tsx), { code: 2, note: 'no stylesheet found' })
  assert.deepEqual(run('a { color: red }\n', tsx), { code: 2, note: 'the stylesheet has no token block' })
  assert.deepEqual(run('@theme { --spacing-x: 1px; }\n', tsx), { code: 2, note: 'the stylesheet defines no color tokens it can read' })
  assert.deepEqual(run(TOKENS, { 'src/notes.md': '# x\n' }), { code: 2, note: 'nothing staged is a file it scans' })
  assert.deepEqual(run(TOKENS, tsx), { code: 0, note: '' }, 'a run that read something writes nothing')
})

How to tell it works: node --test passes. Stage an edit to check-tokens.mjs that stops z-[999] being caught and the commit is refused, naming this file.

Included in