2ab2eeaa7d
BookComparison.tsx was built but UNROUTED (dead). Route it to the GradeResultCard via a new self-fetching BookComparisonPanel that reads the live /api/books feed (source:'bookprices' — the snapshot-locked, fenced, byte-identical store). Contract fix (Review Zero 0.1): books frequently sit at DIFFERENT lines (WNBA DK 21.5 / FD 18.5; MLB 2/3), so BookComparison now renders EACH book's own line per-row — never one shared header line implying a false same-number comparison. Honest states: single-book (the common case for MLB) → one book, "One book posting this prop.", NO crown/second row; multi-book → all books' own line+price, NONE crowned (BOOK_CROWN_ENABLED=false — no best-price claim, verified live crowned:false); no books → renders NULL (panel self-hides), never a placeholder. No regression: only the always-empty inline d.books section was replaced; grade, projection, PropLine line, and PriceTriplet price are untouched (wiring test asserts them). Freshness (0.4): bookprices is written in the SAME snapshot that locks the grade (intraday refresh touches neither gradedAt.line nor bookprices) — same fresh, no stale-label needed. Web-only → grade byte-identical trivially. Full suite 3851 green, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
66 lines
2.7 KiB
JavaScript
66 lines
2.7 KiB
JavaScript
// Book Comparison wired to the prop card (display-only). Source-assertion style,
|
||
// matching vyndrCoreScreens.test.js.
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const read = (p) => fs.readFileSync(path.join(__dirname, '../../web/src', p), 'utf8');
|
||
|
||
describe('BookComparison — honest states', () => {
|
||
const src = read('components/BookComparison.tsx');
|
||
|
||
it('renders EACH book’s own line (books frequently differ in line)', () => {
|
||
// per-row line, not one shared header line.
|
||
expect(src).toMatch(/sideChar\}\$\{b\.line\}/);
|
||
expect(src).toMatch(/gridTemplateColumns: '1fr auto auto auto'/); // book · line · price · best
|
||
});
|
||
|
||
it('single-book renders an intentional state, never a fake second row', () => {
|
||
expect(src).toMatch(/const single = books\.length === 1/);
|
||
expect(src).toContain('One book posting this prop.');
|
||
});
|
||
|
||
it('crown/BEST only shows on isBest (gated off → never shows)', () => {
|
||
expect(src).toContain('b.isBest ?');
|
||
// savings copy is guarded behind savings > 0 (0 when crown off)
|
||
expect(src).toMatch(/savings != null && savings > 0/);
|
||
});
|
||
|
||
it('empty book list → renders nothing (honest-absent)', () => {
|
||
expect(src).toMatch(/if \(!books \|\| books\.length === 0\) return null/);
|
||
});
|
||
});
|
||
|
||
describe('BookComparisonPanel — the wire to /api/books', () => {
|
||
const src = read('components/vyndr/BookComparisonPanel.tsx');
|
||
|
||
it('fetches the live /api/books per-prop endpoint', () => {
|
||
expect(src).toMatch(/\/api\/books\/\$\{encodeURIComponent\(sport\)\}/);
|
||
expect(src).toContain('BookComparison');
|
||
});
|
||
|
||
it('honest-absent: renders NULL until real book rows exist', () => {
|
||
expect(src).toMatch(/data\.books\.length === 0\) return null/);
|
||
});
|
||
|
||
it('does not enable the crown or pass a fabricated best', () => {
|
||
expect(src).not.toMatch(/isBest:\s*true/);
|
||
expect(src).not.toContain('BOOK_CROWN_ENABLED');
|
||
});
|
||
});
|
||
|
||
describe('GradeResultCard — wired + existing elements survive', () => {
|
||
const src = read('components/vyndr/GradeResultCard.tsx');
|
||
|
||
it('renders the live BookComparisonPanel (BookComparison is no longer dead)', () => {
|
||
expect(src).toContain('import BookComparisonPanel');
|
||
expect(src).toContain('<BookComparisonPanel sport={d.sport} player={d.player} stat={d.stat} side={d.side} />');
|
||
});
|
||
|
||
it('the existing grade / projection / line / price elements are untouched', () => {
|
||
expect(src).toContain('PriceTriplet'); // the price layer
|
||
expect(src).toMatch(/PROJECTION|d\.projection/); // projection row
|
||
expect(src).toContain('GradeBadge'); // the grade
|
||
expect(src).toContain("l: 'LINE'"); // the PropLine line cell
|
||
});
|
||
});
|