Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+117 -25
View File
@@ -3,40 +3,118 @@ import { render } from 'svelte/server';
import { writable, type Writable } from 'svelte/store';
import { page } from '$app/stores';
import Leaderboard from '../../src/routes/leaderboard/+page.svelte';
import { createLeaderboardUpdates, leaderboardPageSize } from '../../src/lib/leaderboard-updates.js';
import type { LeaderboardEntry, LeaderboardResponse, RecordEvidence } from '../../src/lib/leaderboard.js';
import {
createLeaderboardUpdates,
leaderboardPageSize
} from '../../src/lib/leaderboard-updates.js';
import type {
LeaderboardEntry,
LeaderboardResponse,
RecordEvidence
} from '../../src/lib/leaderboard.js';
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/leaderboard') }), navigating: writable(null) }));
vi.mock('$app/stores', () => ({
page: writable({ url: new URL('http://localhost/leaderboard') }),
navigating: writable(null)
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
const evidence: RecordEvidence = { matchId: 71, game: 't5', mapKey: 't5:theater', mapName: 'Kino Der Toten', server: 't5_zombie_theater_1', startedAt: '2026-10-06T12:00:00Z', endedAt: '2026-10-06T13:00:00Z', startObserved: true, presentAtStart: false, firstObservedRound: 8, value: 49 };
const entry: LeaderboardEntry = { rank: 27, guid: '12345', name: 'Cherry', highestRound: 49, kills: 1000, roundsSurvived: 60, headshots: 100, revives: 3, downs: 5, gamesPlayed: 6, gamesCompleted: 5, bestKills: 500, bestScore: 4000, lastSeen: evidence.endedAt!, records: { highestRound: evidence, bestKills: evidence, bestScore: evidence }, headshotRate: 0.1, killsPerDown: 200, averageKills: 1000 / 6, averageRounds: 10, completionRate: 5 / 6 };
const evidence: RecordEvidence = {
matchId: 71,
game: 't5',
mapKey: 't5:theater',
mapName: 'Kino Der Toten',
server: 't5_zombie_theater_1',
startedAt: '2026-10-06T12:00:00Z',
endedAt: '2026-10-06T13:00:00Z',
startObserved: true,
presentAtStart: false,
firstObservedRound: 8,
value: 49
};
const entry: LeaderboardEntry = {
rank: 27,
guid: '12345',
name: 'Cherry',
highestRound: 49,
kills: 1000,
roundsSurvived: 60,
headshots: 100,
revives: 3,
downs: 5,
gamesPlayed: 6,
gamesCompleted: 5,
bestKills: 500,
bestScore: 4000,
lastSeen: evidence.endedAt!,
records: { highestRound: evidence, bestKills: evidence, bestScore: evidence },
headshotRate: 0.1,
killsPerDown: 200,
averageKills: 1000 / 6,
averageRounds: 10,
completionRate: 5 / 6
};
function report(): LeaderboardResponse {
return { entries: [structuredClone(entry)], maps: [{ key: 't5:theater', game: 't5', map: 'theater', name: 'Kino Der Toten' }], total: 60, limit: 25, offset: 25, metric: 'highestRound', summary: { players: 473, matches: 411, kills: 112899, roundsSurvived: 7062, highestRound: 49, firstTrackedAt: evidence.startedAt, updatedAt: evidence.endedAt } };
return {
entries: [structuredClone(entry)],
maps: [{ key: 't5:theater', game: 't5', map: 'theater', name: 'Kino Der Toten' }],
total: 60,
limit: 25,
offset: 25,
metric: 'highestRound',
summary: {
players: 473,
matches: 411,
kills: 112899,
roundsSurvived: 7062,
highestRound: 49,
firstTrackedAt: evidence.startedAt,
updatedAt: evidence.endedAt
}
};
}
function scope(search = '') {
(page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost/leaderboard${search}`) });
(page as unknown as Writable<{ url: URL }>).set({
url: new URL(`http://localhost/leaderboard${search}`)
});
}
function html(data = report()) {
return render(Leaderboard, {
props: { data: { leaderboard: data, user: null, setupRequired: false } }
}).body;
}
function html(data = report()) { return render(Leaderboard, { props: { data: { leaderboard: data, user: null, setupRequired: false } } }).body; }
beforeEach(() => scope('?map='));
it('shows compact totals and core stat columns, preserving ranks without repeated row disclosures', () => {
const body = html();
const headers = body.slice(body.indexOf('<thead'), body.indexOf('</thead>'));
expect(headers.match(/scope="col"/g)).toHaveLength(6);
expect(headers).toContain('Highest round'); expect(headers).toContain('Total kills'); expect(headers).toContain('Games played');
expect(headers).toContain('Highest round');
expect(headers).toContain('Total kills');
expect(headers).toContain('Games played');
expect(headers).toContain('Total rounds');
for (const label of ['Headshots', 'Revives', 'Downs', 'Games completed', 'Best game kills', 'Highest reported score']) expect(body).toContain(label);
for (const label of [
'Headshots',
'Revives',
'Downs',
'Games completed',
'Best game kills',
'Highest reported score'
])
expect(body).toContain(label);
expect(body).toContain('Show all stats');
expect(body).toMatch(/<span[^>]*>27<\/span>/);
expect(body).toContain('26–26 of 60 players');
expect(body.replace(/\s+/g, ' ')).toContain('26–26 of 60 players');
const overview = body.slice(body.indexOf('class="overview'), body.indexOf('class="rankings'));
expect(overview).toContain('All-time stats for All games &amp; maps');
for (const value of ['49', '112,899', '7,062', '411', '473']) expect(overview).toMatch(new RegExp(`<strong[^>]*>${value}</strong>`));
for (const value of ['49', '112,899', '7,062', '411', '473'])
expect(overview).toMatch(new RegExp(`<strong[^>]*>${value}</strong>`));
expect(overview).toContain('Fleet record');
expect(overview).not.toContain('<details');
expect(body).not.toMatch(/<details[^>]*open/);
expect(body).not.toContain('Copy GUID'); expect(body).not.toContain('Every round counts');
expect(body).not.toContain('Record details'); expect(body).not.toContain('Hide details');
expect(body).not.toContain('Copy GUID');
expect(body).not.toContain('Every round counts');
expect(body).not.toContain('Record details');
expect(body).not.toContain('Hide details');
const rows = body.slice(body.indexOf('<tbody'), body.indexOf('</tbody>'));
expect(rows.match(/<a\b/g)).toHaveLength(2);
expect(rows).not.toContain('<details');
@@ -47,16 +125,19 @@ it('shows compact totals and core stat columns, preserving ranks without repeate
it('deduplicates the selected metric and preserves scope and pagination on player and match links', () => {
scope('?game=t5&map=t5%3Atheater&metric=kills&q=Cherry&offset=25');
const data = report(); data.metric = 'kills';
const data = report();
data.metric = 'kills';
const body = html(data);
const headers = body.slice(body.indexOf('<thead'), body.indexOf('</thead>'));
expect(headers.match(/scope="col"/g)).toHaveLength(5);
expect(body).toContain('offset%3D25'); expect(body).toContain('q%3DCherry');
expect(body).toContain('offset%3D25');
expect(body).toContain('q%3DCherry');
scope('?game=t5&map=t5%3Atheater&metric=highestRound&q=Cherry&offset=25');
const recordBody = html();
expect(recordBody).toContain('/matches/71?returnTo=%2Fleaderboard');
expect(recordBody).toContain('map%3Dt5%253Atheater');
expect(recordBody).toContain('Selection record'); expect(recordBody).not.toContain('Fleet record');
expect(recordBody).toContain('Selection record');
expect(recordBody).not.toContain('Fleet record');
});
it('provides separate mobile filter controls and preserves explicit all-map and squad context', () => {
@@ -66,39 +147,50 @@ it('provides separate mobile filter controls and preserves explicit all-map and
expect(body).toContain('id="leaderboard-players-mobile"');
expect(body).toContain('value="2" selected=""');
expect(body).toContain('All squad sizes');
expect(body).toContain('Solo'); expect(body).toContain('Duo'); expect(body).toContain('Trio'); expect(body).toContain('Four players');
expect(body).toContain('unknown starting counts appear only under All squad sizes');
expect(body).toContain('Solo');
expect(body).toContain('Duo');
expect(body).toContain('Trio');
expect(body).toContain('Four players');
expect(body.replace(/\s+/g, ' ')).toContain(
'unknown starting counts appear only under All squad sizes'
);
expect(body).toContain('players%3D2');
expect(body).toContain('map%3D');
const ids = [...body.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
const ids = [...body.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]);
expect(new Set(ids).size).toBe(ids.length);
expect(body).not.toMatch(/>Refresh<\/span>/);
});
it('keeps qualification and denominator visible for efficiency rankings and missing values distinct', () => {
scope('?metric=headshotRate');
const data = report(); data.metric = 'headshotRate';
const data = report();
data.metric = 'headshotRate';
let body = html(data);
expect(body).toContain('At least 5 ended games and 100 kills within the selected game and map');
expect(body).toContain('100 headshots / 1,000 kills · 6 games');
expect(body).toContain('10%');
data.entries[0].headshotRate = null;
body = html(data); expect(body).toContain('>—');
body = html(data);
expect(body).toContain('>—');
});
it('holds changing ranks until explicitly applied, coalescing later polls even after focus leaves', () => {
const first = report();
const updates = createLeaderboardUpdates('all', first);
const second = structuredClone(first); second.entries[0].rank = 28;
const second = structuredClone(first);
second.entries[0].rank = 28;
expect(updates.receive('all', second, true)).toEqual({ current: first, pending: second });
const third = structuredClone(second); third.entries[0].rank = 30;
const third = structuredClone(second);
third.entries[0].rank = 30;
expect(updates.receive('all', third, false)).toEqual({ current: first, pending: third });
expect(updates.apply()).toEqual(third);
expect(updates.receive('all', third, true)?.pending).toBeNull();
});
it('ignores old scopes, clears a reverted pending update, and accepts unattended refreshes', () => {
const first = report(); const second = structuredClone(first); second.total = 100;
const first = report();
const second = structuredClone(first);
second.total = 100;
const updates = createLeaderboardUpdates('all', first);
updates.receive('all', second, true);
expect(updates.receive('all', structuredClone(first), true)?.pending).toBeNull();