- Consolidate cellular, eSIM, network and device module interactions. - Add aggregate SMS workflows and identity-aware action controls. - Extend SMSC privacy handling and module action icons.
265 lines
9.6 KiB
TypeScript
265 lines
9.6 KiB
TypeScript
// @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 { SensitiveRevealProvider } from '../privacy/sensitive-reveal.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',
|
|
resources: {
|
|
cpuPercent: 18.4,
|
|
memoryPercent: 63.2,
|
|
maxTemperatureCelsius: 46.7,
|
|
phoneNumbers: ['13800138000'],
|
|
},
|
|
};
|
|
const overviewCapabilities: 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={overviewCapabilities}
|
|
overviewDataSource={pending.source}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByRole('status', { name: '概览加载状态' }).textContent).toContain(
|
|
'正在加载概览',
|
|
);
|
|
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
|
|
pending.resolve(snapshot);
|
|
|
|
for (const heading of ['设备', 'SIM', '网络', '统计', 'CPU', '连接状态']) {
|
|
expect(await screen.findByRole('heading', { name: heading })).toBeTruthy();
|
|
}
|
|
expect(within(screen.getByRole('region', { name: '设备' })).getByText('SIMBox 8')).toBeTruthy();
|
|
expect(screen.getByText(/观测时间:2026-07-17T10:00:00Z/)).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '重启服务' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '系统重启' })).toBeTruthy();
|
|
expect(screen.getByText(/服务重启与系统重启均为高风险操作/i)).toBeTruthy();
|
|
});
|
|
|
|
it('translates only known field names and values while preserving unknown safe data', async () => {
|
|
render(
|
|
<OverviewSystemPage
|
|
instance={owner}
|
|
dataSource={{
|
|
load: async () => ({
|
|
...snapshot,
|
|
device: { Model: 'SIMBox 8', Enabled: true, Mystery: 'vendor-value' },
|
|
network: { Registration: 'registered' },
|
|
connectivity: { State: 'connected' },
|
|
}),
|
|
}}
|
|
/>,
|
|
);
|
|
|
|
const device = await screen.findByRole('region', { name: '设备' });
|
|
expect(within(device).getByText('型号')).toBeTruthy();
|
|
expect(within(device).getByText('Enabled')).toBeTruthy();
|
|
expect(within(device).getByText('是')).toBeTruthy();
|
|
expect(within(device).getByText('Mystery')).toBeTruthy();
|
|
expect(within(device).getByText('vendor-value')).toBeTruthy();
|
|
expect(within(screen.getByRole('region', { name: '网络' })).getByText('已注册')).toBeTruthy();
|
|
expect(
|
|
within(screen.getByRole('region', { name: '连接状态' })).getByText('已连接'),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it('renders the route-owned Fleet resource summary without inventing another endpoint', () => {
|
|
render(
|
|
<SensitiveRevealProvider>
|
|
<AppShell
|
|
pathname="/instances/alpha/overview"
|
|
instance={owner}
|
|
capabilities={overviewCapabilities}
|
|
/>
|
|
,
|
|
</SensitiveRevealProvider>,
|
|
);
|
|
const operations = screen.getByRole('region', { name: '运行状态与资源' });
|
|
expect(within(operations).getByText('在线')).toBeTruthy();
|
|
expect(within(operations).getByRole('meter', { name: 'CPU使用率' }).getAttribute('value')).toBe(
|
|
'18.4',
|
|
);
|
|
expect(
|
|
within(operations).getByRole('meter', { name: '内存使用率' }).getAttribute('value'),
|
|
).toBe('63.2');
|
|
expect(within(operations).getByText('46.7 °C')).toBeTruthy();
|
|
expect(within(operations).getByText('138 •••• 8000')).toBeTruthy();
|
|
expect(within(operations).queryByText('13800138000')).toBeNull();
|
|
expect(screen.queryByText(/没有可用的安全概览只读数据源/i)).toBeNull();
|
|
expect(screen.getByRole('button', { name: '重启服务' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '系统重启' })).toBeTruthy();
|
|
});
|
|
|
|
it('reveals the overview phone number through the console-wide switch', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<AppShell
|
|
pathname="/instances/alpha/overview"
|
|
instance={owner}
|
|
capabilities={overviewCapabilities}
|
|
/>,
|
|
);
|
|
const operations = screen.getByRole('region', { name: '运行状态与资源' });
|
|
expect(within(operations).getByText('138 •••• 8000')).toBeTruthy();
|
|
|
|
await user.click(screen.getByRole('button', { name: '显示敏感标识' }));
|
|
expect(within(operations).getByText('13800138000')).toBeTruthy();
|
|
expect(within(operations).queryByText('138 •••• 8000')).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(/需要先完成认证/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: '概览数据新鲜度' })).textContent).toMatch(
|
|
/可能已过期/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('无法加载概览数据。');
|
|
await user.click(screen.getByRole('button', { name: '重试加载概览' }));
|
|
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();
|
|
});
|
|
});
|