feat(web): add overview system module foundation

This commit is contained in:
chick
2026-07-17 19:40:13 +08:00
parent be1741c2f8
commit 4a595540e9
7 changed files with 1106 additions and 6 deletions
@@ -0,0 +1,203 @@
// @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 {
OverviewSystemPage,
type OverviewDataSource,
type OverviewSnapshot,
} from './overview-system.js';
import type { InstanceCapabilityMap } from './instance-detail.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const overviewCapability: InstanceCapabilityMap = {
overview: { state: 'supported' },
};
const snapshot: OverviewSnapshot = {
observedAt: '2026-07-17T10:00:00Z',
device: { Model: 'SIMBox 8', Uptime: '2 days' },
sim: { Slots: 8, Active: 6 },
network: { Operator: 'Example Mobile', Registration: 'registered' },
stats: { 'Messages today': 42, Calls: 7 },
cpu: { Usage: '18%', Temperature: '46 °C' },
connectivity: { State: 'connected', Latency: '21 ms' },
};
function deferredSource() {
let resolve!: (value: OverviewSnapshot) => void;
let reject!: (reason: unknown) => void;
const load = vi.fn<OverviewDataSource['load']>(
(_instanceId, _signal) =>
new Promise((done, fail) => {
void _instanceId;
void _signal;
resolve = done;
reject = fail;
}),
);
return {
source: { load },
load,
resolve: (value: OverviewSnapshot) => resolve(value),
reject: (reason: unknown) => reject(reason),
};
}
describe('Phase 6.1 Overview / System read slice', () => {
it('loads only through an injected source and renders six structured sections', async () => {
const pending = deferredSource();
render(
<AppShell
pathname="/instances/alpha/overview"
instance={owner}
capabilities={overviewCapability}
overviewDataSource={pending.source}
/>,
);
expect(screen.getByRole('status', { name: 'Overview loading status' }).textContent).toContain(
'Loading overview',
);
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
for (const heading of ['Device', 'SIM', 'Network', 'Statistics', 'CPU', 'Connectivity']) {
expect(await screen.findByRole('heading', { name: heading })).toBeTruthy();
}
expect(
within(screen.getByRole('region', { name: 'Device' })).getByText('SIMBox 8'),
).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T10:00:00Z/)).toBeTruthy();
expect(screen.queryByRole('button', { name: /restart/i })).toBeNull();
expect(screen.getByText(/Restart is unavailable.*R3/i)).toBeTruthy();
});
it('is honest when no safe read source is injected and never attempts a production endpoint', () => {
render(
<AppShell
pathname="/instances/alpha/overview"
instance={owner}
capabilities={overviewCapability}
/>,
);
expect(screen.getByRole('status', { name: 'Overview unavailable' }).textContent).toMatch(
/no safe overview read data source/i,
);
expect(screen.queryByRole('button', { name: /restart/i })).toBeNull();
});
it('shows authentication-required without loading or leaking prior owner data', async () => {
const load = vi.fn<OverviewDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<OverviewSystemPage instance={owner} dataSource={{ load }} />);
expect(await screen.findByText('SIMBox 8')).toBeTruthy();
rerender(
<OverviewSystemPage
instance={{
...owner,
id: 'bravo',
name: 'Bravo',
authentication: 'auth-required',
status: 'auth-required',
}}
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.queryByText('SIMBox 8')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
});
it('marks injected data stale from owner context while retaining structured values', async () => {
render(
<OverviewSystemPage
instance={{ ...owner, freshness: 'stale' }}
dataSource={{ load: async () => snapshot }}
/>,
);
expect((await screen.findByRole('status', { name: 'Overview freshness' })).textContent).toMatch(
/stale/i,
);
expect(screen.getByText('SIMBox 8')).toBeTruthy();
});
it('surfaces read errors and supports a real injected retry', async () => {
const user = userEvent.setup();
const load = vi
.fn<OverviewDataSource['load']>()
.mockRejectedValueOnce(new Error('collector unavailable'))
.mockResolvedValueOnce(snapshot);
render(<OverviewSystemPage instance={owner} dataSource={{ load }} />);
expect((await screen.findByRole('alert')).textContent).toContain(
'Overview data could not be loaded.',
);
await user.click(screen.getByRole('button', { name: 'Retry loading overview' }));
expect(await screen.findByText('SIMBox 8')).toBeTruthy();
expect(load).toHaveBeenCalledTimes(2);
});
it('aborts old reads, fences late owners, and reloads on refreshSignal', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<OverviewDataSource['load']>((instanceId, signal) =>
instanceId === 'alpha'
? alpha.source.load(instanceId, signal)
: bravo.source.load(instanceId, signal),
);
const source = { load };
const { rerender } = render(
<OverviewSystemPage instance={owner} dataSource={source} refreshSignal={0} />,
);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(
<OverviewSystemPage
instance={{ ...owner, id: 'bravo', name: 'Bravo' }}
dataSource={source}
refreshSignal={0}
/>,
);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve({ ...snapshot, device: { Model: 'Alpha stale' } });
bravo.resolve({ ...snapshot, device: { Model: 'Bravo current' } });
expect(await screen.findByText('Bravo current')).toBeTruthy();
expect(screen.queryByText('Alpha stale')).toBeNull();
rerender(
<OverviewSystemPage
instance={{ ...owner, id: 'bravo', name: 'Bravo' }}
dataSource={source}
refreshSignal={1}
/>,
);
expect(load).toHaveBeenCalledTimes(3);
});
it('keeps unsupported capability explicit and does not invoke the overview source', () => {
const load = vi.fn<OverviewDataSource['load']>();
render(
<AppShell
pathname="/instances/alpha/overview"
instance={owner}
capabilities={{
overview: { state: 'unsupported', explanation: 'No overview read support' },
}}
overviewDataSource={{ load }}
/>,
);
expect(screen.getAllByText('No overview read support').length).toBeGreaterThan(0);
expect(load).not.toHaveBeenCalled();
});
});