Files
electric-cherry/tests/client/ui-match-results.test.ts

246 lines
8.9 KiB
TypeScript

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> = {}): 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<typeof mount> | 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();
});
});