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:
@@ -75,9 +75,20 @@ Completed matches link to their own results page at `/matches/[id]` from global
|
|||||||
Match History, server history, and player profiles. The page shows the recorded
|
Match History, server history, and player profiles. The page shows the recorded
|
||||||
map and server, final round, start/end times, duration when the start was observed,
|
map and server, final round, start/end times, duration when the start was observed,
|
||||||
player counts, and the full saved roster. Players missing from the final report
|
player counts, and the full saved roster. Players missing from the final report
|
||||||
retain their earlier counters; players with no stats display dashes. Counters and
|
retain their earlier counters; players with no stats display dashes. Counters are
|
||||||
score are the highest reported values within the match, not necessarily its final
|
the highest reported values within the match, not necessarily its final scoreboard.
|
||||||
scoreboard. Active, interrupted, and **End report missing** matches remain listed
|
|
||||||
|
Each match shows one score column. If any player has a recorded total (including
|
||||||
|
zero), **Total score** shows each player's highest recorded cumulative points
|
||||||
|
earned in that match. Players absent from the final report retain their latest
|
||||||
|
recorded total. A dash in this column means no total was recorded for that player;
|
||||||
|
their points balance is never substituted. Historical matches where all totals
|
||||||
|
are missing retain **Highest reported score**, the highest recorded points
|
||||||
|
balance, not total points earned or necessarily the final score. This choice is
|
||||||
|
based on recorded data, not the match date. It does not change leaderboard or
|
||||||
|
profile score rankings, which still use balances.
|
||||||
|
|
||||||
|
Active, interrupted, and **End report missing** matches remain listed
|
||||||
without results links. Matches can filter each status and show known gaps in the
|
without results links. Matches can filter each status and show known gaps in the
|
||||||
reporter's sequence; a delayed report can reduce that missing-report count.
|
reporter's sequence; a delayed report can reduce that missing-report count.
|
||||||
Match pages remain available after the original server is removed.
|
Match pages remain available after the original server is removed.
|
||||||
|
|||||||
Generated
+95
@@ -24,6 +24,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.23.1",
|
"eslint-plugin-svelte": "^3.23.1",
|
||||||
"globals": "^17.13.0",
|
"globals": "^17.13.0",
|
||||||
|
"happy-dom": "^21.0.1",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^3.9.9",
|
||||||
"prettier-plugin-svelte": "^3.5.2",
|
"prettier-plugin-svelte": "^3.5.2",
|
||||||
"svelte": "^5.1.0",
|
"svelte": "^5.1.0",
|
||||||
@@ -1531,6 +1532,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/whatwg-mimetype": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
|
||||||
|
"integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.71.1",
|
"version": "8.71.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz",
|
||||||
@@ -2079,6 +2097,19 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-image-size": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cacheable": {
|
"node_modules/cacheable": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
|
||||||
@@ -2271,6 +2302,19 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-errors": {
|
"node_modules/es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
@@ -2807,6 +2851,25 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/happy-dom": {
|
||||||
|
"version": "21.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-21.0.1.tgz",
|
||||||
|
"integrity": "sha512-pRBs/u9fhrncyByOl58DMex0R2uAj6ZPKWIjV5j5yNGZBqLw3P6BTRhlc0DD5b/Ht4/6JYTMuE2LJhUWRC6SGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": ">=20.0.0",
|
||||||
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"buffer-image-size": "^0.6.4",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"whatwg-mimetype": "^3.0.0",
|
||||||
|
"ws": "^8.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hashery": {
|
"node_modules/hashery": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
|
||||||
@@ -4328,6 +4391,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -4377,6 +4450,28 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
|
||||||
|
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.23.1",
|
"eslint-plugin-svelte": "^3.23.1",
|
||||||
"globals": "^17.13.0",
|
"globals": "^17.13.0",
|
||||||
|
"happy-dom": "^21.0.1",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^3.9.9",
|
||||||
"prettier-plugin-svelte": "^3.5.2",
|
"prettier-plugin-svelte": "^3.5.2",
|
||||||
"svelte": "^5.1.0",
|
"svelte": "^5.1.0",
|
||||||
|
|||||||
@@ -24,15 +24,19 @@
|
|||||||
: match.game.toUpperCase()
|
: match.game.toUpperCase()
|
||||||
);
|
);
|
||||||
const number = new Intl.NumberFormat('en-US');
|
const number = new Intl.NumberFormat('en-US');
|
||||||
const stats = [
|
const hasScoreTotals = $derived(match.players.some((player) => player.scoreTotal != null));
|
||||||
|
const stats = $derived([
|
||||||
{ key: 'highestRound', label: 'Highest round' },
|
{ key: 'highestRound', label: 'Highest round' },
|
||||||
{ key: 'kills', label: 'Kills' },
|
{ key: 'kills', label: 'Kills' },
|
||||||
{ key: 'headshots', label: 'Headshots' },
|
{ key: 'headshots', label: 'Headshots' },
|
||||||
{ key: 'revives', label: 'Revives' },
|
{ key: 'revives', label: 'Revives' },
|
||||||
{ key: 'downs', label: 'Downs' },
|
{ key: 'downs', label: 'Downs' },
|
||||||
{ key: 'roundsSurvived', label: 'Rounds survived' },
|
{ key: 'roundsSurvived', label: 'Rounds survived' },
|
||||||
{ key: 'bestScore', label: 'Highest reported score' }
|
{
|
||||||
] as const;
|
key: hasScoreTotals ? 'scoreTotal' : 'bestScore',
|
||||||
|
label: hasScoreTotals ? 'Total score' : 'Highest reported score'
|
||||||
|
}
|
||||||
|
] as const);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><title>{match.map} · Match #{match.id} · Electric Cherry</title></svelte:head>
|
<svelte:head><title>{match.map} · Match #{match.id} · Electric Cherry</title></svelte:head>
|
||||||
@@ -122,6 +126,11 @@
|
|||||||
<p>
|
<p>
|
||||||
Every recorded player, including those absent from the final report. Stats are the
|
Every recorded player, including those absent from the final report. Stats are the
|
||||||
highest reported counters for this match.
|
highest reported counters for this match.
|
||||||
|
{#if hasScoreTotals}
|
||||||
|
Total score is the highest recorded cumulative points earned in this match.
|
||||||
|
{:else}
|
||||||
|
Highest reported score is the highest recorded points balance.
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if match.players.length === 0}
|
{#if match.players.length === 0}
|
||||||
@@ -161,7 +170,7 @@
|
|||||||
>{#if !player.hasStats}<small>No stats recorded</small>{/if}</td
|
>{#if !player.hasStats}<small>No stats recorded</small>{/if}</td
|
||||||
>
|
>
|
||||||
{#each stats as stat}<td class:round={stat.key === 'highestRound'}
|
{#each stats as stat}<td class:round={stat.key === 'highestRound'}
|
||||||
>{player[stat.key] === null
|
>{player[stat.key] == null
|
||||||
? '—'
|
? '—'
|
||||||
: number.format(player[stat.key]!)}</td
|
: number.format(player[stat.key]!)}</td
|
||||||
>{/each}
|
>{/each}
|
||||||
@@ -178,11 +187,20 @@
|
|||||||
|
|
||||||
<details class="note">
|
<details class="note">
|
||||||
<summary>About these results</summary>
|
<summary>About these results</summary>
|
||||||
|
<p>Kills, headshots, revives, and downs retain each player's highest reported value.</p>
|
||||||
|
{#if hasScoreTotals}
|
||||||
<p>
|
<p>
|
||||||
Kills, headshots, revives, downs, and score retain each player's highest reported value.
|
Total score is the highest recorded cumulative points earned in this match. Players
|
||||||
Highest reported score is a points balance, not total points earned or necessarily the
|
absent from the final report retain their latest recorded total. A dash in Total
|
||||||
final score.
|
score means no total was recorded for that player.
|
||||||
</p>
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
Highest reported score is a points balance, not total points earned or necessarily
|
||||||
|
the final score. This match has no recorded totals, so each player's highest
|
||||||
|
reported balance is shown.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<p>
|
<p>
|
||||||
Highest round includes late joins. Rounds survived counts only observed round
|
Highest round includes late joins. Rounds survived counts only observed round
|
||||||
progression while the player was present. “At match end” means the player appeared in
|
progression while the player was present. “At match end” means the player appeared 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+11
-1
@@ -12,7 +12,17 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
name: 'application',
|
name: 'application',
|
||||||
include: ['tests/**/*.{test,spec}.{js,ts}'],
|
include: ['tests/**/*.{test,spec}.{js,ts}'],
|
||||||
exclude: [...configDefaults.exclude, migrationMemoryTest],
|
exclude: [...configDefaults.exclude, migrationMemoryTest, 'tests/client/**'],
|
||||||
|
sequence: { groupOrder: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
resolve: { conditions: ['browser'] },
|
||||||
|
test: {
|
||||||
|
name: 'client',
|
||||||
|
environment: 'happy-dom',
|
||||||
|
include: ['tests/client/**/*.{test,spec}.{js,ts}'],
|
||||||
sequence: { groupOrder: 0 }
|
sequence: { groupOrder: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user