- 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.
264 lines
9.5 KiB
TypeScript
264 lines
9.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { ReactElement } from 'react';
|
|
import type { InstanceContext } from '../app-shell.js';
|
|
import { SensitiveRevealProvider } from '../privacy/sensitive-reveal.js';
|
|
import { ConfigurationModule, SimModule, VowifiModule } from './device-module-panels.js';
|
|
import type { InstanceModuleSnapshot } from './instance-module-api-data-source.js';
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
localStorage.clear();
|
|
});
|
|
|
|
function renderWithPrivacy(ui: ReactElement) {
|
|
return render(<SensitiveRevealProvider>{ui}</SensitiveRevealProvider>);
|
|
}
|
|
|
|
const instance: InstanceContext = {
|
|
id: 'owner',
|
|
name: 'Owner modem',
|
|
origin: 'https://node.internal',
|
|
status: 'online',
|
|
authentication: 'authenticated',
|
|
freshness: 'fresh',
|
|
};
|
|
|
|
function snapshot(overrides: Partial<InstanceModuleSnapshot> = {}): InstanceModuleSnapshot {
|
|
return {
|
|
instanceId: 'owner',
|
|
module: 'sim',
|
|
observedAt: '2026-09-04T02:00:00.000Z',
|
|
authenticated: true,
|
|
sections: [
|
|
{
|
|
key: 'sim',
|
|
path: '/sim',
|
|
state: 'ok',
|
|
status: 200,
|
|
data: {
|
|
slots: [
|
|
{
|
|
slot: 1,
|
|
status: 'ready',
|
|
iccid: '89860112345678901234',
|
|
phone_number: '13800001234',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{ key: 'apn', path: '/apn', state: 'empty', status: 204, data: null },
|
|
{ key: 'bandLock', path: '/band-lock', state: 'unsupported', status: 404, data: null },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('DeviceModulePanel', () => {
|
|
it('renders nested device rows and reports per-section availability', async () => {
|
|
const reader = { read: vi.fn(async () => snapshot()) };
|
|
renderWithPrivacy(<SimModule instance={instance} reader={reader} />);
|
|
|
|
expect(await screen.findByRole('heading', { name: /SIM 卡状态/ })).toBeTruthy();
|
|
expect(screen.getByRole('heading', { name: /APN 配置/ })).toBeTruthy();
|
|
expect(screen.getByText('设备返回成功,但没有可用数据。')).toBeTruthy();
|
|
expect(screen.getByText('当前设备固件未提供该接口。')).toBeTruthy();
|
|
// Device enum values arrive in English; the shared dictionary renders them in Chinese.
|
|
expect(screen.getByRole('cell', { name: '就绪' })).toBeTruthy();
|
|
expect(screen.queryByRole('heading', { name: /安全与锁卡/ })).toBeNull();
|
|
});
|
|
|
|
it('spreads a nested settings object into the parent card', async () => {
|
|
const reader = {
|
|
read: vi.fn(async () =>
|
|
snapshot({
|
|
module: 'configuration',
|
|
sections: [
|
|
{
|
|
key: 'authSettings',
|
|
path: '/auth/settings',
|
|
state: 'ok',
|
|
status: 200,
|
|
data: {
|
|
configured: true,
|
|
settings: { password_min_length: 8, session_ttl_seconds: 604_800 },
|
|
},
|
|
},
|
|
{ key: 'workMode', path: '/work-mode', state: 'empty', status: 200, data: {} },
|
|
{
|
|
key: 'authStatus',
|
|
path: '/auth/status',
|
|
state: 'unsupported',
|
|
status: 404,
|
|
data: null,
|
|
},
|
|
{ key: 'hub', path: '/hub', state: 'empty', status: 200, data: {} },
|
|
],
|
|
}),
|
|
),
|
|
};
|
|
renderWithPrivacy(<ConfigurationModule instance={instance} reader={reader} />);
|
|
expect(await screen.findByRole('heading', { name: /设备安全设置/ })).toBeTruthy();
|
|
expect(screen.getByText('604800')).toBeTruthy();
|
|
expect(screen.getByText('是')).toBeTruthy();
|
|
// A probe the firmware never exposed is stated plainly instead of rendered as raw JSON.
|
|
expect(screen.getByRole('heading', { name: /设备认证状态/ })).toBeTruthy();
|
|
expect(screen.getAllByText('当前设备固件未提供该接口。').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('splits PIN lock fields out of the SIM identity card', async () => {
|
|
const reader = {
|
|
read: vi.fn(async () =>
|
|
snapshot({
|
|
sections: [
|
|
{
|
|
key: 'sim',
|
|
path: '/sim',
|
|
state: 'ok',
|
|
status: 200,
|
|
data: {
|
|
slot: 1,
|
|
iccid: '8986011234567890123',
|
|
pin1_status: 'pin_locked',
|
|
pin1_retry: 2,
|
|
puk1_retry: 9,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
};
|
|
renderWithPrivacy(<SimModule instance={instance} reader={reader} />);
|
|
|
|
const identity = await screen.findByRole('region', { name: 'SIM 卡状态' });
|
|
const lock = screen.getByRole('region', { name: '安全与锁卡' });
|
|
expect(identity.textContent).toContain('ICCID');
|
|
expect(identity.textContent).not.toContain('PIN1');
|
|
expect(lock.textContent).toContain('PIN1 状态');
|
|
expect(lock.textContent).toContain('PIN 已锁定');
|
|
expect(lock.textContent).toContain('PIN1 剩余尝试');
|
|
expect(lock.textContent).toContain('PUK1 剩余尝试');
|
|
});
|
|
|
|
it('labels snake_case device keys and localizes boolean values', async () => {
|
|
const reader = {
|
|
read: vi.fn(async () =>
|
|
snapshot({
|
|
sections: [
|
|
{ key: 'sim', path: '/sim', state: 'ok', status: 200, data: { slots: [] } },
|
|
{
|
|
key: 'bandLock',
|
|
path: '/band-lock',
|
|
state: 'ok',
|
|
status: 200,
|
|
data: { enabled: true, mode: 'auto', support_bands: [1, 3, 41] },
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
};
|
|
renderWithPrivacy(<SimModule instance={instance} reader={reader} />);
|
|
|
|
const band = await screen.findByRole('region', { name: '频段锁定' });
|
|
expect(band.textContent).toContain('已启用');
|
|
expect(band.textContent).toContain('自动');
|
|
expect(band.textContent).toContain('支持频段');
|
|
expect(band.textContent).not.toContain('support bands');
|
|
});
|
|
|
|
it('masks subscriber identifiers until the operator reveals them', async () => {
|
|
const reader = { read: vi.fn(async () => snapshot()) };
|
|
renderWithPrivacy(<SimModule instance={instance} reader={reader} />);
|
|
|
|
expect(await screen.findByText(/^•+ 1234$/u)).toBeTruthy();
|
|
expect(screen.queryByText('89860112345678901234')).toBeNull();
|
|
|
|
const toggle = screen.getByRole('button', { name: '显示敏感标识' });
|
|
toggle.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
|
|
await waitFor(() => expect(screen.getByText('89860112345678901234')).toBeTruthy());
|
|
expect(toggle.getAttribute('aria-pressed')).toBe('true');
|
|
});
|
|
|
|
it('stays idle with a clear message when the device is not authenticated', () => {
|
|
const reader = { read: vi.fn(async () => snapshot()) };
|
|
renderWithPrivacy(
|
|
<SimModule instance={{ ...instance, authentication: 'auth-required' }} reader={reader} />,
|
|
);
|
|
|
|
expect(screen.getByRole('alert').textContent).toContain('需要先完成设备认证');
|
|
expect(reader.read).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows a retry panel instead of a blank tab when the read fails', async () => {
|
|
const reader = {
|
|
read: vi.fn(async () => {
|
|
throw new Error('upstream down');
|
|
}),
|
|
};
|
|
renderWithPrivacy(<SimModule instance={instance} reader={reader} />);
|
|
|
|
const alert = await screen.findByRole('alert');
|
|
expect(alert.textContent).toContain('无法加载');
|
|
expect(screen.getByRole('button', { name: '重试' })).toBeTruthy();
|
|
});
|
|
|
|
it('aggregates WiFi Calling SMS deliveries into China-day counters', async () => {
|
|
// Anchor the fixture to the current China day rather than the test runner's local timezone.
|
|
const chinaDayStartUtc = new Date(Date.now() + 8 * 3_600_000);
|
|
chinaDayStartUtc.setUTCHours(0, 0, 0, 0);
|
|
const chinaDayBase = chinaDayStartUtc.getTime() - 8 * 3_600_000;
|
|
const reader = {
|
|
read: vi.fn(async () =>
|
|
snapshot({
|
|
module: 'vowifi',
|
|
sections: [
|
|
{
|
|
key: 'smsDeliveries',
|
|
path: '/vowifi/sms/delivery?limit=20',
|
|
state: 'ok',
|
|
status: 200,
|
|
data: {
|
|
deliveries: [
|
|
{
|
|
created_at: new Date(chinaDayBase + 8 * 3_600_000).toISOString(),
|
|
direction: 'incoming',
|
|
sender: '10010',
|
|
},
|
|
{
|
|
created_at: new Date(chinaDayBase + 9 * 3_600_000).toISOString(),
|
|
direction: 'outgoing',
|
|
state: 'delivered',
|
|
},
|
|
{
|
|
created_at: new Date(chinaDayBase + 10 * 3_600_000).toISOString(),
|
|
direction: 'outgoing',
|
|
state: 'failed',
|
|
},
|
|
{
|
|
created_at: '2020-01-01T15:59:59.000Z',
|
|
direction: 'outgoing',
|
|
state: 'delivered',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
};
|
|
renderWithPrivacy(<VowifiModule instance={instance} reader={reader} />);
|
|
|
|
const summary = await screen.findByRole('region', { name: '今日短信统计' });
|
|
expect(summary.textContent).toContain('今日接收');
|
|
expect(summary.textContent).toContain('2 条');
|
|
expect(summary.textContent).toContain('今日发送');
|
|
expect(summary.textContent).toContain('2 条');
|
|
expect(summary.textContent).toContain('1 / 2');
|
|
expect(summary.textContent).not.toContain('10010');
|
|
});
|
|
});
|