feat(web): complete fleet and instance management slices

This commit is contained in:
chick
2026-07-17 17:12:35 +08:00
parent 4093634e13
commit 6e817f380a
15 changed files with 1737 additions and 203 deletions
+77 -4
View File
@@ -6,7 +6,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { AppShell, type InstanceContext } from './app-shell.js';
import { type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
afterEach(cleanup);
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
const snapshot: FleetSnapshot = {
instances: [
@@ -19,8 +22,29 @@ const snapshot: FleetSnapshot = {
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin' },
],
statuses: new Map([
['bravo', { reachable: true, authenticated: false, latencyMs: 40 }],
['alpha', { reachable: true, authenticated: true, latencyMs: 10 }],
[
'bravo',
{
reachable: true,
authenticated: false,
latencyMs: 40,
summary: {
version: '2.0',
capabilities: ['sms'],
freshness: 'stale',
anomalies: ['clock drift'],
},
},
],
[
'alpha',
{
reachable: true,
authenticated: true,
latencyMs: 10,
summary: { version: '1.0', capabilities: ['ussd'], freshness: 'fresh' },
},
],
]),
};
@@ -29,7 +53,8 @@ function source(load: FleetDataSource['load']): FleetDataSource {
}
describe('React AppShell and Fleet vertical slice', () => {
it('loads real injected data and renders canonical origins and owner routes', async () => {
it('loads real injected data and renders canonical origins and owner routes without React key warnings', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
let resolve!: (value: FleetSnapshot) => void;
const dataSource = source(() => new Promise((done) => (resolve = done)));
render(<AppShell pathname="/fleet" version="0.1.0" fleetDataSource={dataSource} />);
@@ -49,6 +74,11 @@ describe('React AppShell and Fleet vertical slice', () => {
.getAttribute('href'),
).toBe('http://bravo.example:8080');
expect(screen.getByText('Version 0.1.0')).toBeTruthy();
expect(
consoleError.mock.calls.some((call) =>
call.some((argument) => String(argument).includes('unique "key" prop')),
),
).toBe(false);
});
it('supports accessible search, status filtering, sorting, and visible selection', async () => {
@@ -102,6 +132,49 @@ describe('React AppShell and Fleet vertical slice', () => {
unmount();
});
it('supports metadata filters, column visibility, pagination, and a batch-action entry', async () => {
const user = userEvent.setup();
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
await screen.findByRole('row', { name: /Alpha/ });
await user.selectOptions(screen.getByRole('combobox', { name: 'Capability' }), 'sms');
expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
expect(screen.getByRole('row', { name: /Bravo/ }).textContent).toContain('clock drift');
await user.click(screen.getByRole('button', { name: 'Choose columns' }));
await user.click(screen.getByRole('checkbox', { name: 'Show anomalies column' }));
expect(screen.queryByRole('columnheader', { name: 'Anomalies' })).toBeNull();
await user.click(screen.getByRole('checkbox', { name: 'Select Bravo' }));
expect(
(screen.getByRole('button', { name: 'Batch actions' }) as HTMLButtonElement).disabled,
).toBe(false);
await user.click(screen.getByRole('button', { name: 'Batch actions' }));
expect(screen.getByText('Choose an action for 1 selected instance.')).toBeTruthy();
});
it('paginates fleet rows and selects only the current page', async () => {
const user = userEvent.setup();
const many: FleetSnapshot = {
instances: Array.from({ length: 11 }, (_, index) => ({
id: `instance-${String(index + 1).padStart(2, '0')}`,
name: `Instance ${String(index + 1).padStart(2, '0')}`,
url: `https://instance-${index + 1}.example`,
})),
statuses: new Map(),
};
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => many)} />);
await screen.findByRole('row', { name: /Instance 01/ });
expect(screen.queryByRole('row', { name: /Instance 11/ })).toBeNull();
expect(screen.getByText('Page 1 of 2')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: 'Select all visible instances' }));
expect(screen.getByText('10 selected')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Next page' }));
expect(await screen.findByRole('row', { name: /Instance 11/ })).toBeTruthy();
expect(screen.getByText('Page 2 of 2')).toBeTruthy();
});
it('binds instance context to the route owner and never leaks mismatched context', () => {
const instance: InstanceContext = {
id: 'owner',