import { afterEach, expect, it, vi } from 'vitest'; import { flushSync, mount, unmount } from 'svelte'; import { writable } from 'svelte/store'; import MatchHistory from '../../src/lib/ui/components/MatchHistory.svelte'; import type { MatchHistoryResponse, ServerMatch } from '../../src/lib/match-history.js'; import type { UserRole } from '../../src/lib/auth.js'; const currentPage = writable({ url: new URL('http://localhost/matches'), data: { user: { role: 'moderator' as UserRole } } }); const navigation = vi.hoisted(() => ({ goto: vi.fn().mockResolvedValue(undefined) })); vi.mock('$app/stores', () => ({ page: { subscribe: (run: Parameters[0]) => currentPage.subscribe(run) } })); vi.mock('$app/navigation', () => ({ goto: navigation.goto })); vi.mock('../../src/lib/ui/poll.js', () => ({ pollVisible: () => () => {} })); let component: ReturnType | undefined; afterEach(async () => { if (component) await unmount(component); component = undefined; document.body.replaceChildren(); navigation.goto.mockClear(); }); const statuses: ServerMatch['status'][] = [ 'completed', 'interrupted', 'end-report-missing', 'active' ]; const report: MatchHistoryResponse = { total: 4, page: 1, limit: 25, maps: [], matches: statuses.map((status, index) => ({ id: index + 1, game: 't5', server: 't5_zombie_theater_1', map: 'Kino Der Toten', observedRound: 1, finalRound: status === 'completed' ? 1 : null, status, reporterMatchKey: null, missingReports: null, startedAt: '2026-10-17T10:00:00Z', lastReportedAt: '2026-10-17T10:01:00Z', endedAt: status === 'completed' ? '2026-10-17T10:01:00Z' : null, durationMs: null, startPlayers: 1, uniquePlayers: 1, endPlayers: status === 'completed' ? 1 : null })) }; function show(search = '', role: UserRole = 'moderator') { currentPage.set({ url: new URL(`http://localhost/matches${search}`), data: { user: { role } } }); component = mount(MatchHistory, { target: document.body, props: { initial: report } }); flushSync(); } it('enables the moderation archive through a namespaced URL while preserving filters', async () => { show('?matchGame=t5&matchPage=2'); document.querySelector('input[type="checkbox"]')!.click(); await vi.waitFor(() => expect(navigation.goto).toHaveBeenCalledOnce()); expect(navigation.goto.mock.calls[0][0]).toBe('/matches?matchGame=t5&matchArchive=1'); }); it('links round-one and unfinished archive rows to retained match detail with return context', () => { show('?matchArchive=1&matchGame=t5'); expect(document.querySelector('input[type="checkbox"]')?.checked).toBe(true); const links = [...document.querySelectorAll('tbody a.match-link')]; expect(links).toHaveLength(4); for (const [index, link] of links.entries()) { const url = new URL(link.href); expect(url.pathname).toBe(`/matches/${index + 1}`); expect(url.searchParams.get('matchArchive')).toBe('1'); expect(url.searchParams.get('returnTo')).toBe('/matches?matchArchive=1&matchGame=t5'); } expect(document.body.textContent).toContain('Highest observed'); expect(document.body.textContent).toContain('Last report'); }); it('keeps ordinary history navigation and hides archive controls from viewers', () => { show('', 'viewer'); expect(document.querySelector('input[type="checkbox"]')).toBeNull(); const links = [...document.querySelectorAll('tbody a.match-link')]; expect(links).toHaveLength(1); expect(new URL(links[0].href).searchParams.has('matchArchive')).toBe(false); });