ae3cff9dbd
ESPN BET is defunct — PENN/ESPN terminated the deal; PENN rebranded it to theScore Bet (Dec 1 2025) and ESPN is now exclusive with DraftKings. Removed the ESPN BET entries from the BookChip map (web/src/lib/books.js) and added theScore Bet (mono TS, slug thescore) as the successor. Added 'thescore' to the backend oddsNormalizer ALLOWED_BOOKS so the feed's lines are accepted; synced the bookWordmark test list. The ESPN references in src/config/sports.js are ESPN's STATS API (data provider, unrelated to the sportsbook) — left untouched. Flagged in specs/design-reference/HANDOFF.md that the design mockups' BookChip row still shows ESPN BET and needs the same one-swap on the next refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.3 KiB
JavaScript
120 lines
5.3 KiB
JavaScript
// Wave 2B (data train) — sportsbook wordmarks. Every book the odds feed emits
|
|
// resolves to a real brand (name + non-gray color); the ~8 major books render a
|
|
// bundled local wordmark SVG; the ledger row renders BookWordmark, never bare
|
|
// lowercase `row.book`. The SVGs are self-authored styled text wordmarks (not
|
|
// copied trademarked logos), swappable for official press-kit art later.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { bookInfo, bookSlug, hasBookSvg, BUNDLED_BOOK_SVGS } = require('../../web/src/lib/books');
|
|
|
|
// The exact keys oddsNormalizer.ALLOWED_BOOKS emits into the pipeline.
|
|
// 'thescore' = theScore Bet (PENN), successor to the defunct ESPN BET (item 7).
|
|
const ALLOWED_BOOKS = ['draftkings', 'fanduel', 'betmgm', 'caesars', 'fanatics', 'bet365', 'hardrockbet', 'pointsbet', 'betrivers', 'pinnacle', 'thescore'];
|
|
const DEFAULT_FG = '#B8BCC8';
|
|
|
|
const REPO = path.resolve(__dirname, '../..');
|
|
const read = (p) => fs.readFileSync(path.join(REPO, p), 'utf8');
|
|
|
|
describe('bookInfo — every ALLOWED_BOOK is a real brand (no neutral-gray fall)', () => {
|
|
test.each(ALLOWED_BOOKS)('%s resolves to a real brand name + color', (key) => {
|
|
const b = bookInfo(key);
|
|
// name must be a real brand, not the raw uppercased key echoed back
|
|
// (compare the ACTUAL name to the uppercased key — do NOT uppercase the
|
|
// name, or a correct casing-only brand like "DraftKings" would trivially
|
|
// equal "DRAFTKINGS" and false-fail; "bet365" ≠ "BET365" still passes).
|
|
expect(b.name).toBeTruthy();
|
|
expect(b.name).not.toBe(key.toUpperCase());
|
|
// color must not be the neutral-gray default
|
|
expect(b.fg).toBeTruthy();
|
|
expect(b.fg.toLowerCase()).not.toBe(DEFAULT_FG.toLowerCase());
|
|
// never a bare lowercase feed key echoed as the display name — EXCEPT
|
|
// bet365, whose official brand wordmark genuinely is lowercase "bet365".
|
|
if (key !== 'bet365') expect(b.name).not.toBe(key);
|
|
});
|
|
|
|
test('the 6 previously-missing keys now resolve (were gray)', () => {
|
|
expect(bookInfo('fanatics').name).toBe('Fanatics');
|
|
expect(bookInfo('bet365').name).toBe('bet365');
|
|
expect(bookInfo('hardrockbet').name).toBe('Hard Rock');
|
|
expect(bookInfo('betrivers').name).toBe('BetRivers');
|
|
expect(bookInfo('pointsbet').name).toBe('PointsBet');
|
|
expect(bookInfo('pinnacle').name).toBe('Pinnacle');
|
|
});
|
|
|
|
test('case-insensitive: DraftKings / DK / draftkings all resolve the same', () => {
|
|
expect(bookInfo('DraftKings').name).toBe('DraftKings');
|
|
expect(bookInfo('DK').name).toBe('DraftKings');
|
|
expect(bookInfo('draftkings').name).toBe('DraftKings');
|
|
});
|
|
});
|
|
|
|
describe('bundled book wordmark SVGs', () => {
|
|
const EXPECTED = ['draftkings', 'fanduel', 'betmgm', 'caesars', 'bet365', 'pinnacle', 'hardrockbet', 'betrivers'];
|
|
|
|
test('BUNDLED_BOOK_SVGS is exactly the 8 major books', () => {
|
|
expect([...BUNDLED_BOOK_SVGS].sort()).toEqual([...EXPECTED].sort());
|
|
});
|
|
|
|
test.each(EXPECTED)('web/public/books/%s.svg exists and is a real <svg>', (slug) => {
|
|
const svg = read(`web/public/books/${slug}.svg`);
|
|
expect(svg).toMatch(/<svg[\s\S]*<\/svg>/);
|
|
});
|
|
|
|
test('bookSlug resolves feed keys + codes to the bundled slug', () => {
|
|
expect(bookSlug('draftkings')).toBe('draftkings');
|
|
expect(bookSlug('DK')).toBe('draftkings');
|
|
expect(bookSlug('hardrockbet')).toBe('hardrockbet');
|
|
expect(hasBookSvg('betmgm')).toBe(true);
|
|
expect(hasBookSvg('MGM')).toBe(true);
|
|
});
|
|
|
|
test('a book with no bundled SVG (fanatics) still has NO svg but a real name', () => {
|
|
expect(hasBookSvg('fanatics')).toBe(false);
|
|
expect(bookInfo('fanatics').name).toBe('Fanatics');
|
|
});
|
|
|
|
test('unknown book → no svg, no crash', () => {
|
|
expect(hasBookSvg('zzzbook')).toBe(false);
|
|
expect(bookSlug('zzzbook')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('BookWordmark source — SVG-first, styled-text fallback, no lowercase leak', () => {
|
|
const src = read('web/src/components/vyndr/BookWordmark.tsx');
|
|
|
|
test('references the local /books/{slug}.svg path', () => {
|
|
expect(src).toContain('/books/');
|
|
expect(src).toMatch(/\$\{slug\}\.svg/);
|
|
});
|
|
|
|
test('resolves via the books registry (hasBookSvg + bookInfo + bookSlug)', () => {
|
|
expect(src).toContain('hasBookSvg');
|
|
expect(src).toContain('bookInfo');
|
|
expect(src).toContain('bookSlug');
|
|
});
|
|
|
|
test('falls back to the brand NAME (never the raw lowercase key)', () => {
|
|
// the fallback renders b.name (properly-cased brand), not the raw `book` prop
|
|
expect(src).toContain('b.name');
|
|
expect(src).not.toMatch(/>\s*\{book\}\s*</); // never render the raw prop verbatim
|
|
});
|
|
});
|
|
|
|
describe('ledger + public-profile rows render BookWordmark, not bare row.book', () => {
|
|
test('ledger page imports + uses BookWordmark', () => {
|
|
const src = read('web/src/app/ledger/page.tsx');
|
|
expect(src).toMatch(/import\s*\{[^}]*BookWordmark[^}]*\}\s*from\s*'@\/components\/vyndr'/);
|
|
expect(src).toContain('<BookWordmark book={row.book}');
|
|
// the old bare-text render is gone
|
|
expect(src).not.toContain("{row.book || '—'}");
|
|
});
|
|
|
|
test('public profile page imports + uses BookWordmark', () => {
|
|
const src = read('web/src/app/u/[handle]/PublicProfile.tsx');
|
|
expect(src).toContain('BookWordmark');
|
|
expect(src).toContain('<BookWordmark book={row.book}');
|
|
expect(src).not.toContain("{row.book || '—'}");
|
|
});
|
|
});
|