Files

96 lines
3.8 KiB
TypeScript

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<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
}
}));
vi.mock('$app/navigation', () => ({ goto: navigation.goto }));
vi.mock('../../src/lib/ui/poll.js', () => ({ pollVisible: () => () => {} }));
let component: ReturnType<typeof mount> | 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<HTMLInputElement>('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<HTMLInputElement>('input[type="checkbox"]')?.checked).toBe(true);
const links = [...document.querySelectorAll<HTMLAnchorElement>('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<HTMLAnchorElement>('tbody a.match-link')];
expect(links).toHaveLength(1);
expect(new URL(links[0].href).searchParams.has('matchArchive')).toBe(false);
});