// @vitest-environment jsdom import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { ConnectionSettingsPanel } from './connection-settings-panel.js'; import type { SystemMaintenanceDataSource } from './system-maintenance-api-data-source.js'; afterEach(cleanup); const defaults = { heartbeatSeconds: 30, offlineSeconds: 90, authorizationMode: 'auto' }; function source(overrides: Partial = {}): SystemMaintenanceDataSource { return { connection: vi.fn(async () => defaults), updateConnection: vi.fn(async (settings) => settings), refreshHeartbeat: vi.fn(async () => ({ probed: 7, failed: 2, startedAt: '2026-09-05T00:00:00.000Z', finishedAt: '2026-09-05T00:00:01.000Z', })), ...overrides, } as unknown as SystemMaintenanceDataSource; } async function mount(overrides: Partial = {}) { const data = source(overrides); const view = render(); const region = await screen.findByRole('region', { name: '连接设置' }); if (!overrides.connection) { await waitFor(() => { expect(within(region).getByLabelText('心跳间隔(秒)').value).toBe('30'); }); } return { ...view, data, region }; } function field(region: HTMLElement, label: string): HTMLInputElement { const node = within(region).getByLabelText(label) as HTMLInputElement; return node; } function modeButton(region: HTMLElement, label: string): HTMLButtonElement { return within(region).getByRole('button', { name: label }) as HTMLButtonElement; } describe('ConnectionSettingsPanel', () => { it('loads the saved cadence into the inputs', async () => { const { data, region } = await mount(); expect(data.connection).toHaveBeenCalledTimes(1); expect(field(region, '心跳间隔(秒)').value).toBe('30'); expect(field(region, '离线判定(秒)').value).toBe('90'); }); it('starts new-device authorization in automatic mode', async () => { const { region } = await mount(); expect(modeButton(region, '自动授权').getAttribute('aria-pressed')).toBe('true'); expect(modeButton(region, '人工确认').getAttribute('aria-pressed')).toBe('false'); }); it('saves a selected manual authorization mode and echoes it', async () => { const { data, region } = await mount(); await userEvent.click(modeButton(region, '人工确认')); const save = within(region).getByRole('button', { name: '保存设置' }) as HTMLButtonElement; expect(save.disabled).toBe(false); await userEvent.click(save); expect(data.updateConnection).toHaveBeenCalledWith({ heartbeatSeconds: 30, offlineSeconds: 90, authorizationMode: 'manual', }); const notice = await within(region).findByRole('status'); expect(notice.textContent).toContain('新设备人工确认'); }); it('keeps save disabled until a valid change is made', async () => { const { region } = await mount(); const save = within(region).getByRole('button', { name: '保存设置' }) as HTMLButtonElement; expect(save.disabled).toBe(true); await userEvent.clear(field(region, '心跳间隔(秒)')); await userEvent.type(field(region, '心跳间隔(秒)'), '4'); expect(save.disabled).toBe(true); await userEvent.clear(field(region, '心跳间隔(秒)')); await userEvent.type(field(region, '心跳间隔(秒)'), '45'); expect(save.disabled).toBe(false); }); it('sends whole numbers and echoes the saved window', async () => { const { data, region } = await mount(); await userEvent.clear(field(region, '心跳间隔(秒)')); await userEvent.type(field(region, '心跳间隔(秒)'), '45'); await userEvent.clear(field(region, '离线判定(秒)')); await userEvent.type(field(region, '离线判定(秒)'), '180'); await userEvent.click(within(region).getByRole('button', { name: '保存设置' })); expect(data.updateConnection).toHaveBeenCalledWith({ heartbeatSeconds: 45, offlineSeconds: 180, authorizationMode: 'auto', }); const notice = await within(region).findByRole('status'); expect(notice.textContent).toContain( '连接设置已保存,新设备自动授权,心跳 45 秒,离线判定 180 秒。', ); }); it('blocks a save when the offline window drops below twice the heartbeat', async () => { const { data, region } = await mount(); await userEvent.clear(field(region, '离线判定(秒)')); await userEvent.type(field(region, '离线判定(秒)'), '59'); const save = within(region).getByRole('button', { name: '保存设置' }) as HTMLButtonElement; expect(save.disabled).toBe(true); expect(data.updateConnection).not.toHaveBeenCalled(); }); it('runs an on-demand beat and reports the offline devices', async () => { const { data, region } = await mount(); await userEvent.click(within(region).getByRole('button', { name: '立即刷新设备状态' })); expect(data.refreshHeartbeat).toHaveBeenCalledTimes(1); const notice = await within(region).findByRole('status'); expect(notice.textContent).toContain('已检查 9 台设备,2 台无响应。'); }); it('surfaces a failed save without losing the form', async () => { const { region } = await mount({ updateConnection: vi.fn(async () => { throw new Error('offline'); }), }); await userEvent.clear(field(region, '心跳间隔(秒)')); await userEvent.type(field(region, '心跳间隔(秒)'), '60'); await userEvent.clear(field(region, '离线判定(秒)')); await userEvent.type(field(region, '离线判定(秒)'), '150'); await userEvent.click(within(region).getByRole('button', { name: '保存设置' })); const problem = await within(region).findByRole('alert'); expect(problem.textContent).toContain('连接设置未保存,请检查各项设置的取值范围。'); expect(field(region, '心跳间隔(秒)').value).toBe('60'); }); it('explains itself when the cadence cannot be read', async () => { const { region } = await mount({ connection: vi.fn(async () => { throw new Error('offline'); }), }); const problem = await within(region).findByRole('alert'); expect(problem.textContent).toContain('无法读取连接设置,请刷新页面重试。'); }); });