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> = {}) { 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(/]*>Players<\/dt>\s*]*>4]*>\/4<\/span><\/dd>/); expect(body).toMatch(/]*>Round<\/dt>\s*]*>31<\/dd>/); expect(body).toMatch(/]*>Match time<\/dt>\s*]*>1h 40m<\/dd>/); expect(body).toContain('Longest match'); expect(body).not.toMatch(/Last seen|Last reported|Zombies Classic|>Active { 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]*>\/4<\/span>/); expect(body).not.toContain('/?'); } expect(html({ players: 2, capacity: 8 })).toMatch(/>2]*>\/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( /]*>Round<\/dt>\s*]*>35<\/dd>/ ); expect(html({ round: 40 })).toMatch(/]*>Round<\/dt>\s*]*>40<\/dd>/); expect(html({ round: null })).toMatch(/]*>Round<\/dt>\s*]*>โ€”<\/dd>/); expect(html({ gameStartedAt: at(0) })).toMatch(/]*>Round<\/dt>\s*]*>โ€”<\/dd>/); telemetryByServer.set(new Map()); expect(html({ telemetry: telemetry(31, 100_000) })).toMatch( /]*>Round<\/dt>\s*]*>โ€”<\/dd>/ ); expect(html({ gameEndedAt: at(1000), round: 40 })).toContain('Match ended'); expect(html({ gameEndedAt: at(1000), round: 40 })).not.toMatch(/]*>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(/]*scope="col"[^>]*>Player<\/th>/); expect(body).toContain('99,170'); expect(body).toContain('Connecting'); expect(body).not.toContain('36ms'); }); it('places Pin outside the only link and retains its pressed state and server name', () => { const body = html({ pinned: true, onPin: () => {} }); expect(body).toMatch(//)?.[0]).not.toContain(' { 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('