Files
vyndr/tests/unit/vyndrSystems.test.js
T
builtbykev 956a7455eb Session 38: Design system Phase G — living layer, i18n/odds, a11y, paywall, parlay math (1890 tests)
VYNDR 2.0 conversion, Phase G (the systems that make the design alive). All 5
wired. Frontend-only; zero backend changes.

- lib/parlayMath.js: correlation model (0.62/0.34/0.06/0) + parlayGrade penalty
  + grade->odds + combined odds (frontend; backend parlayService unchanged).
- lib/oddsFormat.js: fmtOdds across american/decimal/fractional/implied with the
  totals-pass-through rule (safer than the prototype's parseAm, which would
  mis-convert 228.5) + region presets.
- lib/prefs.js: applyPrefs sets <html data-*> (the S33 a11y CSS layer) + load/save.
- lib/liveTick.js: single tick engine (SSR/test-safe, no auto-start, fresh state).
- lib/checkout.js: checkoutUrl(plan).
- LiveLayer (useLive/LiveNumber/HeartbeatBar) under the Nav ticker; GlobalHosts in
  layout applies prefs + registers __prefs/__goPaywall/__checkout + hosts the
  Preferences and Paywall modals. Nav read-meter is now a paywall trigger.

Gotchas: useEffect can't return a Set.delete unsub directly (boolean != cleanup);
header grew to 124px so layout paddingTop + Slate sticky-top updated to match.

18 new tests. Backend 1872 -> 1890, 146 suites, zero regressions. Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:37:31 -04:00

175 lines
7.4 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 — Phase G systems (Session 38): living layer, i18n/odds, a11y
// prefs, paywall/checkout, parlay correlation math. Pure logic runs directly
// via the CommonJS modules; the .tsx glue is asserted against source text.
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 parlay = require('../../web/src/lib/parlayMath');
const odds = require('../../web/src/lib/oddsFormat');
const prefs = require('../../web/src/lib/prefs');
const { liveTick } = require('../../web/src/lib/liveTick');
const { checkoutUrl } = require('../../web/src/lib/checkout');
describe('Phase G.5 — parlay correlation math (§12)', () => {
const jokicPts = { player: 'Jokic', team: 'DEN', sport: 'nba', grade: 'A' };
const jokicReb = { player: 'Jokic', team: 'DEN', sport: 'nba', grade: 'A' };
const murray = { player: 'Murray', team: 'DEN', sport: 'nba', grade: 'A' };
const judge = { player: 'Judge', team: 'NYY', sport: 'mlb', grade: 'A' };
const tatum = { player: 'Tatum', team: 'BOS', sport: 'nba', grade: 'A' };
it('encodes the correlation tiers exactly', () => {
expect(parlay.getCorrelation(jokicPts, jokicReb)).toBe(0.62); // same player
expect(parlay.getCorrelation(jokicPts, murray)).toBe(0.34); // same team+sport
expect(parlay.getCorrelation(jokicPts, tatum)).toBe(0.06); // same league
expect(parlay.getCorrelation(jokicPts, judge)).toBe(0); // cross-sport
});
it('parlayGrade penalizes a correlated stack vs an independent one', () => {
const correlated = parlay.parlayGrade([jokicPts, jokicReb]); // 0.62 → penalty
const independent = parlay.parlayGrade([judge, tatum]); // cross-sport, no penalty
const order = parlay.GRADE_ORDER;
expect(order.indexOf(correlated)).toBeGreaterThan(order.indexOf(independent));
expect(independent).toBe('A');
});
it('converts American↔decimal correctly', () => {
expect(parlay.amToDec(110)).toBeCloseTo(2.1, 3);
expect(parlay.amToDec(-135)).toBeCloseTo(1.741, 2);
expect(parlay.decToAm(2.1)).toBe(110);
});
it('maps grades to representative odds and combines a slip', () => {
expect(parlay.GRADE_ODDS['A+']).toBe(-135);
const dec = parlay.combinedDecimal([jokicPts, judge]); // 110 × 110
expect(dec).toBeCloseTo(2.1 * 2.1, 3);
expect(parlay.combinedAmerican([])).toBe('—');
expect(parlay.combinedAmerican([jokicPts])).toMatch(/^[+-]\d+$/);
});
});
describe('Phase G.2 — odds formatting (§9, the totals-pass-through rule)', () => {
it('converts moneylines across formats', () => {
expect(odds.fmtOdds('+150', 'decimal')).toBe('2.50');
expect(odds.fmtOdds('-110', 'decimal')).toBe('1.91');
expect(odds.fmtOdds('+150', 'fractional')).toBe('3/2');
expect(odds.fmtOdds('-110', 'implied')).toBe('52%');
expect(odds.fmtOdds('+150', 'american')).toBe('+150');
});
it('treats integer NUMBERS (the grade→odds map) as moneylines', () => {
expect(odds.fmtOdds(110, 'american')).toBe('+110');
expect(odds.fmtOdds(-135, 'decimal')).toBe('1.74');
});
it('CRITICAL: totals / lines pass through UNCHANGED', () => {
expect(odds.fmtOdds('228.5', 'decimal')).toBe('228.5'); // half-point total
expect(odds.fmtOdds('229', 'implied')).toBe('229'); // unsigned integer total
expect(odds.fmtOdds('-7.5', 'american')).toBe('-7.5'); // spread
expect(odds.fmtOdds(228.5, 'decimal')).toBe(228.5); // non-integer number
});
it('region presets set odds format + currency', () => {
expect(odds.regionPreset('UK')).toMatchObject({ odds: 'fractional', currency: 'GBP', currencySymbol: '£' });
expect(odds.regionPreset('EU').odds).toBe('decimal');
expect(odds.regionPreset('US').odds).toBe('american');
});
});
describe('Phase G.3 — accessibility prefs (§10)', () => {
function fakeEl() {
const attrs = {};
return {
attrs,
setAttribute: (k, v) => { attrs[k] = v; },
removeAttribute: (k) => { delete attrs[k]; },
};
}
function fakeStore() {
const m = {};
return { getItem: (k) => (k in m ? m[k] : null), setItem: (k, v) => { m[k] = String(v); } };
}
it('applyPrefs sets the data-* attributes the CSS layer keys off', () => {
const el = fakeEl();
prefs.applyPrefs({ contrast: 'high', motion: 'reduced', text: 'xl', cb: 'on', font: 'readable' }, el);
expect(el.attrs['data-contrast']).toBe('high');
expect(el.attrs['data-motion']).toBe('reduced');
expect(el.attrs['data-text']).toBe('xl');
expect(el.attrs['data-cb']).toBe('1');
expect(el.attrs['data-font']).toBe('readable');
});
it('applyPrefs removes attributes for default values', () => {
const el = fakeEl();
el.setAttribute('data-contrast', 'high');
prefs.applyPrefs({ contrast: 'off', text: 'base' }, el);
expect(el.attrs['data-contrast']).toBeUndefined();
expect(el.attrs['data-text']).toBeUndefined();
});
it('prefs round-trip through storage', () => {
const store = fakeStore();
prefs.savePrefs({ contrast: 'high', region: 'UK' }, store);
const loaded = prefs.loadPrefs(store);
expect(loaded.contrast).toBe('high');
expect(loaded.region).toBe('UK');
expect(loaded.lang).toBe('en'); // defaults merged
});
});
describe('Phase G.1 — living-layer tick engine (§8)', () => {
beforeEach(() => liveTick.reset());
afterEach(() => liveTick.reset());
it('fans out one tick to all subscribers', () => {
let calls = 0;
const unsub = liveTick.subscribe(() => { calls += 1; });
liveTick.tick();
liveTick.tick();
expect(calls).toBe(2);
unsub();
liveTick.tick();
expect(calls).toBe(2); // unsubscribed
});
it('advances tick and creeps the graded count, with a fresh state object', () => {
const first = liveTick.state;
for (let i = 0; i < 7; i++) liveTick.tick();
expect(liveTick.state.tick).toBe(7);
expect(liveTick.state.graded).toBeGreaterThan(first.graded);
expect(liveTick.state).not.toBe(first); // new object → React re-renders
});
});
describe('Phase G.4 — checkout (§12)', () => {
it('maps a plan tier to the checkout URL', () => {
expect(checkoutUrl('analyst')).toBe('/api/checkout?tier=analyst');
expect(checkoutUrl('desk')).toBe('/api/checkout?tier=desk');
expect(checkoutUrl('garbage')).toBe('/api/checkout?tier=analyst'); // safe default
});
});
describe('Phase G — React glue wired correctly', () => {
it('HeartbeatBar consumes the live tick + renders SIGNAL LIVE', () => {
const src = read('components/vyndr/LiveLayer.tsx');
expect(src).toContain('useLive');
expect(src).toContain('SIGNAL LIVE');
expect(src).toContain('ekg-track');
expect(src).toContain('count-tick'); // LiveNumber pop
});
it('GlobalHosts registers the design globals + applies prefs on mount', () => {
const src = read('components/vyndr/GlobalHosts.tsx');
expect(src).toContain('window.__prefs');
expect(src).toContain('window.__goPaywall');
expect(src).toContain('window.__checkout');
expect(src).toContain('applyPrefs');
});
it('Nav mounts the heartbeat, a prefs trigger, and a paywall read-meter', () => {
const src = read('components/Nav.tsx');
expect(src).toContain('<HeartbeatBar');
expect(src).toContain('window.__prefs');
expect(src).toContain('window.__goPaywall');
});
it('layout mounts GlobalHosts', () => {
expect(read('app/layout.tsx')).toContain('<GlobalHosts />');
});
});