1664e1b3d5
Extend /explore (ExploreHub) into the 365-day never-dark hub. Two new self-hiding sections feed REAL always-available data into the offseason: - NewsWire (components/vyndr/NewsWire.tsx): real ESPN headlines from /api/news/:sport (newest-first, mono timestamps, type chips, player/team links) + real injuries from /api/schedule/:sport/injuries (OUT/GTD chips, token colors). Reuses the retired TerminalTemplates INJURY_WIRE layout but never routes its sample constants. Self-hides when both feeds are empty. - FuturesBoard (components/vyndr/FuturesBoard.tsx): real futures from /api/futures/:sport — championship/win-total/award markets, mono tabular prices + movement colored by the contract (shortening=green / drifting=amber / flat=dim, NEVER red; move shown ONLY when the backend supplies one). Carries the honest "TRACKED · NOT GRADED" label — no fabricated grades on futures. Self-hides when markets:[]. - ExploreHub is offseason-aware (via emptyState OFF_SEASON month check): the hub LEADS with futures + wire when the board is dark, COMPLEMENTS the live board in-season. Sport selector kept; each section self-hides independently. - Testable pure helpers: lib/futuresMove.js (move→color, never red) + lib/newsFormat.js (timeAgo mono-stamp, ESPN type labels). Contracts consumed (Wave 2A owns the proxy/service files); code self-hides on fetch failure if a proxy isn't present yet. Tests: tests/unit/newsWire.test.js + futuresBoard.test.js (26 new). Full suite 259 suites / 3144 green; web build exit 0. vyndrParityQA stays green (mono data, no glitch on data surfaces). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.2 KiB
JavaScript
81 lines
3.2 KiB
JavaScript
// Wave 2B — OFFSEASON HUB · FuturesBoard. Source + helper asserts. Futures are
|
|
// TRACKED, NOT GRADED — the board carries that label, never renders a grade,
|
|
// self-hides on an empty feed, and colors movement per the contract
|
|
// (shortening→green / drifting→amber / flat→dim) — NEVER red for a drift.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const src = fs.readFileSync(path.join(WEB, 'components', 'vyndr', 'FuturesBoard.tsx'), 'utf8');
|
|
const { moveColor, moveLabel, MOVE_COLOR } = require('../../web/src/lib/futuresMove');
|
|
|
|
describe('futuresMove — the color contract (never red)', () => {
|
|
it('shortening → green (toward the selection)', () => {
|
|
expect(moveColor('shortening')).toBe('var(--g-a)');
|
|
});
|
|
it('drifting → amber (caution, NOT a miss)', () => {
|
|
expect(moveColor('drifting')).toBe('var(--amber)');
|
|
});
|
|
it('flat / absent / unknown → dim (say less)', () => {
|
|
expect(moveColor('flat')).toBe('var(--text-2)');
|
|
expect(moveColor(undefined)).toBe('var(--text-2)');
|
|
expect(moveColor('sideways')).toBe('var(--text-2)');
|
|
});
|
|
it('NO move ever renders red (--miss reserved for settled-negative)', () => {
|
|
Object.values(MOVE_COLOR).forEach((v) => expect(v).not.toMatch(/miss/));
|
|
['shortening', 'drifting', 'flat', undefined, 'x'].forEach((m) =>
|
|
expect(moveColor(m)).not.toMatch(/miss/)
|
|
);
|
|
});
|
|
it('moveLabel is empty when there is no move (never fabricated)', () => {
|
|
expect(moveLabel(undefined)).toBe('');
|
|
expect(moveLabel('shortening')).toMatch(/SHORTENING/);
|
|
});
|
|
});
|
|
|
|
describe('FuturesBoard.tsx source', () => {
|
|
it('fetches the REAL futures endpoint', () => {
|
|
expect(src).toMatch(/\/api\/futures\//);
|
|
});
|
|
|
|
it('carries the honest "TRACKED · NOT GRADED" label', () => {
|
|
expect(src).toContain('TRACKED · NOT GRADED');
|
|
});
|
|
|
|
it('NEVER renders a grade on a future (no GradeBadge)', () => {
|
|
expect(src).not.toContain('GradeBadge');
|
|
expect(src).not.toMatch(/import.*GradeBadge/);
|
|
});
|
|
|
|
it('SELF-HIDES when there are no markets (returns null)', () => {
|
|
expect(src).toMatch(/return null/);
|
|
expect(src).toMatch(/list\.length === 0/);
|
|
});
|
|
|
|
it('renders real markets + selections with mono tabular prices', () => {
|
|
expect(src).toMatch(/list\.map/);
|
|
expect(src).toMatch(/selections/);
|
|
expect(src).toContain('className="mono"');
|
|
expect(src).toContain('tabular-nums');
|
|
});
|
|
|
|
it('shows a move ONLY when the backend supplied one, via the contract color', () => {
|
|
expect(src).toContain('moveColor');
|
|
expect(src).toContain('moveLabel');
|
|
// movement is gated on a truthy label (never fabricated)
|
|
expect(src).toMatch(/label &&/);
|
|
});
|
|
|
|
it('data surface carries NO glitch classes', () => {
|
|
expect(src).not.toMatch(/wm-tear|glitch-shift|head-tear|glitch-hover/);
|
|
});
|
|
});
|
|
|
|
describe('ExploreHub mounts FuturesBoard', () => {
|
|
const hub = fs.readFileSync(path.join(WEB, 'components', 'ExploreHub.tsx'), 'utf8');
|
|
it('imports + mounts <FuturesBoard sport=', () => {
|
|
expect(hub).toMatch(/import FuturesBoard from '@\/components\/vyndr\/FuturesBoard'/);
|
|
expect(hub).toMatch(/<FuturesBoard sport=\{sport\}/);
|
|
});
|
|
});
|