Files
vyndr/tests/unit/rowGrammar.test.js
T
builtbykev f24f9412f1 Rev 3 matchup chips: TeamChip primitive + board-row team context
Design Rev 3 anchors every matchup/context abbr with a 10-12px team tile. New
reusable TeamChip renders the real TeamLogo (licensed ESPN logo where it
resolves, team-colored monogram otherwise — the resolver is already built) at
that size + the abbr, sitting inside the row so it inherits the ranked opacity
ramp. First placement: StatStrip's player/team context (name → team-chip →
archetype). ROW-GRAMMAR identity-run test updated to the chip marker.

Remaining Rev 3 placements to thread TeamChip into (reusable, mechanical):
parlay legs, grade-shift header, pitcher "vs", /u recent-settled, other
matchup context lines. Game-card headers already carry TeamLogo (TeamLink).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:27:47 -04:00

134 lines
5.8 KiB
JavaScript

// S6 (A1 board) — ROW-GRAMMAR source locks (specs/ROW-GRAMMAR.md).
// Density is unlimited when grammar is fixed: every element is DATA with ONE
// meaning in the SAME slot on every row of its type. These tests fail if a
// slot moves, a color changes meaning, or truncation sneaks onto a name.
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 strip = read('components/vyndr/StatStrip.tsx');
const spec = fs.readFileSync(path.join(__dirname, '..', '..', 'specs', 'ROW-GRAMMAR.md'), 'utf8');
/** Assert `markers` appear in this exact order in `src`, starting at `from`. */
function assertOrder(src, markers, from = 0) {
let cursor = from;
for (const m of markers) {
const i = src.indexOf(m, cursor);
expect({ marker: m, found: i >= 0 }).toEqual({ marker: m, found: true });
cursor = i + m.length;
}
return cursor;
}
describe('ROW-GRAMMAR §2 — canonical prop-row slot order (snapshot mode)', () => {
test('stat+line → market context (dot, movement) → model output (revision, grade) → outcome → actions → provenance', () => {
// Anchor on the graded row's stat+side+line span (unique to it).
const base = strip.indexOf("<span style={{ color: '#fff' }}>{p.stat} {p.side}{p.line}</span>");
expect(base).toBeGreaterThan(-1);
assertOrder(strip, [
'<BestPriceDot p={p} />', // slot 4a — best available price
'<MovementChip p={p} />', // slot 4b — market movement
'p.revisedFrom', // slot 5a — revision strikethrough
'<GradeBadge grade={p.grade}', // slot 5b — the current grade
'<LiveTracker p={p} />', // slot 6a — live proto-outcome (S11)
'<OutcomeChip p={p} />', // slot 6b — settled result
'<ParlayBtn p={p} />', // slot 7a — action
'<BookItTeaser p={p} />', // slot 7b — action
'Graded {p.gradedAt.ago}', // slot 8 — provenance, always last
], base);
});
test('sub-line order: last-10 dots → sparkline → current-line delta', () => {
const base = strip.indexOf('ROW-GRAMMAR sub-line');
expect(base).toBeGreaterThan(-1);
assertOrder(strip, [
'<DotStrip dots={p.last10Dots} />',
'<LineSparkline p={p} />',
'Current {p.delta.currentLine}',
], base);
});
test('strip header: identity run (name → team → archetype) then viability chips', () => {
const base = strip.indexOf('// compact');
expect(base).toBeGreaterThan(-1);
assertOrder(strip, [
'<PlayerName',
'<TeamChip team={team}', // Rev 3 — team chip anchors the abbr
'<ArchetypeBadge archetype={archetype.primary}',
'<ViabilityChips lineup={lineup} injury={injury} />',
], base);
});
test('live/settled/dead rows suppress actions (the bet window is over) — S11 amendment', () => {
expect(strip).toContain('{!p.outcome && !p.dead && !p.live && <ParlayBtn p={p} />}');
expect(strip).toContain('{!p.outcome && !p.dead && !p.live && <BookItTeaser p={p} />}');
});
});
describe('ROW-GRAMMAR §3 — one meaning per color', () => {
const section = (start, end) => {
const a = strip.indexOf(start);
const b = end ? strip.indexOf(end, a) : strip.length;
expect(a).toBeGreaterThan(-1);
return strip.slice(a, b === -1 ? strip.length : b);
};
test('sparkline is green/amber/dim — never red (nothing has settled)', () => {
const src = section('export function LineSparkline', 'export function ViabilityChips');
expect(src).toContain("'var(--g-a)'");
expect(src).toContain("'var(--amber)'");
expect(src).not.toContain('--miss');
});
test('dot strip: filled = signal green, hollow = dim (never red/amber)', () => {
const src = section('export function DotStrip', 'export function LineSparkline');
expect(src).toContain('var(--g-a');
expect(src).toContain('var(--text-2');
expect(src).not.toContain('--miss');
expect(src).not.toContain('--amber');
});
test('movement chips keep STEAM amber / VALUE green (QA.20 family)', () => {
const src = section('function MovementChip', 'export interface StripArchetype');
expect(src).toContain("steam ? 'var(--amber");
expect(src).not.toContain('--miss');
});
test('live TRACKING mark is green/amber only — an in-progress prop is never red (S11)', () => {
const src = section('export function LiveTracker', 'export function DotStrip');
expect(src).toContain('var(--g-a');
expect(src).toContain('var(--amber');
expect(src).not.toContain('--miss');
// The label never implies re-grading: TRACKING, read locked pre-game.
expect(src).toContain('TRACKING — read locked pre-game');
// Data → mono.
expect(src).toContain('className="mono"');
});
});
describe('ROW-GRAMMAR §1.4 — no truncation of names/times/pitchers', () => {
test('StatStrip never ellipsizes', () => {
expect(strip).not.toMatch(/textOverflow|text-overflow|ellipsis/);
});
test('SearchModal result names wrap, never truncate', () => {
const sm = read('components/vyndr/SearchModal.tsx');
expect(sm).not.toMatch(/textOverflow|ellipsis/);
});
});
describe('ROW-GRAMMAR — the law is written down and wired', () => {
test('spec exists with the canonical slot order and color law', () => {
expect(spec).toContain('CANONICAL PROP-ROW SLOT ORDER');
expect(spec).toContain('one meaning per color');
expect(spec).toContain('never');
expect(spec).not.toMatch(/!/); // VOICE — no exclamation points
});
test('data marks are mono', () => {
// DotStrip + sub-line render with the mono class (data never sans).
const dot = strip.slice(strip.indexOf('export function DotStrip'), strip.indexOf('export function LineSparkline'));
expect(dot).toContain('className="mono"');
});
});