Files
multi-simadmin/apps/web/src/settings/connection-settings-panel.test.tsx
T

156 lines
6.5 KiB
TypeScript

// @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> = {}): 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<SystemMaintenanceDataSource> = {}) {
const data = source(overrides);
const view = render(<ConnectionSettingsPanel dataSource={data} />);
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('无法读取连接设置,请刷新页面重试。');
});
});