Files
vyndr/tests/unit/vyndrDesignSystem.test.js
T
builtbykev a74b5dd1ed Session 33: Design system Phase A+B — tokens, fonts, glitch CSS, shared components (1792 tests)
VYNDR 2.0 design-system conversion, foundation only (pages/mobile/systems are
Sessions 34+). Frontend-only; zero backend changes.

Phase A: §2 token set + token-wired typography + Inter/JetBrains Mono fonts +
§4 glitch keyframes (ported verbatim from the prototype's vyndr.css) + scanline
texture + §10 a11y data-* layer in globals.css. Legacy alias block kept.

Phase B: web/src/lib/vyndrTokens.js (CommonJS helpers) + 9 shared components
under web/src/components/vyndr/ (Wordmark, GradeBadge, SportBadge, TerminalInput,
SectionHead, VBtn, Card, Sparkline, Ticker).

74 new design-system tests. Backend 1718 -> 1792, 141 suites, zero regressions.
Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:58:57 -04:00

186 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VYNDR 2.0 design system — Phase A (tokens/CSS/fonts/glitch) + Phase B
// (shared components). The .tsx components can't run under the plain-JS Jest
// config (no Babel/TS transform), so component contracts are asserted against
// their source text; the helper LOGIC is exercised directly via the CommonJS
// vyndrTokens module that the components actually import.
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
const css = read('app/globals.css');
const layout = read('app/layout.tsx');
const tokens = require('../../web/src/lib/vyndrTokens');
describe('Phase A — design tokens (§2)', () => {
const REQUIRED_TOKENS = [
'--bg-0', '--bg-1', '--bg-2', '--bg-3', '--border', '--border-hi',
'--text-0', '--text-1', '--text-2',
'--g-ap', '--g-a', '--g-b', '--g-c', '--g-d',
'--s-nba', '--s-mlb', '--s-wnba', '--s-soccer',
'--acc-0', '--acc-1', '--amber', '--amber-glow',
'--live', '--hit', '--miss',
'--glitch', '--scan-op', '--grade-hero', '--sans', '--mono',
];
it.each(REQUIRED_TOKENS)('defines %s in :root', (token) => {
expect(css).toMatch(new RegExp(`${token.replace(/[-]/g, '\\-')}\\s*:`));
});
it('uses the exact §2 grade hex values', () => {
expect(css).toContain('--g-ap: #00ffb8');
expect(css).toContain('--g-a: #00d4a0');
expect(css).toContain('--g-b: #4a9eff');
expect(css).toContain('--g-c: #ffb347');
expect(css).toContain('--g-d: #ff5252');
});
it('sets --sans to Inter and --mono to JetBrains Mono', () => {
expect(css).toMatch(/--sans:\s*'Inter'/);
expect(css).toMatch(/--mono:\s*'JetBrains Mono'/);
});
it('sets glitch intensity to the §2 baseline of 1 and scan-op to 0.04', () => {
expect(css).toMatch(/--glitch:\s*1;/);
expect(css).toMatch(/--scan-op:\s*0\.04;/);
});
});
describe('Phase A — fonts (§2)', () => {
it('loads Inter (400900) and JetBrains Mono (400800) from Google Fonts', () => {
expect(layout).toContain('Inter:wght@400;500;600;700;800;900');
expect(layout).toContain('JetBrains+Mono:wght@400;500;600;700;800');
});
});
describe('Phase A — glitch + animation keyframes (§4, ported verbatim)', () => {
const KEYFRAMES = [
'wm-tear', 'glitch-shift-r', 'glitch-shift-b', 'wm-caret', 'head-tear',
'crt-sweep', 'crt-sweep-local', 'phosphor-pulse', 'grade-reveal',
'ticker-scroll', 'live-pulse', 'flash-up', 'flash-down', 'ekg-scroll',
'node-pulse', 'synapse-travel', 'count-tick', 'factor-ignite',
'toast-in', 'sheet-up', 'cmd-in', 'fade-in', 'scan-drift',
];
it.each(KEYFRAMES)('defines @keyframes %s', (name) => {
expect(css).toContain(`@keyframes ${name}`);
});
it('floors fade-in at the visible state (opacity .6, never 0)', () => {
expect(css).toMatch(/@keyframes fade-in\s*\{\s*from\s*\{\s*opacity:\s*0\.6;/);
});
});
describe('Phase A — scanline texture + reduced motion (§4, §10)', () => {
it('has the .scanlines::after CRT texture class', () => {
expect(css).toContain('.scanlines::after');
expect(css).toMatch(/repeating-linear-gradient\(0deg, transparent 50%, rgba\(0,0,0,1\) 50%\)/);
expect(css).toMatch(/opacity: var\(--scan-op\)/);
});
it('respects prefers-reduced-motion and the in-app data-motion toggle', () => {
expect(css).toContain('@media (prefers-reduced-motion: reduce)');
expect(css).toContain('html[data-motion="reduced"]');
});
it('ships the a11y preference layer (contrast / text / colorblind / readable font)', () => {
expect(css).toContain('html[data-contrast="high"]');
expect(css).toContain('html[data-text="xl"]');
expect(css).toContain('html[data-cb="1"]');
expect(css).toContain('html[data-font="readable"]');
});
});
describe('Phase B — token helpers (vyndrTokens)', () => {
it('gradeColor returns the correct CSS variable for each grade tier', () => {
expect(tokens.gradeColor('A+')).toBe('var(--g-ap)');
expect(tokens.gradeColor('A')).toBe('var(--g-a)');
expect(tokens.gradeColor('A-')).toBe('var(--g-a)');
expect(tokens.gradeColor('B+')).toBe('var(--g-b)');
expect(tokens.gradeColor('B')).toBe('var(--g-b)');
expect(tokens.gradeColor('C')).toBe('var(--g-c)');
expect(tokens.gradeColor('D')).toBe('var(--g-d)');
});
it('gradeColor falls back to primary text for unknown grades', () => {
expect(tokens.gradeColor('Z')).toBe('var(--text-0)');
});
it('gradeHex returns the exact §2 hex for each grade', () => {
expect(tokens.gradeHex('A+')).toBe('#00ffb8');
expect(tokens.gradeHex('A')).toBe('#00d4a0');
expect(tokens.gradeHex('B')).toBe('#4a9eff');
expect(tokens.gradeHex('C')).toBe('#ffb347');
expect(tokens.gradeHex('D')).toBe('#ff5252');
});
it('SPORT map carries every sport with its token color and label', () => {
expect(tokens.SPORT.nba).toEqual({ label: 'NBA', color: 'var(--s-nba)', hex: '#e94b3c' });
expect(tokens.SPORT.mlb.hex).toBe('#1e90ff');
expect(tokens.SPORT.wnba.hex).toBe('#f7944a');
expect(tokens.SPORT.soccer.hex).toBe('#3ddc84');
});
it('gradeBadgeSize maps variants and passes raw numbers through; hero is 80120px', () => {
expect(tokens.gradeBadgeSize('sm')).toBe(16);
expect(tokens.gradeBadgeSize('lg')).toBe(48);
expect(tokens.gradeBadgeSize(72)).toBe(72);
const hero = tokens.gradeBadgeSize('hero');
expect(hero).toBeGreaterThanOrEqual(80);
expect(hero).toBeLessThanOrEqual(120);
});
});
describe('Phase B — shared component contracts (§5)', () => {
const comp = (name) => read(path.join('components', 'vyndr', name));
it('Wordmark renders the green R, caret, VYNDR data-text and BETA pill', () => {
const src = comp('Wordmark.tsx');
expect(src).toContain('data-text="VYNDR"');
expect(src).toContain('className="wm-r"');
expect(src).toContain('wm-cursor');
expect(src).toContain('wm-beta');
});
it('GradeBadge colors by grade token and supports the hero size', () => {
const src = comp('GradeBadge.tsx');
expect(src).toContain('gradeColor(grade)');
expect(src).toContain('gradeBadgeSize(size)');
expect(src).toMatch(/'hero'/);
});
it('SportBadge derives its color from the SPORT token map', () => {
expect(comp('SportBadge.tsx')).toContain('SPORT[sport');
});
it('VBtn defines all five variants', () => {
const src = comp('VBtn.tsx');
['primary', 'outline', 'ghost', 'amber', 'danger'].forEach((v) => {
expect(src).toContain(`${v}:`);
});
expect(src).toContain("background: 'var(--g-a)'"); // primary = grade-green filled
});
it('Card is a Level-1 (bg-1) surface with the scanline skin', () => {
const src = comp('Card.tsx');
expect(src).toContain("background: 'var(--bg-1)'");
expect(src).toContain('scanlines');
});
it('TerminalInput uses a phosphor cursor while empty', () => {
expect(comp('TerminalInput.tsx')).toContain('phosphor-cursor');
});
it('Sparkline renders an SVG path from its data points', () => {
const src = comp('Sparkline.tsx');
expect(src).toContain('<svg');
expect(src).toContain('<path');
});
it('Ticker uses the ticker-track marquee and duplicates content for a seamless loop', () => {
const src = comp('Ticker.tsx');
expect(src).toContain('ticker-track');
expect(src).toMatch(/\{content\}\s*\{content\}/);
});
});