Files
vyndr/tests/unit/vyndrDesignSystem.test.js
builtbykev e0a26a8055 M3.1 token alignment — the palette to Design's exact package values
Reconcile finding: the live tokens had DRIFTED from Design's package (HANDOFF
"Tokens (exact)"). Per the standing conflict rule (Design specified exact hexes
→ rejected the drift → Design wins), aligned the whole palette:
- Surfaces: --bg-1 #0E0E16→#0E0E14, --bg-2 #15151F→#14141E; added --bg-deep
  #0A0A10 + --hairline #101018 (Design's ramp).
- Text: --text-0 #E8E8F0→#F0F0F0, --text-1 #7A7A8E→#B8BCC8 (Design's secondary
  is far brighter), --text-2 #4A4A5E→#707080, added --text-3 #4a4a58 micro.
- Borders: #1E1E2E→#1E1E2A, #2A2A3E→#2A2A38.
- GRADES (the big one): B blue #4A9EFF → neutral-bright WHITE #F0F0F0; C amber
  #FFB347 → muted GREY #B8BCC8; D #FF5252 → #FF4757. The old blue/amber actually
  violated DESIGN-SPEC v2's OWN "B neutral-bright, C muted" — this fixes a
  long-standing drift, confirmed by Design's package. Amber stays its own token
  (--amber); --warning decoupled to --amber; --miss → #FF4757.
- vyndrTokens.js GRADE_HEX mirror + the design-system test updated to match.

Big VISUAL change (grade color language), VISUALLY UNVERIFIED at 390px/desktop
— on the Chrome-audit list. Every surface inherits it, so it lands first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:19:30 -04:00

194 lines
7.8 KiB
JavaScript
Raw Permalink 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: #F0F0F0');
expect(css).toContain('--g-c: #B8BCC8');
expect(css).toContain('--g-d: #FF4757');
});
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('#f0f0f0');
expect(tokens.gradeHex('C')).toBe('#b8bcc8');
expect(tokens.gradeHex('D')).toBe('#ff4757');
});
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 RESTS on each item (DS5 punctuated stillness) — no infinite marquee', () => {
// DS5 (Part 4, #7) retired the continuous .ticker-track marquee: the ticker
// now holds each ranked item ≥4s and pulses only on change. See
// ds5PricingStates.test.js for the full motion lock.
const src = comp('Ticker.tsx');
expect(src).not.toContain('ticker-track');
expect(src).toContain('ticker-rest');
});
});