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 => ({ 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.replace(/\s+/g, ' ')).toContain('16–24 of 24 maps'); expect(body).toContain('game=t5&days=30&view=maps'); expect(body).toContain('Partial previous history'); expect(body).toContain('About this report'); expect(body).not.toContain('Explore this period'); expect(body).toContain('Updates every minute'); expect(body).not.toContain('>Refresh<'); const heatmap = body.slice( body.indexOf('class="heatmap'), body.indexOf('aria-label="Metrics breakdown"') ); expect(heatmap).toContain(']*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 servers'); expect(body).toContain('tabindex="0"'); expect(body).toContain('Inspect a period'); expect(body).toContain('Partial period'); expect(body).not.toMatch(/]*open/); expect(body).toContain('Weekdays show when each period starts in America/New York.'); expect(body).toContain('Daily periods are 24-hour windows.'); expect(body).toContain('Server activity over time'); }); 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.replace(/\s+/g, ' ')).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'); });