Show recorded match score totals with legacy fallback (Refs #147)
CI / checks (push) Successful in 5m22s
CI / checks (push) Successful in 5m22s
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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('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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user