Files
vyndr/tests/unit/vyndrDesignSystem.test.js
T
builtbykev 32069863dc Session 41: P0 audit fixes — MLB stat_types, broken routes, tier mismatch, self-hosted fonts (1940 tests)
- Backend: whitelist MLB stat_types in analyze.js + scan.js gates (mirrors
  python validation.py); fixes MLB scans 400ing.
- Routes: /settings -> /profile, /report -> /blog redirect pages.
- Profile: read tier from useAuth().tier (nav's source) to kill the
  Free-vs-DESK mismatch.
- Fonts: self-host Inter/JetBrains Mono/IBM Plex Mono via next/font, drop the
  503ing fonts.googleapis.com <link>; rewire literal font-family refs to vars.
- Kept /settings/security (real MFA page) intact — NOT clobbered to a redirect.
- +33 tests (1907 -> 1940), 149 suites; web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:55:40 -04:00

191 lines
7.6 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', () => {
// Session 41 — fonts are self-hosted via next/font, so the families now
// come through the --font-* variables (with the literal names kept as
// fallbacks for any non-next/font context).
expect(css).toMatch(/--sans:\s*var\(--font-sans\),\s*'Inter'/);
expect(css).toMatch(/--mono:\s*var\(--font-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('self-hosts Inter + JetBrains Mono via next/font (Session 41 — no CDN <link>)', () => {
expect(layout).toContain("from 'next/font/google'");
expect(layout).toContain('Inter(');
expect(layout).toContain('JetBrains_Mono(');
expect(layout).not.toMatch(/href=["'][^"']*fonts\.googleapis\.com/);
});
});
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\}/);
});
});