Files

188 lines
6.7 KiB
TypeScript

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> = {}): 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&amp;game=t5&amp;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</th>');
expect(body).toContain('Not eligible');
expect(body).toContain('Latest game files are not ready.');
expect(body).toContain('Offline');
expect(body).toContain('>—</td>');
expect(body).not.toContain('>Ready<');
expect(body).toMatch(/<tr hidden[^>]*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(/<details[^>]*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');
});