223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
import { expect, it, vi } from 'vitest';
|
|
import { render } from 'svelte/server';
|
|
import { writable } from 'svelte/store';
|
|
import PlayerProfilePage from '../../src/routes/leaderboard/players/[guid]/+page.svelte';
|
|
import PlayerMapCards from '../../src/lib/ui/components/PlayerMapCards.svelte';
|
|
import type { PlayerMapRecord, PlayerProfile, RecordEvidence } from '../../src/lib/leaderboard.js';
|
|
|
|
vi.mock('$app/stores', () => ({
|
|
page: writable({
|
|
url: new URL('http://localhost/leaderboard/players/101?players=2'),
|
|
data: { user: null }
|
|
})
|
|
}));
|
|
vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: 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: true,
|
|
firstObservedRound: 0,
|
|
value: 22
|
|
};
|
|
const stats = {
|
|
highestRound: 22,
|
|
bestKills: 507,
|
|
bestScore: 5290,
|
|
kills: 722,
|
|
roundsSurvived: 41,
|
|
headshots: 10,
|
|
revives: 2,
|
|
downs: 3,
|
|
gamesPlayed: 9,
|
|
gamesCompleted: 8
|
|
};
|
|
const map: PlayerMapRecord = {
|
|
...stats,
|
|
game: 't5',
|
|
key: 't5:theater',
|
|
name: 'Kino Der Toten',
|
|
lastSeen: evidence.endedAt!,
|
|
ranks: { highestRound: 7, bestKills: 12, bestScore: 3 },
|
|
records: {
|
|
highestRound: evidence,
|
|
bestKills: { ...evidence, matchId: 72, value: 507 },
|
|
bestScore: { ...evidence, matchId: 73, value: 5290 }
|
|
}
|
|
};
|
|
|
|
function expectRankedRecords(body: string) {
|
|
for (const [label, rank] of [
|
|
['Best round', 7],
|
|
['Best kills', 12],
|
|
['Best score', 3]
|
|
]) {
|
|
expect(body).toContain(`aria-label="Kino Der Toten ${label} rank ${rank}"`);
|
|
expect(body).toContain(`Rank #${rank}`);
|
|
}
|
|
for (const id of [71, 72, 73]) expect(body).toContain(`/matches/${id}?returnTo=`);
|
|
for (const value of ['22', '507', '5,290']) expect(body).toContain(`>${value}</a>`);
|
|
}
|
|
|
|
it('shows each record rank and preserves match links in mobile map cards', () => {
|
|
expectRankedRecords(
|
|
render(PlayerMapCards, {
|
|
props: { maps: [map], returnTo: '/leaderboard/players/101?players=2' }
|
|
}).body
|
|
);
|
|
});
|
|
|
|
it('shows the same ranks in the desktop profile with lifetime map scope explained', () => {
|
|
const profile: PlayerProfile = {
|
|
guid: '101',
|
|
name: 'Cherry',
|
|
firstSeen: evidence.startedAt,
|
|
lastSeen: evidence.endedAt!,
|
|
stats,
|
|
records: map.records,
|
|
maps: [map],
|
|
matches: [],
|
|
matchesTotal: 0,
|
|
limit: 25,
|
|
offset: 0
|
|
};
|
|
const body = render(PlayerProfilePage, {
|
|
props: {
|
|
data: {
|
|
profile,
|
|
bank: { balance: 0, available: 0, pending: false, entries: [] },
|
|
returnTo: '/leaderboard',
|
|
scope: { game: '', map: '', players: '2', metric: 'kills', entry: null }
|
|
}
|
|
}
|
|
}).body;
|
|
const table = body.slice(body.indexOf('<table'), body.indexOf('</table>'));
|
|
expectRankedRecords(table);
|
|
expect(body.replace(/\s+/g, ' ')).toContain(
|
|
'Lifetime personal bests across all squad sizes, ranked against other players on each map.'
|
|
);
|
|
expect(body).toContain('Tied records share a rank.');
|
|
expect(body).not.toContain('No qualifying match history yet');
|
|
expect(body).toContain('Show all completed matches');
|
|
});
|
|
|
|
it('renders a known player without qualifying history with identity and unavailable personal bests', () => {
|
|
const profile: PlayerProfile = {
|
|
guid: '101',
|
|
name: 'New player',
|
|
firstSeen: evidence.startedAt,
|
|
lastSeen: evidence.startedAt,
|
|
stats: {
|
|
highestRound: 0,
|
|
bestKills: 0,
|
|
bestScore: 0,
|
|
kills: 0,
|
|
roundsSurvived: 0,
|
|
headshots: 0,
|
|
revives: 0,
|
|
downs: 0,
|
|
gamesPlayed: 0,
|
|
gamesCompleted: 0
|
|
},
|
|
records: null,
|
|
maps: [],
|
|
matches: [],
|
|
matchesTotal: 0,
|
|
limit: 25,
|
|
offset: 0
|
|
};
|
|
const body = render(PlayerProfilePage, {
|
|
props: {
|
|
data: {
|
|
profile,
|
|
bank: { balance: 0, available: 0, pending: false, entries: [] },
|
|
returnTo: '/leaderboard',
|
|
scope: { game: '', map: '', players: '', metric: 'highestRound', entry: null }
|
|
}
|
|
}
|
|
}).body;
|
|
const text = body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ');
|
|
expect(text).toContain('New player');
|
|
expect(text).toContain('No qualifying match history yet');
|
|
expect(text).toContain("Stats appear after a game's end report arrives");
|
|
expect(text).toContain('observed at round 2 or later');
|
|
expect(text).toContain('Lifetime best round —');
|
|
expect(text).toContain('Best game kills —');
|
|
expect(text).toContain('Highest reported score —');
|
|
expect(text).toContain('Lifetime games played 0');
|
|
expect(text).toContain('No map records yet');
|
|
expect(text).toContain('No qualifying matches recorded');
|
|
expect(text).toContain('First seen');
|
|
expect(text).toContain('Last seen');
|
|
expect(body).toContain('Player GUID');
|
|
expect(body).not.toContain('/matches/');
|
|
expect(body).not.toContain('Show all completed matches');
|
|
expect(body).not.toContain('<table');
|
|
expect(body).not.toContain('NaN');
|
|
});
|
|
|
|
it('shows the shared balance and ledger with retained and expired match references', () => {
|
|
const profile: PlayerProfile = {
|
|
guid: '101',
|
|
name: 'Cherry',
|
|
firstSeen: evidence.startedAt,
|
|
lastSeen: evidence.endedAt!,
|
|
stats,
|
|
records: map.records,
|
|
maps: [],
|
|
matches: [],
|
|
matchesTotal: 0,
|
|
limit: 25,
|
|
offset: 0
|
|
};
|
|
const entry = {
|
|
id: 1,
|
|
server: evidence.server,
|
|
amount: 2000,
|
|
balance: 2000,
|
|
createdAt: evidence.startedAt,
|
|
matchId: 71,
|
|
matchAvailable: 1
|
|
};
|
|
const body = render(PlayerProfilePage, {
|
|
props: {
|
|
data: {
|
|
profile,
|
|
bank: {
|
|
balance: 1500,
|
|
available: 1000,
|
|
pending: true,
|
|
entries: [
|
|
{
|
|
...entry,
|
|
id: 2,
|
|
amount: -500,
|
|
balance: 1500,
|
|
matchId: 72,
|
|
matchAvailable: 0
|
|
},
|
|
entry
|
|
]
|
|
},
|
|
returnTo: '/leaderboard',
|
|
scope: { game: '', map: '', metric: 'highestRound', entry: null }
|
|
}
|
|
}
|
|
}).body;
|
|
const text = body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ');
|
|
expect(text).toContain('1,500 points');
|
|
expect(text).toContain('shared across Black Ops I and II');
|
|
expect(text).toContain('Available: 1,000 points');
|
|
expect(text).toContain('+2,000');
|
|
expect(text).toContain('-500');
|
|
expect(text).toContain('#72 (history expired)');
|
|
expect(body).toContain('href="/matches/71"');
|
|
expect(body).not.toContain('href="/matches/72"');
|
|
});
|