d3637e7abd
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
StatStrip violations fixed: MovementChip before the grade (market
context before model output); ViabilityChips after the archetype
(identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
real {t,line} points per grade (seeded with the lock, deduped when
flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
/api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
buckets, outliers clamped) only past the centralized n>=20 gate;
ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
players via /api/players/search per sport + static lib/teams.js
(soccer deliberately absent); Nav search icon + Search first in the
mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
(4 decorative font files off the slow-4G critical path).
2654 -> 2698 tests (226 suites) green; web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
4.0 KiB
JavaScript
93 lines
4.0 KiB
JavaScript
// S6 (A1 board) — global search: ⌘K modal (players + teams) + mobile entry
|
|
// points. Source locks + the static team registry's honesty rules.
|
|
|
|
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 { TEAMS, searchTeams, teamHref } = require('../../web/src/lib/teams');
|
|
|
|
describe('lib/teams — the static registry', () => {
|
|
test('full leagues: 30 MLB, 30 NBA, 13 WNBA — soccer deliberately absent', () => {
|
|
const by = (sp) => TEAMS.filter((t) => t.sport === sp);
|
|
expect(by('mlb')).toHaveLength(30);
|
|
expect(by('nba')).toHaveLength(30);
|
|
expect(by('wnba')).toHaveLength(13);
|
|
expect(by('soccer')).toHaveLength(0); // no canonical registry → absent beats wrong
|
|
});
|
|
|
|
test('no duplicate abbr within a sport; every entry has name + abbr', () => {
|
|
const seen = new Set();
|
|
for (const t of TEAMS) {
|
|
expect(Boolean(t.name && t.abbr && t.sport)).toBe(true);
|
|
const k = `${t.sport}|${t.abbr}`;
|
|
expect(seen.has(k)).toBe(false);
|
|
seen.add(k);
|
|
}
|
|
});
|
|
|
|
test('searchTeams matches name substring, mascot prefix, and abbr prefix', () => {
|
|
expect(searchTeams('yank')[0].name).toBe('New York Yankees');
|
|
expect(searchTeams('NYY')[0].abbr).toBe('NYY');
|
|
expect(searchTeams('tigers')[0].name).toBe('Detroit Tigers');
|
|
expect(searchTeams('q')).toEqual([]); // under 2 chars → nothing
|
|
expect(searchTeams('zzzz')).toEqual([]);
|
|
});
|
|
|
|
test('teamHref → the S51 Team Hub route', () => {
|
|
expect(teamHref({ abbr: 'DET', sport: 'mlb' })).toBe('/team/DET?sport=mlb');
|
|
});
|
|
});
|
|
|
|
describe('SearchModal — grouped results, keyboard nav, canonical links', () => {
|
|
const src = read('components/vyndr/SearchModal.tsx');
|
|
|
|
test('players come from /api/players/search per sport (the canonical resolver)', () => {
|
|
expect(src).toContain('/api/players/search?sport=');
|
|
expect(src).toContain("['MLB', 'NBA', 'WNBA']");
|
|
});
|
|
test('grouped PLAYERS / TEAMS with arrow-key nav and Enter-to-open', () => {
|
|
expect(src).toContain('"PLAYERS"');
|
|
expect(src).toContain('"TEAMS"');
|
|
expect(src).toContain("e.key === 'ArrowDown'");
|
|
expect(src).toContain("e.key === 'ArrowUp'");
|
|
expect(src).toContain("e.key === 'Enter'");
|
|
expect(src).toContain("e.key === 'Escape'");
|
|
});
|
|
test('destinations are the canonical builders (playerHref / teamHref)', () => {
|
|
expect(src).toContain("from '@/lib/playerHref'");
|
|
expect(src).toContain('playerHref(name, res.sport)');
|
|
expect(src).toContain('teamHref(t)');
|
|
});
|
|
test('debounced + stale-response guarded (never a flood, never a regression paint)', () => {
|
|
expect(src).toContain('250');
|
|
expect(src).toContain('seqRef');
|
|
});
|
|
});
|
|
|
|
describe('entry points — ⌘K, Nav icon, mobile More sheet', () => {
|
|
test('GlobalHosts registers window.__search and the ⌘K/Ctrl-K shortcut', () => {
|
|
const src = read('components/vyndr/GlobalHosts.tsx');
|
|
expect(src).toContain('window.__search');
|
|
expect(src).toContain('e.metaKey || e.ctrlKey');
|
|
expect(src).toContain("=== 'k'");
|
|
expect(src).toContain('<SearchModal');
|
|
expect(src).toContain("removeEventListener('keydown'");
|
|
});
|
|
|
|
test('Nav keeps a search icon button (the mobile/mouse path)', () => {
|
|
const src = read('components/Nav.tsx');
|
|
expect(src).toContain('window.__search && window.__search()');
|
|
expect(src).toContain('aria-label="Search players and teams (⌘K)"');
|
|
});
|
|
|
|
test('BottomTabBar: Search is the FIRST item in the More sheet; Explore tab stays', () => {
|
|
const src = read('components/BottomTabBar.tsx');
|
|
const sheetSearch = src.indexOf('window.__search) window.__search()');
|
|
const items = src.indexOf('{MORE_ITEMS.map((it) =>');
|
|
expect(sheetSearch).toBeGreaterThan(-1);
|
|
expect(items).toBeGreaterThan(sheetSearch); // Search renders before the item list
|
|
expect(src).toContain("{ id: 'explore', label: 'Explore', href: '/explore'"); // tab NOT replaced
|
|
});
|
|
});
|