Wire BookComparison to the prop card (display-only, honest states)

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
This commit is contained in:
Kev
2026-07-29 04:09:34 -04:00
parent 3a05447f77
commit 2ab2eeaa7d
5 changed files with 179 additions and 30 deletions
+65
View File
@@ -0,0 +1,65 @@
// 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 books 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
});
});