/* Actual menu functions, deterministic local responses; no browser/network I/O. */ const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const assert = require('node:assert/strict'); const support = fs.readFileSync(path.join(__dirname, 'match_selection_frontend.cjs'), 'utf8'); const boundary = support.indexOf('const appNames='); assert.ok(boundary > 0, 'Menu harness boundary missing'); const sandbox = { require, __dirname, AbortController, URLSearchParams, performance }; vm.createContext(sandbox); vm.runInContext(support.slice(0, boundary) + '\nthis.rosterSupport={menuHarness,details,compact,defer,tick,copy};', sandbox); const { menuHarness, details, compact, defer, tick, copy } = sandbox.rosterSupport; function empty(id, { finished = false, loaded = false } = {}) { const value = details(id, 1, { finished }); value.game.home.players = []; value.game.away.players = []; value.game.referees = []; value.details.loaded = loaded; if (!loaded) value.details.synced_at = null; return value; } const cases = {}; cases.first_selection_unchanged_live_hydrates = async () => { const h = menuHarness({ initialId: '', finished: false, onRequest: url => { if (url.includes('/games/B/details?')) return empty('B'); if (url.includes('/games/B/details/sync')) return Promise.reject(Error('temporary details timeout')); if (url.includes('/games/B/live')) return { ...details('B', 2, { finished: false }), changed: false }; } }); h.state.selectedGame = null; h.state.selectedMatchDetails = null; h.state.games.items = [compact('B', { finished: false })]; await h.c.openGame('B'); await tick(); assert.equal(h.state.selectedGameId, 'B'); assert.equal(h.state.selectedGame.home.players.length, 0); await h.c.pollSelectedGame(); assert.equal(h.state.selectedGame.home.players[0]?.id, 'B-home-v2', 'First selection discarded a complete unchanged live roster'); assert.equal(h.data().hockey.away.players[0]?.id, 'B-away-v2'); assert.equal(h.events.at(-1).detail.game.external_id, 'B'); assert.equal(h.state.selectedMatchDetails.loaded, true); }; cases.finished_empty_loaded_cache_repairs_once = async () => { const h = menuHarness({ initialId: '', onRequest: url => { if (url.includes('/games/B/details?')) return empty('B', { finished: true, loaded: true }); if (url.includes('/games/B/details/sync')) return details('B', 2); } }); h.state.selectedGame = null; h.state.selectedMatchDetails = null; h.state.games.items = [compact('B')]; await h.c.openGame('B'); await tick(); assert.equal(h.state.selectedGame.home.players[0]?.id, 'B-home-v2', 'An empty finished cache was mistaken for a complete roster'); assert.equal(h.data().hockey.away.players[0]?.id, 'B-away-v2'); assert.equal(h.requests.filter(row => row.url.includes('/details/sync')).length, 1); assert.equal(h.timers.size, 0, 'A finished repaired match started endless polling'); }; cases.finished_complete_cache_remains_offline = async () => { const h = menuHarness({ initialId: '' }); h.state.selectedGame = null; h.state.selectedMatchDetails = null; await h.c.openGame('B'); await tick(); assert.equal(h.data().hockey.home.players[0]?.id, 'B-home-v1'); assert.equal(h.requests.filter(row => row.url.includes('/details/sync') || row.url.includes('/live')).length, 0); assert.equal(h.timers.size, 0); }; cases.unchanged_newer_details_preserve_new_score = async () => { const h = menuHarness({ finished: false, onRequest: url => { if (url.includes('/live')) { const value = details('A', 2, { finished: false }); value.game.home.score = 7; value.game.away.score = 6; return { ...value, changed: false }; } } }); await h.c.pollSelectedGame(); assert.equal(h.state.selectedGame.home.players[0]?.id, 'A-home-v2'); assert.equal(h.data().hockey.home.score, 7); assert.equal(h.data().hockey.away.score, 6); }; cases.unchanged_incomplete_live_preserves_loaded_roster = async () => { const h = menuHarness({ finished: false, onRequest: url => { if (url.includes('/live')) return { ...empty('A'), changed: false }; } }); await h.c.pollSelectedGame(); assert.equal(h.state.selectedGame.home.players[0]?.id, 'A-home-v1'); assert.equal(h.data().hockey.away.players[0]?.id, 'A-away-v1'); assert.equal(h.state.selectedMatchDetails.loaded, true); }; cases.restore_empty_finished_cache_repairs = async () => { const h = menuHarness({ onRequest: url => { if (url.includes('/games/A/details?')) return empty('A', { finished: true, loaded: true }); if (url.includes('/games/A/details/sync')) return details('A', 2); } }); h.state.selectedGame = null; h.state.selectedMatchDetails = null; await h.c.restoreSelection(); await tick(); assert.equal(h.data().hockey.home.players[0]?.id, 'A-home-v2'); assert.equal(h.requests.filter(row => row.url.includes('/details/sync')).length, 1); assert.equal(h.timers.size, 0); }; cases.late_unchanged_roster_cannot_cross_selection = async () => { const pending = defer(); const h = menuHarness({ finished: false, onRequest: url => url.includes('/live') ? pending.promise : undefined }); const work = h.c.pollSelectedGame(); h.c.stopSelectedGamePolling(); h.state.gameOpenGeneration += 1; h.state.selectedGameId = 'B'; h.state.selectedGame = details('B', 3).game; h.state.selectedMatchDetails = details('B', 3).details; const count = h.events.length; pending.resolve({ ...details('A', 4, { finished: false }), changed: false }); await work; assert.equal(h.state.selectedGame.home.players[0].id, 'B-home-v3'); assert.equal(h.events.length, count); }; (async () => { const name = process.argv[2]; assert.ok(cases[name], 'Unknown case ' + name); await cases[name](); console.log('PASS first roster selection: ' + name); })().catch(error => { console.error(error); process.exitCode = 1; });