// @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({ui}); } const instance: InstanceContext = { id: 'owner', name: 'Owner modem', origin: 'https://node.internal', status: 'online', authentication: 'authenticated', freshness: 'fresh', }; function snapshot(overrides: Partial = {}): 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(); 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(); 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(); 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(); 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(); 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( , ); 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(); 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(); 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'); }); });