// VYNDR 2.0 — Phase E remaining screens (Session 36): slate adapter + GameCard // Bloomberg reskin, the four stubs-turned-real pages, and login/pricing reskins. // Adapter LOGIC runs directly via the CommonJS module; .tsx is asserted as 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 slate = require('../../web/src/lib/slateAdapter'); describe('Phase E.1 — slate adapter (best/worst line detection)', () => { it('parses American odds to a decimal payout (higher = better)', () => { expect(slate.parseAmericanOdds('+150')).toBeCloseTo(2.5, 3); expect(slate.parseAmericanOdds('-110')).toBeCloseTo(1.909, 2); expect(slate.parseAmericanOdds('garbage')).toBeNull(); expect(slate.parseAmericanOdds(null)).toBeNull(); }); it('marks the best/worst moneyline across books', () => { const rows = slate.detectBestLines({ dk: { awayML: '+150', homeML: '-170', total: '228.5' }, fd: { awayML: '+140', homeML: '-160', total: '229' }, }); const dk = rows.find((r) => r.book === 'dk'); const fd = rows.find((r) => r.book === 'fd'); expect(dk.bestAway).toBe(true); // +150 pays more than +140 expect(fd.worstAway).toBe(true); expect(fd.bestHome).toBe(true); // -160 pays more than -170 expect(dk.worstHome).toBe(true); expect(dk.ou).toBe('O/U 228.5'); }); it('does not mark best/worst for a lone book', () => { const rows = slate.detectBestLines({ dk: { awayML: '+150', homeML: '-170' } }); expect(rows[0].bestAway).toBe(false); expect(rows[0].worstAway).toBe(false); }); it('maps schedule → GameCard contract with abbrs, time, lines', () => { const cards = slate.mapScheduleToGameCards( [{ id: 'g1', sport: 'NBA', status: 'in', score: { away: 50, home: 48 }, awayTeam: { abbreviation: 'LAL', name: 'Lakers' }, homeTeam: { abbreviation: 'SA', name: 'Spurs' }, gameTime: null }], { g1: { books: { dk: { awayML: '+120', homeML: '-140' }, fd: { awayML: '+110', homeML: '-130' } } } }, [{ player: 'LeBron', team: 'LAL', text: '5-game 25+ streak' }], [{ player: 'LeBron', team: 'LAL', stat: 'Points', line: 26.5, grade: 'A', side: 'Over' }], ); expect(cards).toHaveLength(1); expect(cards[0].id).toBe('g1'); expect(cards[0].live).toBe(true); expect(cards[0].away.abbr).toBe('LAL'); expect(cards[0].lines.length).toBe(2); expect(cards[0].props.length).toBe(1); expect(cards[0].streaks.length).toBe(1); }); it('degrades gracefully when a game has no lines', () => { const cards = slate.mapScheduleToGameCards( [{ id: 'g2', awayTeam: { abbreviation: 'NYK' }, homeTeam: { abbreviation: 'BOS' } }], {}, [], [], ); expect(cards[0].lines).toEqual([]); expect(cards[0].live).toBe(false); }); }); describe('Phase E.1 — GameCard reskin (Bloomberg best/worst)', () => { const src = read('components/GameCard.tsx'); it('uses the slate adapter best-line detection + new components', () => { expect(src).toContain('detectBestLines'); expect(src).toContain('SportBadge'); expect(src).toContain('SectionHead'); }); it('renders best = green tint + green border, worst = subtle red', () => { expect(src).toContain('rgba(0,212,160,.13)'); expect(src).toContain('rgba(255,82,82,.07)'); }); it('dropped the emoji sport marker', () => { expect(src).not.toContain('SPORT_EMOJI'); }); }); describe('Phase E.3 — stubs are now real pages', () => { const pages = ['compare', 'invite', 'help', 'about']; it.each(pages)('/%s is no longer a RouteStub', (p) => { expect(read(`app/${p}/page.tsx`)).not.toContain('RouteStub'); }); it('compare renders two player inputs + a verdict', () => { const src = read('app/compare/page.tsx'); expect((src.match(/TerminalInput/g) || []).length).toBeGreaterThanOrEqual(2); expect(src).toContain('VYNDR VERDICT'); }); it('invite renders the 3-friends referral messaging', () => { expect(read('app/invite/page.tsx')).toContain('3 friends'); }); it('help renders a searchable FAQ', () => { const src = read('app/help/page.tsx'); expect(src).toContain('TerminalInput'); expect(src).toContain('FAQ'); }); it('about uses the system-voice brand line', () => { expect(read('app/about/page.tsx')).toContain('give it back'); }); }); describe('Phase E.2 — login + pricing reskins', () => { it('login uses scanlines, system voice, and the new Wordmark', () => { const src = read('app/login/page.tsx'); expect(src).toContain('scanlines'); expect(src).toContain('ACCESS THE SIGNAL'); expect(src).toContain("from '@/components/vyndr'"); }); it('pricing mounts the ClaimMeter', () => { expect(read('app/pricing/page.tsx')).toContain('ClaimMeter'); }); });