// 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(' { 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 }); });