Files
electric-cherry/tests/unit/ui-leaderboard.test.ts
T

85 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, expect, it, vi } from 'vitest';
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';
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 };
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 } };
}
function scope(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; }
beforeEach(() => scope());
it('shows five columns, preserves recorded ranks, and collapses totals, identity and evidence', () => {
const body = html();
const headers = body.slice(body.indexOf('<thead'), body.indexOf('</thead>'));
expect(headers.match(/scope="col"/g)).toHaveLength(5);
expect(headers).toContain('Highest round'); expect(headers).toContain('Total kills'); expect(headers).toContain('Games played');
expect(headers).not.toContain('Headshots');
expect(body).toMatch(/<span[^>]*>27<\/span>/);
expect(body).toContain('26–26 of 60 players');
expect(body).toContain('473 players · 411 completed games · Highest round 49');
expect(body).not.toMatch(/<details[^>]*open/);
expect(body).not.toContain('Copy GUID'); expect(body).not.toContain('Every round counts');
expect(body).toContain('Record details'); expect(body).toContain('First observed at round 8');
expect(body).not.toContain('>Search</button>');
});
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 body = html(data);
const headers = body.slice(body.indexOf('<thead'), body.indexOf('</thead>'));
expect(headers.match(/scope="col"/g)).toHaveLength(4);
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('Scope totals'); expect(recordBody).not.toContain('Fleet totals');
});
it('keeps qualification and denominator visible for efficiency rankings and missing values distinct', () => {
scope('?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('>—');
});
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;
expect(updates.receive('all', second, true)).toEqual({ current: first, pending: second });
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 updates = createLeaderboardUpdates('all', first);
updates.receive('all', second, true);
expect(updates.receive('all', structuredClone(first), true)?.pending).toBeNull();
expect(updates.receive('all', second, false)?.current).toEqual(second);
updates.receive('all', first, true);
updates.reset('t5', first);
expect(updates.receive('all', second, false)).toBeNull();
expect(updates.apply()).toEqual(first);
expect(leaderboardPageSize).toBe(25);
});