feat(web): add runnable fleet console foundation
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user