/** * The touch and safe-area tokens, owned here rather than by whichever component uses them. * * WHY THIS EXISTS AS ITS OWN FILE, for the same reason `border-control.test.ts` does. A * per-component touch test asserts that component's sizing; nothing then asserts that 44 is * still 44. `--color-border-control` was 2.83:1 for seven components at once and every one of * their own tests passed, because none of them owned the token. These six tokens are shared by * every control the rollout touches, so the properties that make them correct are asserted * once, here, against the generated CSS rather than against a component. * * Read from the emitted CSS rather than imported, matching `tokens.test.ts`: the assertion is * about what ships, and the files are generated by `scripts/build-tokens.mjs`. */ import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; const dir = path.join(process.cwd(), 'src/tokens'); const read = (f: string) => fs.readFileSync(path.join(dir, f), 'utf8'); /** The declared value of a custom property, from the first place it is declared. */ const valueOf = (css: string, name: string) => { const match = css.match(new RegExp(`^\\s*${name}\\s*:\\s*([^;]+);`, 'm')); return match ? match[1].trim() : undefined; }; describe('touch target tokens', () => { it('declares a 44px touch target, the WCAG 2.5.5 minimum', () => { // 44, not 24. WCAG 2.5.8's floor is 24px and Strix's controls are already ~36px, so a // 24px token would be satisfied by construction and would assert nothing about whether // anything here is usable with a finger. expect(valueOf(read('theme.css'), '--spacing-touch-target')).toBe('44px'); }); it('declares a compact touch target that still clears the 24px floor', () => { const value = valueOf(read('theme.css'), '--spacing-touch-target-sm'); expect(value).toBe('40px'); expect(Number.parseFloat(value!)).toBeGreaterThanOrEqual(24); }); it('does not put the touch tokens behind a media query', () => { // The load-bearing property. The coarse-pointer application belongs to each component // via the `pointer-coarse:` variant, because the pointer is what matters and not the // viewport — a 1280px touch laptop needs the larger target, a narrow desktop window does // not. If the token itself were conditional, one component would resolve two different // values at two widths for no reason a reader could infer from the class name, and the // variant would be redundant. // // Asserted by checking the responsive layer, which is the only file that carries a media // query, does not declare them at all. const responsive = read('responsive.css'); expect(responsive).not.toContain('--spacing-touch-target'); expect(responsive).not.toContain('--spacing-safe-'); }); it('declares the touch tokens in @theme, so Tailwind generates utilities from them', () => { // A `--spacing-*` declaration outside `@theme` is a plain custom property: no // `min-h-touch-target` utility exists and the class silently generates nothing. That is // the same silent failure `no-shadcn-token` exists for, arriving from the other side. const theme = read('theme.css'); const themeBlock = theme.slice(theme.indexOf('@theme')); for (const name of ['--spacing-touch-target', '--spacing-touch-target-sm']) { expect(themeBlock, name).toContain(name); } }); }); describe('safe-area tokens', () => { const insets = ['top', 'right', 'bottom', 'left'] as const; it('declares all four insets', () => { const theme = read('theme.css'); for (const side of insets) { expect(valueOf(theme, `--spacing-safe-${side}`), side).toBe(`env(safe-area-inset-${side}, 0px)`); } }); it('gives every inset a 0px fallback', () => { // Without the fallback, `env()` with no matching inset makes the whole declaration // invalid at computed-value time, so `pb-safe-bottom` would resolve to nothing on every // browser without a notch — which is most of them. The fallback is what lets these be // unconditional and need no feature query. const theme = read('theme.css'); for (const side of insets) { expect(valueOf(theme, `--spacing-safe-${side}`), side).toContain(', 0px)'); } }); });