import { afterEach, describe, expect, it, vi } from 'vitest'; import { flushSync, mount, unmount } from 'svelte'; import { fromStore, writable } from 'svelte/store'; import MatchPage from '../../src/routes/matches/[id]/+page.svelte'; import type { MatchDetail, MatchPlayer } from '../../src/lib/match-history.js'; vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/matches/1'), data: { user: null } }) })); function player(overrides: Partial = {}): MatchPlayer { return { guid: '101', name: 'Cherry', hasStats: true, presentAtEnd: true, canViewProfile: true, firstSeenAt: '2026-10-07T00:00:00Z', lastSeenAt: '2026-10-07T00:01:00Z', highestRound: 4, roundsSurvived: 3, kills: 20, headshots: 5, revives: 1, downs: 2, bestScore: 2760, scoreTotal: 8470, ...overrides }; } function match(players: MatchPlayer[], id = 1): MatchDetail { return { id, server: 't5_zombie_theater_1', game: 't5', map: 'Kino Der Toten', observedRound: 4, finalRound: 4, status: 'completed', reporterMatchKey: null, missingReports: null, startObserved: true, startedAt: '2026-10-07T00:00:00Z', lastReportedAt: '2026-10-07T00:01:00Z', endedAt: '2026-10-07T00:01:00Z', durationMs: 60_000, startPlayers: players.length, uniquePlayers: players.length, endPlayers: players.filter((entry) => entry.presentAtEnd).length, players, chat: [] }; } let component: ReturnType | undefined; afterEach(async () => { if (component) await unmount(component); component = undefined; document.body.replaceChildren(); }); function show(initial: MatchDetail) { const data = writable({ match: initial }); const state = fromStore(data); component = mount(MatchPage, { target: document.body, props: { get data() { return state.current; } } }); flushSync(); return (next: MatchDetail) => { data.set({ match: next }); flushSync(); }; } function expectScores(label: string, values: string[]) { const headers = [...document.querySelectorAll('thead th')].map((cell) => cell.textContent); expect(headers).toEqual([ 'Player', 'Participation', 'Highest round', 'Kills', 'Headshots', 'Revives', 'Downs', 'Rounds survived', label, 'Last seen' ]); const scores = [...document.querySelectorAll('tbody tr')].map( (row) => row.children[8].textContent ); expect(scores).toEqual(values); expect(document.body.textContent).not.toContain('NaN'); } function expectExplanation(totals: boolean) { const intro = document.querySelector('.results-heading')!.textContent!.replace(/\s+/g, ' '); const about = document.querySelector('details.note')!.textContent!.replace(/\s+/g, ' '); if (totals) { expect(intro).toContain('Total score is the highest recorded cumulative points earned'); expect(about).toContain('Total score is the highest recorded cumulative points earned'); expect(about).toContain( 'Players absent from the final report retain their latest recorded total.' ); expect(about).toContain( 'A dash in Total score means no total was recorded for that player.' ); expect(document.body.textContent).not.toContain('Highest reported score'); expect(about).not.toContain('points balance'); } else { expect(intro).toContain('Highest reported score is the highest recorded points balance.'); expect(about).toContain( 'Highest reported score is a points balance, not total points earned' ); expect(document.body.textContent).not.toContain('Total score'); } } describe('match result score presentation', () => { it.each(['interrupted', 'end-report-missing', 'active'] as const)( 'describes %s archives as observed reports without final result claims', (status) => { const archived = match([player({ highestRound: 1, presentAtEnd: false })]); archived.status = status; archived.observedRound = 1; archived.finalRound = null; archived.endedAt = null; archived.endPlayers = null; archived.durationMs = null; show(archived); expect(document.body.textContent).toContain( status === 'interrupted' ? 'Interrupted' : status === 'end-report-missing' ? 'End report missing' : 'Not reporting' ); expect(document.body.textContent).toContain('Highest observed round'); expect(document.body.textContent).toContain('Last reported'); expect(document.querySelector('tbody')?.textContent).toContain('Recorded participant'); expect(document.body.textContent).not.toContain('At match end'); expect(document.body.textContent).not.toContain('Players at end'); expect(document.body.textContent).not.toContain('Final round'); } ); it('shows recorded totals instead of balances, including totals for departed players', () => { show(match([player(), player({ guid: '202', presentAtEnd: false, scoreTotal: 6500 })])); expectScores('Total score', ['8,470', '6,500']); expectExplanation(true); expect(document.querySelector('tbody')!.textContent).not.toContain('2,760'); expect(document.querySelector('tbody')!.textContent).toContain('Not at match end'); }); it('preserves balances when every total is null or missing', () => { show( match([ player({ scoreTotal: null }), player({ guid: '202', scoreTotal: undefined, bestScore: 500 }) ]) ); expectScores('Highest reported score', ['2,760', '500']); expectExplanation(false); }); it('treats zero as a recorded total', () => { show(match([player({ scoreTotal: 0 })])); expectScores('Total score', ['0']); expectExplanation(true); }); it('uses dashes for null and missing totals even when balances are known', () => { show( match([ player({ scoreTotal: null }), player({ guid: '202', scoreTotal: undefined }), player({ guid: '303' }) ]) ); expectScores('Total score', ['—', '—', '8,470']); expectExplanation(true); expect(document.querySelector('tbody')!.textContent).not.toContain('2,760'); }); it.each([null, 8470])( 'keeps participants without stats when the other total is %s', (scoreTotal) => { show( match([ player({ scoreTotal }), player({ guid: '202', hasStats: false, presentAtEnd: false, highestRound: null, roundsSurvived: null, kills: null, headshots: null, revives: null, downs: null, bestScore: null, scoreTotal: null }) ]) ); expectScores(scoreTotal === null ? 'Highest reported score' : 'Total score', [ scoreTotal === null ? '2,760' : '8,470', '—' ]); const row = document.querySelectorAll('tbody tr')[1]; expect(row.textContent).toContain('No stats recorded'); expect([...row.children].slice(2, 9).map((cell) => cell.textContent)).toEqual( Array(7).fill('—') ); } ); it('updates the same mounted page when navigating in both directions and refreshing data', () => { const legacy = match([player({ scoreTotal: null })]); const current = match([player()], 2); const update = show(legacy); expectScores('Highest reported score', ['2,760']); expectExplanation(false); update(current); expectScores('Total score', ['8,470']); expectExplanation(true); update(legacy); expectScores('Highest reported score', ['2,760']); expectExplanation(false); update({ ...legacy, players: [player({ scoreTotal: 0 })] }); expectScores('Total score', ['0']); expectExplanation(true); }); it('renders an empty match without a score table', () => { show(match([])); expect(document.body.textContent).toContain('No player results recorded'); expect(document.querySelector('table')).toBeNull(); }); });