import { expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { writable } from 'svelte/store'; import { plural, countLabel } from '../../src/lib/ui/plural.js'; import { gameTitle } from '../../src/lib/ui/server-labels.js'; import { playerCompletionLabel } from '../../src/lib/ui/player-match.js'; import { hostCalibrationHint } from '../../src/lib/ui/host-overview.js'; import { scopeLabel } from '../../src/lib/script-library.js'; import Setup from '../../src/routes/setup/+page.svelte'; import Hosts from '../../src/routes/hosts/+page.svelte'; import PlayerMatchCards from '../../src/lib/ui/components/PlayerMatchCards.svelte'; import GameFileHosts from '../../src/lib/ui/components/GameFileHosts.svelte'; import type { Host } from '../../src/lib/hosts.js'; import type { PlayerMatch } from '../../src/lib/leaderboard.js'; vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/hosts'), data: { user: { id: 'admin', role: 'admin' } } }) })); vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn(), afterNavigate: vi.fn(), invalidateAll: vi.fn() })); const now = Date.parse('2026-10-07T12:00:00Z'); const host: Host = { id: 'dallas', name: 'Dallas 1', hostname: 'dallas.example', mode: 'active', portMin: 28960, portMax: 28980, bindAddress: '0.0.0.0', publicAddress: '', minimumFreeBytes: 100, createdAt: new Date(now).toISOString(), lastHeartbeat: new Date(now).toISOString(), connected: false, connectionState: 'offline', readiness: 'ready', readinessReasons: [], inventory: {}, enrolled: true, credentialActive: true, decommission: null }; const match: PlayerMatch = { id: 17, game: 't5', server: 't5_zombie_theater_1', mapName: 'Kino Der Toten', startedAt: new Date(now).toISOString(), lastSeen: new Date(now).toISOString(), status: 'completed', completed: 1, highestRound: 30, roundsSurvived: 20, kills: 1200, headshots: 200, revives: 2, downs: 1, bestScore: 45000 }; it('pluralizes zero, one, multiple, fractional and irregular counts consistently', () => { expect(countLabel(0, 'server')).toBe('0 servers'); expect(countLabel(1, 'player')).toBe('1 player'); expect(countLabel(2000, 'host')).toBe('2,000 hosts'); expect(countLabel(1, 'match', 'matches')).toBe('1 match'); expect(countLabel(2, 'match', 'matches')).toBe('2 matches'); expect(plural(1, 'needs', 'need')).toBe('needs'); expect(plural(0, 'needs', 'need')).toBe('need'); expect(plural(1.5, 'hour')).toBe('hours'); }); it('recommends a sampling upgrade only for a known older version', () => { for (const version of ['1.0.0', '1.4.5', 'v1.4.0']) expect(hostCalibrationHint(version)).toContain('Update this host to agent 1.4.6'); for (const version of ['1.4.6', '1.7.0', '1.10.0', '2.0.0', 'v2.0.0+build', null, undefined, 'unknown', '1.4']) { expect(hostCalibrationHint(version)).toContain('still calibrating'); expect(hostCalibrationHint(version)).not.toContain('Update'); } }); it('describes per-player completion as final-report evidence without inventing unfinished results', () => { expect(playerCompletionLabel(match)).toBe('Present in final report'); expect(playerCompletionLabel({ ...match, completed: 0 })).toBe('Not present in final report'); expect(playerCompletionLabel({ ...match, status: 'active', completed: 0 })).toBe('Match in progress'); expect(playerCompletionLabel({ ...match, status: 'interrupted', completed: 0 })).toBe('Final report unavailable'); const body = render(PlayerMatchCards, { props: { matches: [match, { ...match, id: 18, completed: 0 }] } }).body; expect(body).toContain('Present in final report'); expect(body).toContain('Not present in final report'); expect(body).not.toContain('Completion credited'); expect(body).not.toContain('No completion credit'); }); it('uses product game names while preserving unsupported game identifiers', () => { expect(gameTitle('t5')).toBe('Black Ops I'); expect(gameTitle('T6')).toBe('Black Ops II'); expect(gameTitle('t5', true)).toBe('BO1'); expect(gameTitle('t6', true)).toBe('BO2'); expect(gameTitle('custom')).toBe('CUSTOM'); expect(gameTitle(null)).toBe('Unknown game'); expect(scopeLabel({ scope_type: 'game', scope_value: 't5' })).toBe('Black Ops I'); }); it('does not offer an existing-account sign-in path on first administrator setup', () => { const body = render(Setup).body; expect(body).toContain('Create the first administrator'); expect(body).not.toContain('Already have an account'); expect(body).not.toContain('href="/login"'); }); it('renders singular host attention and explicit whitespace before an offline suffix', () => { const body = render(Hosts, { props: { data: { hosts: [{ ...host, serverCount: 0, playerCount: 0, unreportedInstances: 0 }], serverNow: now, user: null, setupRequired: false } } }).body; expect(body).toContain('1 host'); expect(body).toContain('1 needs attention'); expect(body).not.toContain('1 need attention'); const files = render(GameFileHosts, { props: { hosts: [host], caches: [{ hostId: host.id, libraryVersionId: 'files', status: 'missing', bytesDownloaded: 0, bytesTotal: 0, manifestSha256: null, error: null, updatedAt: new Date(now).toISOString(), operationId: null }] } }).body; expect(files).toMatch(/Dallas 1<\/a>.*?> ยท Offline<\/span>/); });