import { expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { writable } from 'svelte/store'; import type { CapacityData, CapacityHost, CapacityHistory } from '../../src/lib/capacity.js'; import CapacityPage from '../../src/routes/capacity/+page.svelte'; import CapacityHosts from '../../src/lib/ui/components/capacity/CapacityHosts.svelte'; import CapacityChart from '../../src/lib/ui/components/capacity/CapacityChart.svelte'; vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/capacity?host=ec-1&game=t5') } })); vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/capacity') }) })); vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), afterNavigate: vi.fn(), goto: vi.fn() })); const allowance = { cpuCores: 0.5, memoryBytes: 1024 ** 3, source: 'host' as const, samples: 20, observedAt: '2026-10-06T12:00:00Z' }; const host = (changes: Partial = {}): CapacityHost => ({ id: 'ec-1', name: 'ec-1', connected: true, readiness: 'ready', updatedAt: '2026-10-06T12:00:00Z', eligible: true, games: ['t5', 't6'], reasons: [], cpuCoresAvailable: 2, memoryBytesAvailable: 4 * 1024 ** 3, portsAvailable: 1, totalPorts: 10, estimatedServers: 1, limits: { cpu: 4, memory: 4, ports: 1 }, limitingResource: 'Ports', calibration: { running: allowance, startup: allowance }, ...changes }); const history: CapacityHistory = { temperatures: [], range: '24h', startAt: 1000, endAt: 5000, bucketMs: 1000, points: [ { at: 1000, cpuCoresAvailable: 2, memoryBytesAvailable: 1024 ** 3, reportingHosts: 1, expectedHosts: 1 }, { at: 2000, cpuCoresAvailable: null, memoryBytesAvailable: null, reportingHosts: 0, expectedHosts: 1 }, { at: 3000, cpuCoresAvailable: 3, memoryBytesAvailable: 2 * 1024 ** 3, reportingHosts: 1, expectedHosts: 1 } ] }; const data = (): CapacityData => ({ query: { range: '24h', game: 't5', hostId: 'ec-1' }, history, current: { updatedAt: '2026-10-06T12:00:00Z', hosts: [host()], hostOptions: [{ id: 'ec-1', name: 'ec-1' }], warnings: [], summary: { estimatedServers: 1, cpuCoresAvailable: 2, memoryBytesAvailable: 4 * 1024 ** 3, portsAvailable: 1, eligibleHosts: 1, totalHosts: 1, startupSlots: 3, startupLimit: 3, activeStartups: 0, queued: 0 } } }); it('keeps current estimates and host comparison before history with scoped history controls', () => { const body = render(CapacityPage, { props: { data: data() } }).body; expect(body.indexOf('id="capacity-now-title"')).toBeLessThan( body.indexOf('id="host-capacity-title"') ); expect(body.indexOf('id="host-capacity-title"')).toBeLessThan( body.indexOf('id="capacity-history-title"') ); expect(body.indexOf('aria-label="History range"')).toBeGreaterThan( body.indexOf('id="capacity-history-title"') ); expect(body).toContain('range=7d&game=t5&host=ec-1'); expect(body).toContain('3 of 3 available'); expect(body).toContain('No queued servers'); expect(body).not.toContain('href="/provisioning"'); expect(body).not.toContain('Manage hosts'); expect(body).toContain('Updates every 30 seconds'); expect(body).not.toContain('>Refresh<'); expect(body.indexOf('id="temperature-history-title"')).toBeGreaterThan( body.indexOf('id="capacity-history-title"') ); expect(body.match(/aria-label="History range"/g)).toHaveLength(2); }); it('keeps fleet-wide queue scope explicit and preserves unavailable estimates', () => { const report = data(); report.current.summary.queued = 2; report.current.summary.estimatedServers = null; const body = render(CapacityPage, { props: { data: report } }).body; expect(body).toContain('Capacity estimate unavailable'); expect(body).toContain('href="/provisioning"'); expect(body.replace(/\s+/g, ' ')).toContain('2 servers queued'); expect(body).toContain('Fleet-wide'); }); it('keeps ineligible hosts and their reasons visible while collapsing detailed assumptions', () => { const body = render(CapacityHosts, { props: { returnTo: '/capacity', hosts: [ host(), host({ id: 'missing-files', name: 'Missing files', eligible: false, estimatedServers: 0, limitingResource: null, reasons: ['Latest game files are not ready.'] }), host({ id: 'offline', name: 'Offline host', connected: false, eligible: false, estimatedServers: 0, cpuCoresAvailable: null, memoryBytesAvailable: null, limitingResource: null, reasons: ['Waiting for a fresh host report.'] }) ] } }).body; expect(body).toContain('Additional servers'); expect(body).toContain('Not eligible'); expect(body).toContain('Latest game files are not ready.'); expect(body).toContain('Offline'); expect(body).toContain('>—'); expect(body).not.toContain('>Ready<'); expect(body).toMatch(/]*id="capacity-host-ec-1"/); expect(body).toContain('aria-expanded="false"'); expect(body).toContain('Running server'); expect(body).toContain('aria-label="Host capacity comparison" tabindex="0"'); }); it('preserves chart gaps, keyboard inspection and reporting coverage in the compact chart', () => { const body = render(CapacityChart, { props: { history, scope: 'All hosts' } }).body; expect(body.match(/class="trend /g)).toHaveLength(2); expect(body).toContain('role="slider"'); expect(body).toContain('1 of 1 hosts reporting'); expect(body).toContain('Reporting hosts'); expect(body).toContain('The game filter applies to current launch estimates only.'); expect(body).toContain('About this chart'); expect(body).not.toMatch(/]*open/); const empty = render(CapacityChart, { props: { history: { ...history, points: [history.points[1]] }, scope: 'All hosts' } }).body; expect(empty).toContain('Waiting for capacity history'); expect(empty).toContain('No complete report'); });