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

142 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import type { MetricsData, MapMetrics, ServerMetrics } from '../../src/lib/metrics.js';
import MetricsPage from '../../src/routes/metrics/+page.svelte';
import MetricsSummary from '../../src/lib/ui/components/metrics/MetricsSummary.svelte';
import MetricsTables from '../../src/lib/ui/components/metrics/MetricsTables.svelte';
import MetricsExplore from '../../src/lib/ui/components/metrics/MetricsExplore.svelte';
import MetricsChart from '../../src/lib/ui/components/metrics/MetricsChart.svelte';
import { metricPage } from '../../src/lib/ui/metrics.js';
import { page } from '$app/state';
vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/metrics'), state: {} } }));
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), afterNavigate: vi.fn(), goto: vi.fn(), replaceState: vi.fn() }));
const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({ game: 't5', map: `Map ${String(i).padStart(2, '0')}`, observed: 10, active: 5, idle: 5, unknown: 0, playerHours: i, full: 1, capacityObserved: 4, ...changes });
const server = (i: number): ServerMetrics => ({ server: `t5_zombie_theater_${i}`, currentRegistered: i !== 1, observed: 10, active: i / 10, idle: 10 - i / 10, unknown: 0, playerHours: i });
function report(): MetricsData {
const now = Date.parse('2026-10-06T12:00:00Z');
const summary = { observed: 10, active: 5, idle: 5, unknown: 2, playerHours: 20 };
return { days: 7, since: now - 7 * 86400000, now, firstRetainedSampleAt: now - 14 * 86400000, latestSampleAt: now,
registeredServerCount: 24, scope: { game: null, map: null, timeZone: 'America/New_York' }, scopeOptions: { games: ['t5'], maps: [] },
scopeFirstSampleAt: now - 14 * 86400000, scopeLatestSampleAt: now, summary,
maps: Array.from({ length: 24 }, (_, i) => map(i + 1)), servers: Array.from({ length: 24 }, (_, i) => server(i + 1)),
points: [{ ...summary, at: now - 86400000, startAt: now - 86400000, endAt: now, partial: true }],
previous: { since: now - 14 * 86400000, until: now - 7 * 86400000, summary: { ...summary, active: 4, idle: 6, playerHours: 10 }, firstSampleAt: now - 14 * 86400000, lastSampleAt: now - 7 * 86400000, partialHistory: true },
pooledPressure: { value: 25, fullMapHours: 1, knownMapHours: 4, coveredMaps: 24, totalMaps: 24 }, heatmap: { timeZone: 'America/New_York', cells: [] }
};
}
function scope(search: string, stateSearch?: string) {
Object.assign(page, { url: new URL(`http://localhost/metrics${search}`), state: stateSearch ? { metricsTableSearch: stateSearch } : {} });
}
beforeEach(() => scope(''));
it('shows a summary and one paginated breakdown, preserving scope on range links', () => {
scope('?game=t5&days=7&view=maps&mapPage=2');
const body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
expect(body).toContain('aria-label="Metrics breakdown"');
expect(body).toContain('aria-label="Map metrics table"');
expect(body).not.toContain('aria-label="Instance metrics table"');
expect(body).toContain('16–24 of 24 maps');
expect(body).toContain('game=t5&amp;days=30&amp;view=maps');
expect(body).toContain('Partial previous history');
expect(body).toContain('About this report');
expect(body).not.toContain('Explore this period');
});
it('keeps percentage points, relative changes and unavailable capacity distinct', () => {
const data = report();
let body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('+10 pp'); expect(body).toContain('+100%');
data.pooledPressure = { value: null, fullMapHours: 0, knownMapHours: 0, coveredMaps: 0, totalMaps: 24 };
data.maps = [map(1, { capacityObserved: 0 })];
data.previous.summary.playerHours = 0;
body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('Capacity data unavailable');
expect(body).toContain('New · no previous player-hours');
expect(body).toContain('1 of 1 maps lack capacity data');
expect(body).not.toContain('Infinity');
data.summary.observed = 0;
body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('Comparison unavailable');
});
it('searches and sorts the complete map set before pagination and clamps invalid pages', () => {
const data = report();
scope('?mapPage=2');
let body = render(MetricsTables, { props: { maps: data.maps, servers: data.servers } }).body;
expect(body).toContain('Map 09'); expect(body).not.toContain('Map 24');
scope('?mapPage=2', '?mapQ=Map+24&mapPage=100&mapSort=playerHours');
body = render(MetricsTables, { props: { maps: data.maps, servers: data.servers } }).body;
expect(body).toContain('Map 24'); expect(body).toContain('1–1 of 1 maps');
expect(body).not.toContain('>Next<');
expect(metricPage(data.maps, '-1').page).toBe(1);
expect(metricPage(data.maps, 'Infinity').page).toBe(1);
expect(metricPage([], '20')).toMatchObject({ page: 1, start: 0, end: 0, total: 0 });
});
it('keeps missing capacity last when sorting and never presents it as zero full time', () => {
scope('?mapSort=full&mapDirection=asc');
const maps = [map(1, { capacityObserved: 0 }), map(2, { full: 0 }), map(3)];
const body = render(MetricsTables, { props: { maps, servers: [] } }).body;
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 03'));
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 01'));
expect(body).toContain('0.0%'); expect(body).toContain('Capacity data unavailable');
});
it('preserves instance return context and distinguishes historic names without current links', () => {
scope('?view=instances&serverQ=theater&days=30');
const body = render(MetricsTables, { props: { maps: [], servers: [server(1), server(2)], view: 'instances' } }).body;
expect(body).toContain('Historical');
expect(body).not.toContain('href="/servers/t5_zombie_theater_1');
expect(body).toContain('href="/servers/t5_zombie_theater_2?returnTo=');
expect(body).toContain('serverQ%3Dtheater');
expect(body).not.toContain('>Idle</button>');
expect(body).not.toContain('Open current server');
});
it('highlights at most three supported trials and collapses tentative findings', () => {
const data = report(); data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, capacityObserved: 0 }));
const body = render(MetricsExplore, { props: { report: data } }).body;
const fold = body.indexOf('Other findings');
expect(body.slice(0, fold).match(/class="recommendation /g)).toHaveLength(3);
expect(body.indexOf('Map 25')).toBeGreaterThan(fold);
expect(body).toContain('Limited evidence');
expect(body).not.toMatch(/<details[^>]*open/);
data.maps = [map(1, { capacityObserved: 0 })];
expect(render(MetricsExplore, { props: { report: data } }).body).toContain('No hosting trials meet the evidence threshold');
});
it('keeps accessible chart selection, partial periods and gaps while collapsing inspection controls', () => {
const data = report();
data.points.push({ at: data.now, startAt: data.now, endAt: data.now + 1000, partial: false, observed: 0, active: 0, idle: 0, unknown: 0, playerHours: 0 });
const body = render(MetricsChart, { props: { points: data.points, days: data.days, timeZone: data.scope.timeZone } }).body;
expect(body).toContain('aria-label="Selected period summary"');
expect(body).toContain('No captured time · not evidence of idle instances');
expect(body).toContain('tabindex="0"');
expect(body).toContain('Inspect a period');
expect(body).toContain('Partial period');
expect(body).not.toMatch(/<details[^>]*open/);
});
it('uses shallow URL state for the selected breakdown while retaining the common summary', () => {
scope('?view=maps', '?view=instances&serverQ=theater&serverPage=2');
let body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
expect(body).toContain('aria-label="Instance metrics table"');
expect(body).not.toContain('aria-label="Map metrics table"');
expect(body).toContain('16–24 of 24 instances');
scope('?view=maps', '?view=insights');
body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
expect(body).toContain('id="metrics-explore-title"');
expect(body).toContain('Time all lobbies were full');
expect(body).not.toContain('aria-label="Map metrics table"');
expect(body).not.toContain('aria-label="Instance metrics table"');
});
it('filters other insights without hiding the highlighted supported trials', () => {
scope('?view=insights&insightQ=Map+24&insightSort=map');
const body = render(MetricsExplore, { props: { report: report() } }).body;
expect(body).toContain('Map 01');
expect(body).toContain('Map 24');
expect(body).not.toContain('Map 23');
});