feat(web): complete fleet and instance management slices
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { InstanceContext, InstanceModule } from '../app-shell.js';
|
||||
import {
|
||||
InstanceDetail,
|
||||
type CapabilityDataSource,
|
||||
type InstanceCapabilityMap,
|
||||
} from './instance-detail.js';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const owner: InstanceContext = {
|
||||
id: 'owner',
|
||||
name: 'Owner modem',
|
||||
origin: 'https://owner.example/path',
|
||||
status: 'online',
|
||||
authentication: 'authenticated',
|
||||
freshness: 'fresh',
|
||||
};
|
||||
|
||||
const capabilities: InstanceCapabilityMap = {
|
||||
overview: { state: 'supported' },
|
||||
messages: { state: 'degraded', explanation: 'Message history is read-only.' },
|
||||
calls: { state: 'unsupported', explanation: 'This modem has no voice support.' },
|
||||
esim: { state: 'unknown', explanation: 'Capability discovery did not report eSIM.' },
|
||||
};
|
||||
|
||||
describe('InstanceDetail', () => {
|
||||
it('makes only supported modules actionable and explains every other capability state', () => {
|
||||
render(
|
||||
<InstanceDetail
|
||||
instanceId="owner"
|
||||
module="overview"
|
||||
instance={owner}
|
||||
capabilities={capabilities}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Overview' }).getAttribute('href')).toBe(
|
||||
'/instances/owner/overview',
|
||||
);
|
||||
expect(screen.queryByRole('link', { name: 'Messages' })).toBeNull();
|
||||
expect(screen.getByText('Message history is read-only.')).toBeTruthy();
|
||||
expect(screen.getByText('This modem has no voice support.')).toBeTruthy();
|
||||
expect(screen.getByText('Capability discovery did not report eSIM.')).toBeTruthy();
|
||||
expect(screen.getAllByText('Capability state is unknown.').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('never renders or loads context when the direct-route owner does not match', () => {
|
||||
const load = vi.fn<CapabilityDataSource['load']>();
|
||||
render(
|
||||
<InstanceDetail
|
||||
instanceId="someone-else"
|
||||
module="messages"
|
||||
instance={owner}
|
||||
capabilityDataSource={{ load }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Owner modem')).toBeNull();
|
||||
expect(screen.queryByRole('navigation', { name: 'Instance modules' })).toBeNull();
|
||||
expect(screen.getByText(/Instance context is unavailable/i)).toBeTruthy();
|
||||
expect(load).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('aborts the prior owner load and fences stale results to the latest owner', async () => {
|
||||
const pending = new Map<
|
||||
string,
|
||||
{ resolve: (value: InstanceCapabilityMap) => void; signal: AbortSignal }
|
||||
>();
|
||||
const dataSource: CapabilityDataSource = {
|
||||
load(instanceId, signal) {
|
||||
return new Promise((resolve) => pending.set(instanceId, { resolve, signal }));
|
||||
},
|
||||
};
|
||||
const second: InstanceContext = { ...owner, id: 'second', name: 'Second modem' };
|
||||
const { rerender } = render(
|
||||
<InstanceDetail
|
||||
instanceId="owner"
|
||||
module="overview"
|
||||
instance={owner}
|
||||
capabilityDataSource={dataSource}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => expect(pending.has('owner')).toBe(true));
|
||||
|
||||
rerender(
|
||||
<InstanceDetail
|
||||
instanceId="second"
|
||||
module="overview"
|
||||
instance={second}
|
||||
capabilityDataSource={dataSource}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => expect(pending.has('second')).toBe(true));
|
||||
expect(pending.get('owner')?.signal.aborted).toBe(true);
|
||||
|
||||
pending.get('second')?.resolve({ overview: { state: 'supported' } });
|
||||
expect(await screen.findByRole('link', { name: 'Overview' })).toBeTruthy();
|
||||
pending
|
||||
.get('owner')
|
||||
?.resolve({ overview: { state: 'unsupported', explanation: 'Stale owner result' } });
|
||||
await waitFor(() => expect(screen.queryByText('Stale owner result')).toBeNull());
|
||||
expect(screen.getByText('Second modem')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each<[InstanceModule, string]>([
|
||||
['cellular', 'Cellular'],
|
||||
['device-network', 'Device Network'],
|
||||
['automation', 'Automation'],
|
||||
['ota', 'OTA'],
|
||||
])('uses the existing module contract for %s', (module, label) => {
|
||||
render(
|
||||
<InstanceDetail
|
||||
instanceId="owner"
|
||||
module={module}
|
||||
instance={owner}
|
||||
capabilities={{ [module]: { state: 'supported' } }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('heading', { name: label })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user