6bc18d823c
Six live untruths corrected — no grade/snapshot/scorer/pipeline touched: 1. /compare — hardcoded Jokic A+/Wembanyama A + fake VERDICT replaced with an honest in-development state; removed from Nav + BottomTabBar (route still resolves, never the sample). Real two-player build is later. 2. Pricing — founder Desk $34.99→$44.99 (matches lib/checkout.js), Analyst $14.99; removed the struck $19.99/$44.99 "regular" numbers and DeskShowcase's stale $34.99. First-100 counter is real (ClaimMeter → Stripe countFounderSeats); no fake "first 50" desk claim added (no such counter exists). 3. FAQ "NexaPay" → Stripe (verified: live checkout is Next→Express→checkout.stripe.com). 4. FAQ + Features "Brier/CLV published from day one" removed (not surfaced yet) — returns when real. Backend Brier compute untouched. 5. MobileEdgeBoard removed from the Slate — its edge% feed was a miscalibrated placeholder (masked >40% as "—"); phones now show the real game cards. 6. Price triplet — never-computed model/EV now derives NO_MODEL (honest absent, MODEL "—" / "NOT PRICED", no verdict) instead of QUARANTINE's false "we suppressed our price / a leg is poisoned" copy. Fixes grade card + LiveHeroProp. Full suite 3833 green, web build exit 0. Tests updated to the new honest contracts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
117 lines
4.9 KiB
JavaScript
117 lines
4.9 KiB
JavaScript
// 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 shows an honest in-development state — no sample grades, no fake verdict', () => {
|
|
const src = read('app/compare/page.tsx');
|
|
expect(src).toMatch(/IN DEVELOPMENT/i);
|
|
expect(src).not.toContain('VYNDR VERDICT');
|
|
expect(src).not.toMatch(/Joki|Wembanyama/); // no hardcoded sample players
|
|
});
|
|
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');
|
|
});
|
|
});
|