Files
electric-cherry/tests/unit/ui-language.test.ts
T

167 lines
5.9 KiB
TypeScript

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>/);
});