147 lines
6.5 KiB
TypeScript
147 lines
6.5 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import {
|
|
readMatchQuery,
|
|
matchHistoryHref,
|
|
matchApiParams
|
|
} from '../../src/lib/match-history-query.js';
|
|
let fixture: Fixture;
|
|
const now = Date.parse('2026-10-06T20:00:00Z');
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([]);
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const handle = db();
|
|
const match =
|
|
handle.prepare(`INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, ended_at, last_received_at, status, start_observed, highest_round, last_event, last_fingerprint)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'game_end', 'test')`);
|
|
const player = handle.prepare(
|
|
`INSERT INTO leaderboard_match_players (match_id, guid, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?)`
|
|
);
|
|
for (let id = 1; id <= 40; id++) {
|
|
const start = new Date(now - id * 86400000).toISOString();
|
|
const end = new Date(now - id * 86400000 + id * 60000).toISOString();
|
|
const game = id % 2 ? 't5' : 't6';
|
|
const name = game === 't5' ? 'Kino Der Toten' : 'Town Survival';
|
|
const map = game === 't5' ? 'theater' : 'town';
|
|
match.run(
|
|
id,
|
|
`${game}_zombie_${map}_1`,
|
|
game,
|
|
map,
|
|
`${game}:${map}`,
|
|
name,
|
|
start,
|
|
end,
|
|
end,
|
|
id === 2 ? 'active' : id === 4 ? 'interrupted' : 'completed',
|
|
id === 3 ? 0 : 1,
|
|
id + 1
|
|
);
|
|
for (let n = 0; n < id % 5; n++) player.run(id, String(n), start, end);
|
|
}
|
|
handle
|
|
.prepare('UPDATE leaderboard_matches SET server_deleted_at = ? WHERE id = 1')
|
|
.run(new Date(now).toISOString());
|
|
});
|
|
afterEach(async () => fixture.cleanup());
|
|
const query = (value = '') => readMatchQuery(new URLSearchParams(value));
|
|
it('sorts the whole history before paging, keeps ties deterministic, and clamps empty pages', async () => {
|
|
const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js');
|
|
const first = serverMatchHistory(undefined, 25, query());
|
|
const second = serverMatchHistory(undefined, 25, query('page=2'));
|
|
expect(first.total).toBe(40);
|
|
expect(first.matches.map((m) => m.id)).toEqual(Array.from({ length: 25 }, (_, i) => i + 1));
|
|
expect(second.matches.map((m) => m.id)).toEqual(Array.from({ length: 15 }, (_, i) => i + 26));
|
|
expect(serverMatchHistory(undefined, 25, query('page=999')).page).toBe(2);
|
|
expect(serverMatchHistory(undefined, 25, query('sort=round')).matches[0].id).toBe(40);
|
|
expect(
|
|
serverMatchHistory(undefined, 25, query('sort=players'))
|
|
.matches.slice(0, 3)
|
|
.map((m) => m.id)
|
|
).toEqual([39, 34, 29]);
|
|
});
|
|
it('combines game, map, readable instance search, status and date filters before counting', async () => {
|
|
const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js');
|
|
const data = serverMatchHistory(
|
|
undefined,
|
|
25,
|
|
query('game=t6&map=t6%3Atown&q=Town+Lobby+1&status=completed&days=7'),
|
|
now
|
|
);
|
|
expect(data.total).toBe(1);
|
|
expect(data.matches.map((m) => m.id)).toEqual([6]);
|
|
expect(data.maps).toEqual([{ key: 't6:town', name: 'Town Survival', game: 't6' }]);
|
|
expect(serverMatchHistory(undefined, 25, query('q=%25')).total).toBe(0);
|
|
expect(serverMatchHistory('t5_zombie_theater_1', 25, query()).total).toBe(20);
|
|
const empty = serverMatchHistory(undefined, 25, query('q=missing&page=5'));
|
|
expect(empty).toMatchObject({ total: 0, matches: [], page: 1 });
|
|
});
|
|
it('keeps unknown durations last in either direction and retains historical and unfinished semantics', async () => {
|
|
const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js');
|
|
for (const direction of ['asc', 'desc']) {
|
|
const data = serverMatchHistory(
|
|
undefined,
|
|
100,
|
|
query(`sort=duration&direction=${direction}`)
|
|
);
|
|
expect(data.matches.slice(-3).map((m) => m.durationMs)).toEqual([null, null, null]);
|
|
}
|
|
const data = serverMatchHistory();
|
|
expect(data.matches[0].server_deleted_at).toBeTruthy();
|
|
expect(data.matches[1]).toMatchObject({
|
|
status: 'active',
|
|
lastReportedAt: '2026-10-04T20:02:00.000Z',
|
|
observedRound: 3,
|
|
finalRound: null,
|
|
durationMs: null
|
|
});
|
|
expect(data.matches[3]).toMatchObject({
|
|
status: 'interrupted',
|
|
observedRound: 5,
|
|
finalRound: null,
|
|
durationMs: null
|
|
});
|
|
expect(data.matches.find((m) => m.id === 5)?.uniquePlayers).toBe(0);
|
|
});
|
|
it('validates both API routes and loads the same filtered page on a direct page visit', async () => {
|
|
const { GET } = await import('../../src/routes/api/matches/+server.js');
|
|
const { GET: instanceGET } =
|
|
await import('../../src/routes/api/servers/[name]/matches/+server.js');
|
|
for (const value of [
|
|
'page=-1',
|
|
'page=1.5',
|
|
'sort=sql',
|
|
'direction=sideways',
|
|
'game=t7',
|
|
'days=3',
|
|
'status=deleted',
|
|
'limit=0'
|
|
]) {
|
|
const url = new URL(`http://localhost/api/matches?${value}`);
|
|
expect(GET({ url }).status).toBe(400);
|
|
expect(instanceGET({ url, params: { name: 'anything' } }).status).toBe(400);
|
|
}
|
|
const { load } = await import('../../src/routes/matches/+page.server.js');
|
|
const url = new URL('http://localhost/matches?matchPage=2&matchSort=round');
|
|
const loaded = load({ url }).history;
|
|
const body = await GET({
|
|
url: new URL(`http://localhost/api/matches?${matchApiParams(url)}`)
|
|
}).json();
|
|
expect(loaded).toEqual(body);
|
|
expect(body.matches[0].id).toBe(15);
|
|
});
|
|
it('keeps return context and sorting while resetting page for a filter change', () => {
|
|
const url = new URL(
|
|
'http://localhost/servers/t5_zombie_theater_1?tab=matches&returnTo=%2Fhosts&matchSort=duration&matchDirection=asc&matchPage=3&matches=100'
|
|
);
|
|
const href = matchHistoryHref(url, { status: 'completed' });
|
|
expect(href).toContain('tab=matches');
|
|
expect(href).toContain('returnTo=%2Fhosts');
|
|
expect(href).toContain('matchSort=duration');
|
|
expect(href).toContain('matchDirection=asc');
|
|
expect(href).not.toContain('matchPage');
|
|
expect(href).not.toContain('matches=100');
|
|
expect(matchHistoryHref(url, { page: '4' })).toContain('matchPage=4');
|
|
});
|