171 lines
6.8 KiB
TypeScript
171 lines
6.8 KiB
TypeScript
import { expect, it, vi } from 'vitest';
|
|
import { render } from 'svelte/server';
|
|
import { writable } from 'svelte/store';
|
|
import { get } from 'svelte/store';
|
|
import type { ComponentProps } from 'svelte';
|
|
import type { GameTelemetry } from '../../src/lib/telemetry.js';
|
|
import ServerPage from '../../src/routes/servers/[name]/+page.svelte';
|
|
import PlayerScoreboard from '../../src/lib/ui/components/PlayerScoreboard.svelte';
|
|
import ConfigInspector from '../../src/lib/ui/components/ConfigInspector.svelte';
|
|
import { uiNow } from '../../src/lib/ui/stores/servers.js';
|
|
import { gameEvent } from '../helpers/telemetry.js';
|
|
import { page } from '$app/stores';
|
|
|
|
vi.mock('$app/state', () => ({
|
|
page: { url: new URL('http://localhost/servers/t5_zombie_theater_1') }
|
|
}));
|
|
vi.mock('$app/stores', () => ({
|
|
page: writable({
|
|
params: { name: 't5_zombie_theater_1' },
|
|
url: new URL('http://localhost/servers/t5_zombie_theater_1')
|
|
})
|
|
}));
|
|
vi.mock('$app/navigation', () => ({
|
|
beforeNavigate: vi.fn(),
|
|
afterNavigate: vi.fn(),
|
|
goto: vi.fn()
|
|
}));
|
|
|
|
const report = (current = true): GameTelemetry => {
|
|
const receivedAt = new Date(get(uiNow) - (current ? 10_000 : 300_000)).toISOString();
|
|
const joined = new Date(Date.parse(receivedAt) - 60_000).toISOString();
|
|
return {
|
|
...gameEvent('heartbeat', 4, 1),
|
|
receivedAt,
|
|
timing: {
|
|
matchId: 1,
|
|
players: {
|
|
'12345678901234560': {
|
|
serverJoinedAt: joined,
|
|
matchJoinedAt: joined,
|
|
roundStarted: 1
|
|
}
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
it('keeps scoreboard columns focused and collapses player identifiers and timings', () => {
|
|
const body = render(PlayerScoreboard, {
|
|
props: { report: report(), matchElapsed: '4m 19s' }
|
|
}).body;
|
|
expect(body).toContain('Live scoreboard');
|
|
expect(body).toContain('Round 4');
|
|
expect(body).toContain('Match 4m 19s');
|
|
expect(body).toContain('aria-expanded="false"');
|
|
expect(body).toMatch(/<tr[^>]*hidden/);
|
|
expect(body).toContain('Time on instance: 1m 10s');
|
|
expect(body).toContain('aria-label="Copy GUID for Player 1"');
|
|
expect(body).not.toContain('>Time on server</th>');
|
|
expect(body).not.toContain('>Round started</th>');
|
|
expect(body).toContain('aria-sort="descending"');
|
|
expect(body).toContain('About these stats');
|
|
});
|
|
|
|
it('freezes historical timing and keeps unreported scores distinct from zero', () => {
|
|
const body = render(PlayerScoreboard, { props: { report: report(false) } }).body;
|
|
expect(body).toContain('Last reported scoreboard');
|
|
expect(body).toContain('in last report');
|
|
expect(body).toContain('Time on instance: 1m 00s');
|
|
expect(body).not.toContain('Time on instance: 6m');
|
|
const start = { ...gameEvent('game_start', 0, 1), receivedAt: report().receivedAt };
|
|
expect(render(PlayerScoreboard, { props: { report: start } }).body).toContain(
|
|
'aria-label="Not yet reported"'
|
|
);
|
|
});
|
|
|
|
it('omits the fixed instance selector while keeping promoted/live distinction', () => {
|
|
const body = render(ConfigInspector, { props: { fixedServer: 't5_zombie_theater_1' } }).body;
|
|
expect(body).not.toContain('<select');
|
|
expect(body).toContain('Promoted configuration');
|
|
expect(body).toContain('Live files and active betas may differ');
|
|
});
|
|
|
|
function pageBody(tab: string, role = 'admin') {
|
|
(page as unknown as ReturnType<typeof writable>).set({
|
|
data: { user: { role } },
|
|
params: { name: 't5_zombie_theater_1' },
|
|
url: new URL(
|
|
`http://localhost/servers/t5_zombie_theater_1?tab=${tab}&returnTo=%2F%3Ffilter%3Doccupied`
|
|
)
|
|
});
|
|
const telemetry = report();
|
|
const server = {
|
|
name: 't5_zombie_theater_1',
|
|
game: 't5',
|
|
map: 'zombie_theater',
|
|
last_map: 'zombie_theater',
|
|
health: 'online',
|
|
last_players: 1,
|
|
last_seen_at: telemetry.receivedAt,
|
|
last_error: null,
|
|
held: [],
|
|
telemetry,
|
|
status_source: 'telemetry',
|
|
host_id: 'host-1',
|
|
native_instance_id: 'instance-1',
|
|
host: { name: 'ec-1', connected: true, lastHeartbeat: telemetry.receivedAt },
|
|
replySnippet: 'legacy private reply 192.0.2.123:28960',
|
|
rcon_status: { last_seen_at: null, last_error: 'timeout: no reply' },
|
|
game_started_at: null,
|
|
game_ended_at: null,
|
|
active_since: null,
|
|
inactive_since: null
|
|
};
|
|
const data = {
|
|
serverSnapshot: {
|
|
servers: [server],
|
|
operations: [],
|
|
serverNow: get(uiNow),
|
|
integrations: {},
|
|
reservedNames: []
|
|
}
|
|
} as unknown as ComponentProps<typeof ServerPage>['data'];
|
|
return render(ServerPage, { props: { data } }).body;
|
|
}
|
|
it('keeps match content out of the shared header and preserves the fleet return filter', () => {
|
|
const body = pageBody('configuration');
|
|
expect(body).toContain('More actions');
|
|
expect(body).toContain('href="/?filter=occupied"');
|
|
expect(body).toContain('ec-1');
|
|
expect(body).not.toContain('Active for');
|
|
expect(body).not.toContain('Round 4');
|
|
expect(body).not.toContain('aria-label="Configuration sections"');
|
|
});
|
|
it('reports RCON failures independently of fresh telemetry and defers the request inspector', () => {
|
|
const body = pageBody('diagnostics');
|
|
expect(body).toContain('Game connection · RCON');
|
|
expect(body).toContain('Issue');
|
|
expect(body).toContain('Reporting');
|
|
expect(body).toContain('Player count source: game telemetry');
|
|
expect(body).toContain('Connection details');
|
|
expect(body).not.toContain('Telemetry API requests');
|
|
expect(body).not.toContain('>Healthy<');
|
|
expect(body).toContain('Test connection');
|
|
expect(body).not.toContain('legacy private reply');
|
|
});
|
|
|
|
it('keeps connection summaries visible to viewers without raw diagnostics or probe controls', () => {
|
|
const body = pageBody('diagnostics', 'viewer');
|
|
expect(body).toContain('Game connection · RCON');
|
|
expect(body).not.toContain('Test connection');
|
|
expect(body).not.toContain('legacy private reply');
|
|
});
|
|
|
|
it('keeps Super Viewer diagnostics readable without offering probes or lifecycle actions', () => {
|
|
const body = pageBody('diagnostics', 'super_viewer');
|
|
expect(body).toContain('Connection details');
|
|
expect(body).toContain('Inspect requests');
|
|
expect(body).not.toContain('Test connection');
|
|
expect(body).not.toContain('More actions');
|
|
expect(body).not.toContain('legacy private reply');
|
|
expect(pageBody('configuration', 'super_viewer')).toContain('Promoted configuration');
|
|
});
|
|
|
|
it('uses the shared Activity view for server history', () => {
|
|
const body = pageBody('history');
|
|
expect(body).toContain('Open in Activity');
|
|
expect(body).not.toContain('Loading campaign history');
|
|
expect(body).not.toContain('>Provisioning history</a>');
|
|
});
|