feat(web): add runnable fleet console foundation

This commit is contained in:
chick
2026-07-17 16:02:15 +08:00
parent b4ae28c8f0
commit 4093634e13
16 changed files with 2730 additions and 7 deletions
+127
View File
@@ -0,0 +1,127 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
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);
const snapshot: FleetSnapshot = {
instances: [
{
id: 'bravo',
name: 'Bravo',
url: 'http://user:secret@bravo.example:8080/private',
tags: ['west'],
},
{ 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 }],
]),
};
function source(load: FleetDataSource['load']): FleetDataSource {
return { load };
}
describe('React AppShell and Fleet vertical slice', () => {
it('loads real injected data and renders canonical origins and owner routes', async () => {
let resolve!: (value: FleetSnapshot) => void;
const dataSource = source(() => new Promise((done) => (resolve = done)));
render(<AppShell pathname="/fleet" version="0.1.0" fleetDataSource={dataSource} />);
expect(screen.getByRole('status', { name: 'Fleet loading status' }).textContent).toContain(
'Loading instances',
);
resolve(snapshot);
const alpha = await screen.findByRole('row', { name: /Alpha/ });
expect(within(alpha).getByRole('link', { name: 'Alpha' }).getAttribute('href')).toBe(
'/instances/alpha/overview',
);
expect(
within(screen.getByRole('row', { name: /Bravo/ }))
.getByRole('link', { name: 'Open Bravo origin' })
.getAttribute('href'),
).toBe('http://bravo.example:8080');
expect(screen.getByText('Version 0.1.0')).toBeTruthy();
});
it('supports accessible search, status filtering, sorting, and visible selection', async () => {
const user = userEvent.setup();
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
await screen.findByRole('row', { name: /Alpha/ });
await user.click(screen.getByRole('button', { name: /Sort by latency/i }));
let rows = screen.getAllByRole('row').slice(1);
expect(rows[0]?.textContent).toContain('Alpha');
await user.click(screen.getByRole('button', { name: /Sort by latency/i }));
rows = screen.getAllByRole('row').slice(1);
expect(rows[0]?.textContent).toContain('Bravo');
await user.selectOptions(screen.getByRole('combobox', { name: 'Status' }), 'auth');
expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
await user.click(screen.getByRole('checkbox', { name: 'Select all visible instances' }));
expect(
(screen.getByRole('checkbox', { name: 'Select Bravo' }) as HTMLInputElement).checked,
).toBe(true);
expect(screen.getByText('1 selected')).toBeTruthy();
await user.type(screen.getByRole('searchbox', { name: 'Search instances' }), 'missing');
expect(screen.getByText(/No instances match your search/i)).toBeTruthy();
});
it('exposes error, retry, configured-empty, and unsafe-origin states without claiming a backend', async () => {
const user = userEvent.setup();
const load = vi
.fn<FleetDataSource['load']>()
.mockRejectedValueOnce(new Error('collector unavailable'))
.mockResolvedValueOnce({
instances: [{ id: 'unsafe', name: 'Unsafe', url: 'javascript:alert(1)' }],
statuses: new Map(),
});
const first = render(<AppShell pathname="/fleet" fleetDataSource={source(load)} />);
expect((await screen.findByRole('alert')).textContent).toContain('collector unavailable');
await user.click(screen.getByRole('button', { name: 'Retry loading instances' }));
expect(await screen.findByRole('row', { name: /Unsafe/ })).toBeTruthy();
expect(screen.queryByRole('link', { name: 'Open Unsafe origin' })).toBeNull();
first.unmount();
const { unmount } = render(
<AppShell
pathname="/fleet"
fleetDataSource={source(async () => ({ instances: [], statuses: new Map() }))}
/>,
);
expect(await screen.findByText(/No instances are configured/i)).toBeTruthy();
unmount();
});
it('binds instance context to the route owner and never leaks mismatched context', () => {
const instance: InstanceContext = {
id: 'owner',
name: 'Owner modem',
origin: 'https://owner.example/path',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const { rerender } = render(
<AppShell pathname="/instances/owner/messages" instance={instance} />,
);
expect(screen.getByRole('heading', { name: 'Messages' })).toBeTruthy();
expect(screen.getByText('Owner modem')).toBeTruthy();
expect(screen.getByRole('link', { name: 'Open original site' }).getAttribute('href')).toBe(
'https://owner.example',
);
rerender(<AppShell pathname="/instances/someone-else/messages" instance={instance} />);
expect(screen.queryByText('Owner modem')).toBeNull();
expect(screen.getByText(/Instance context is unavailable/i)).toBeTruthy();
});
});