import { beforeEach, afterEach, expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { writable, type Writable } from 'svelte/store'; import { page } from '$app/stores'; import MatchHistory from '../../src/lib/ui/components/MatchHistory.svelte'; import MatchHistoryPage from '../../src/routes/matches/+page.svelte'; import { matchStartedLabel, matchInstanceLabel } from '../../src/lib/ui/match-history.js'; import type { MatchHistoryResponse, ServerMatch } from '../../src/lib/match-history.js'; vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/matches') }) })); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); const match: ServerMatch = { id: 1, server: 't6_zombie_town_1', game: 't6', map: 'Town Survival', observedRound: 20, finalRound: 20, status: 'completed', reporterMatchKey: null, missingReports: null, startedAt: '2026-10-06T17:26:00Z', lastReportedAt: '2026-10-06T19:59:59Z', endedAt: '2026-10-06T18:04:00Z', durationMs: 2280000, startPlayers: 2, uniquePlayers: 4, endPlayers: 2 }; const report = (): MatchHistoryResponse => ({ matches: [match], total: 40, page: 1, limit: 25, maps: [{ key: 't6:town', game: 't6', name: 'Town Survival' }] }); function scope(search = '') { (page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost/matches${search}`) }); } beforeEach(() => { scope(); vi.useFakeTimers(); vi.setSystemTime(new Date('2026-10-06T20:00:00Z')); }); afterEach(() => vi.useRealTimers()); it.each([ ['', '0', 'All time'], ['0', '0', 'All time'], ['7', '7', 'Last 7 days'], ['30', '30', 'Last 30 days'], ['90', '90', 'Last 90 days'] ])( 'selects the current date range for matchDays=%s on desktop and mobile', (days, value, label) => { scope(days ? `?matchDays=${days}` : ''); const body = render(MatchHistory, { props: { initial: report() } }).body; for (const suffix of ['', '-mobile']) { const select = body.match( new RegExp(`]*id="match-days${suffix}"[^>]*>([\\s\\S]*?)`) )?.[1]; expect(select).toBeDefined(); expect(select).toContain(``); expect(select?.match(/\bselected=""/g)).toHaveLength(1); } } ); it('renders one page heading, six columns, a combined identity, and compact dates', () => { const body = render(MatchHistoryPage, { props: { data: { history: report(), user: null, setupRequired: false } } }).body; expect(body.match(/]/g)).toHaveLength(1); expect(body.match(/scope="col"/g)).toHaveLength(6); expect(body).toContain('2 h ago'); expect(body).toContain('Lobby 1 · Black Ops II'); expect(body).not.toContain('>Server'); expect(body).not.toContain('Show 25 more'); expect(body).toContain('matchPage=2'); }); it('preserves filters and page in match links and avoids linking deleted instances', () => { scope('?matchGame=t6&matchPage=2&matchSort=round&matchStatus=completed'); const data = report(); data.page = 2; data.matches = [{ ...match, server_deleted_at: '2026-10-06' }]; const body = render(MatchHistory, { props: { initial: data } }).body; expect(body).toContain('/matches/1?returnTo=%2Fmatches'); expect(body).toContain('matchPage%3D2'); expect(body).toContain('matchSort%3Dround'); expect(body).toContain('Historical instance'); expect(body).not.toContain('href="/servers/'); expect(body).not.toContain('>Deleted<'); expect(body).toContain('26–26 of 40 matches'); expect(body).toContain('Filters · 2'); }); it('keeps mobile filters and sorting in one labelled sheet with distinct controls and one named match link', () => { const body = render(MatchHistory, { props: { initial: report() } }).body; expect(body).toContain('Match filters'); expect(body).toContain('id="match-map-mobile"'); expect(body).toContain('id="match-sort-mobile"'); expect(body).toContain('id="match-order-mobile"'); expect(body).toContain('Show results'); const rows = body.slice(body.indexOf('')); expect(rows.match(/ match[1]); expect(new Set(ids).size).toBe(ids.length); expect(body).toContain('Updates every 10 seconds'); }); it('links in-progress matches to their live server with the current filters and page', () => { scope('?matchGame=t6&matchPage=2&matchStatus=active'); const data = report(); data.matches = [ { ...match, server: 'Weekend lobby #1', status: 'active', endedAt: null, durationMs: null } ]; const body = render(MatchHistory, { props: { initial: data } }).body; const rows = body.slice(body.indexOf('')); expect(rows.match(/]*>([\\s\\S]*?)`) )?.[1]; expect(select).toContain( '' ); } const rows = body.slice(body.indexOf('')); expect(rows.match(/End report missing/g)).toHaveLength(2); expect(rows.match(/2 reports missing/g)).toHaveLength(2); expect(rows).toContain('Highest observed'); expect(rows).not.toContain(' { const data = report(); data.matches = [{ ...match, status: 'active', lastReportedAt, durationMs: null }]; const body = render(MatchHistory, { props: { initial: data } }).body.replaceAll( //g, '' ); const rows = body.slice(body.indexOf('')); expect(rows.match(/Not reporting/g)).toHaveLength(2); expect(rows.match(/Last report /g)).toHaveLength(2); expect(rows).not.toContain('In progress'); expect(rows).not.toContain('View live'); expect(rows).not.toContain('—'); if (lastReportedAt && lastReportedAt !== 'invalid') { expect(rows).toMatch(/Last report