123 lines
5.6 KiB
TypeScript
123 lines
5.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { render } from 'svelte/server';
|
|
import { get, writable } from 'svelte/store';
|
|
import type { ComponentProps } from 'svelte';
|
|
import ServerCard from '../../src/lib/ui/components/ServerCard.svelte';
|
|
import { telemetryByServer, uiNow } from '../../src/lib/ui/stores/servers.js';
|
|
import { gameEvent } from '../helpers/telemetry.js';
|
|
|
|
vi.mock('$app/stores', () => ({
|
|
page: writable({ url: new URL('http://localhost/?filter=occupied') })
|
|
}));
|
|
const name = 't5_zombie_theater_1';
|
|
const at = (age: number) => new Date(get(uiNow) - age).toISOString();
|
|
const telemetry = (round = 31, age = 1000) => ({
|
|
...gameEvent('heartbeat', round, 4, name),
|
|
receivedAt: at(age)
|
|
});
|
|
function html(props: Partial<ComponentProps<typeof ServerCard>> = {}) {
|
|
return render(ServerCard, {
|
|
props: {
|
|
name,
|
|
health: 'online',
|
|
players: 4,
|
|
capacity: 4,
|
|
telemetry: telemetry(),
|
|
gameStartedAt: at(6_000_000),
|
|
...props
|
|
}
|
|
}).body.replaceAll(/<!--.*?-->/g, '');
|
|
}
|
|
afterEach(() => telemetryByServer.set(new Map()));
|
|
|
|
describe('compact server cards', () => {
|
|
it('leads with identity and three labelled metrics, without healthy freshness or duplicate activity clocks', () => {
|
|
const body = html({
|
|
gametype: 'zom',
|
|
activeSince: at(6_120_000),
|
|
lastSeenAt: at(1000),
|
|
longestMatch: true
|
|
});
|
|
expect(body).toContain('Kino Der Toten');
|
|
expect(body).toContain('Lobby 1');
|
|
expect(body).toContain('BO1');
|
|
expect(body).toMatch(/<dt[^>]*>Players<\/dt>\s*<dd[^>]*>4<span[^>]*>\/4<\/span><\/dd>/);
|
|
expect(body).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>31<\/dd>/);
|
|
expect(body).toMatch(/<dt[^>]*>Match time<\/dt>\s*<dd[^>]*>1h 40m<\/dd>/);
|
|
expect(body).toContain('Longest match');
|
|
expect(body).not.toMatch(/Last seen|Last reported|Zombies Classic|>Active</);
|
|
});
|
|
|
|
it('defaults to four lobby slots when capacity is unavailable, while preserving a reported limit', () => {
|
|
for (const capacity of [null, undefined, 0, -1, 4.5, NaN, Infinity]) {
|
|
const body = html({ players: 2, capacity });
|
|
expect(body).toContain('title="4 player capacity"');
|
|
expect(body).toMatch(/>2<span[^>]*>\/4<\/span>/);
|
|
expect(body).not.toContain('/?');
|
|
}
|
|
expect(html({ players: 2, capacity: 8 })).toMatch(/>2<span[^>]*>\/8<\/span>/);
|
|
});
|
|
|
|
it('uses the newest fresh report while respecting explicit round data and match boundaries', () => {
|
|
telemetryByServer.set(new Map([[name, telemetry(35, 1000)]]));
|
|
expect(html({ telemetry: telemetry(31, 10_000) })).toMatch(
|
|
/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>35<\/dd>/
|
|
);
|
|
expect(html({ round: 40 })).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>40<\/dd>/);
|
|
expect(html({ round: null })).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>—<\/dd>/);
|
|
expect(html({ gameStartedAt: at(0) })).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>—<\/dd>/);
|
|
telemetryByServer.set(new Map());
|
|
expect(html({ telemetry: telemetry(31, 100_000) })).toMatch(
|
|
/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>—<\/dd>/
|
|
);
|
|
expect(html({ gameEndedAt: at(1000), round: 40 })).toContain('Match ended');
|
|
expect(html({ gameEndedAt: at(1000), round: 40 })).not.toMatch(/<dd[^>]*>40<\/dd>/);
|
|
});
|
|
|
|
it('gives the roster real column headers and formatted scores without repeating ping units', () => {
|
|
const body = html({
|
|
roster: [
|
|
{ num: 0, guid: 'one', name: 'Vexx', score: 99170, ping: 36 },
|
|
{ num: 1, guid: 'two', name: 'Connecting player', score: null, ping: 999 }
|
|
]
|
|
});
|
|
expect(body).toContain('aria-label="Current players"');
|
|
expect(body).toMatch(/<th[^>]*scope="col"[^>]*>Player<\/th>/);
|
|
expect(body).toContain('99,170');
|
|
expect(body).toContain('Connecting');
|
|
expect(body).not.toContain('36ms');
|
|
});
|
|
|
|
it('shows a known round without a match clock and hides match metrics for an empty lobby', () => {
|
|
const occupied = html({ gameStartedAt: null, round: 31 });
|
|
expect(occupied).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>31<\/dd>/);
|
|
expect(occupied).toMatch(/<dt[^>]*>Match time<\/dt>\s*<dd[^>]*>—<\/dd>/);
|
|
const empty = html({ players: 0, round: 31, compactEmpty: false });
|
|
expect(empty).toMatch(/<dt[^>]*>Round<\/dt>\s*<dd[^>]*>—<\/dd>/);
|
|
expect(empty).toMatch(/<dt[^>]*>Match time<\/dt>\s*<dd[^>]*>—<\/dd>/);
|
|
});
|
|
|
|
it('places Pin outside the only link and retains its pressed state and server name', () => {
|
|
const body = html({ pinned: true, onPin: () => {} });
|
|
expect(body).toMatch(/<article\b/);
|
|
expect(body.match(/<a\b/g)).toHaveLength(1);
|
|
expect(body.match(/<a\b[\s\S]*?<\/a>/)?.[0]).not.toContain('<button');
|
|
expect(body).toContain('aria-pressed="true"');
|
|
expect(body).toContain('aria-label="Unpin Kino Der Toten · Lobby 1"');
|
|
});
|
|
|
|
it('collapses empty healthy lobbies to readiness and empty duration', () => {
|
|
const body = html({
|
|
players: 0,
|
|
inactiveSince: at(9 * 60_000),
|
|
roster: [],
|
|
compactEmpty: true
|
|
});
|
|
expect(body).toContain('Ready');
|
|
expect(body).toContain('Empty for 9m 00s');
|
|
expect(body).not.toContain('<dl');
|
|
expect(body).not.toContain('<table');
|
|
expect(body).not.toContain('Player roster unavailable');
|
|
});
|
|
});
|