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

57 lines
3.7 KiB
TypeScript

import { expect, it } from 'vitest';
import { render } from 'svelte/server';
import type { RebalanceCandidate, RebalancePreview, RebalanceRun } from '../../src/lib/rebalancing.js';
import RebalancePreviewView from '../../src/lib/ui/components/capacity/RebalancePreviewView.svelte';
import RebalanceRunView from '../../src/lib/ui/components/capacity/RebalanceRunView.svelte';
const at = '2026-10-08T12:00:00Z';
const move: RebalanceCandidate = { server: 't5_zombie_theater_1', instanceId: 'instance-a', generation: 1,
sourceHostId: 'source-a', sourceHostName: 'Source A', targetHostId: 'target-b', targetHostName: 'Target B', improvement: 0.1 };
const preview: RebalancePreview = { id: 'preview-a', createdAt: at, expiresAt: '2026-10-08T12:05:00Z',
settings: { hostIds: ['source-a', 'target-b'], game: 'all', maxMoves: 5 }, moves: [move],
hosts: [{ id: 'source-a', name: 'Source A', before: { cpuPercent: 75, memoryPercent: 50, instances: 6 }, after: { cpuPercent: 62.5, memoryPercent: 42, instances: 5 } }],
exclusions: [{ server: 't6_zm_transit_1', reason: 'Lobby has players.' }], warnings: ['Occupied lobbies stay in place.'] };
const run: RebalanceRun = { id: 'run-a', createdAt: at, updatedAt: at, settings: preview.settings, state: 'running', message: 'Moving one lobby.',
moves: [{ ...move, id: 'move-a', state: 'moving', operationId: 'operation-a', message: 'Waiting for destination health.', updatedAt: at }] };
const noop = () => {};
it('shows current and projected resources, the move destination, and exclusion reasons before starting', () => {
const body = render(RebalancePreviewView, { props: { preview, expired: false, onstart: noop } }).body;
expect(body).toContain('6 → 5');
expect(body).toContain('75% → 62.5%');
expect(body).toContain('50% → 42%');
expect(body).toContain('Source A → Target B');
expect(body).toContain('Lobby has players.');
expect(body).toContain('Occupied lobbies stay in place.');
expect(body).toContain('aria-label="Projected host distribution" tabindex="0"');
expect(body).not.toMatch(/<button[^>]*disabled/);
});
it.each(['expired', 'empty', 'blocked'] as const)('prevents starting a %s preview', condition => {
const body = render(RebalancePreviewView, { props: {
preview: condition === 'empty' ? { ...preview, moves: [] } : preview,
expired: condition === 'expired', disabled: condition === 'blocked', onstart: noop
} }).body;
expect(body).toMatch(/<button[^>]*disabled/);
if (condition === 'expired') expect(body).toContain('This preview has expired.');
if (condition === 'empty') expect(body).toContain('No eligible moves would improve');
});
it('keeps run progress tied to its provisioning record and explains stopping semantics', () => {
const body = render(RebalanceRunView, { props: { run, canManage: true, onstop: noop } }).body;
expect(body).toContain('0 of 1 moves completed');
expect(body).toContain('Waiting for destination health.');
expect(body).toContain('href="/provisioning/operation-a?returnTo=%2Fcapacity%3Frebalance%3D1%26rebalanceRun%3Drun-a"');
expect(body).toContain('Stop remaining moves');
expect(body).toContain('A move already in progress continues through health verification.');
});
it('hides stop from viewers and terminal or already-stopping runs', () => {
expect(render(RebalanceRunView, { props: { run, canManage: false, onstop: noop } }).body).not.toContain('Stop remaining moves');
for (const state of ['stopping', 'done', 'stopped', 'failed'] as const) {
const body = render(RebalanceRunView, { props: { run: { ...run, state }, canManage: true, onstop: noop } }).body;
expect(body).not.toContain('Stop remaining moves');
expect(body).toContain('View provisioning');
}
});