Render the Hub feature set in the existing Animal Island styling: fleet messages, notification centre, log centre, organisation panel, per-instance device module panels and the settings backup, connection and maintenance surfaces.
165 lines
6.3 KiB
TypeScript
165 lines
6.3 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 { DeviceDiscoveryPanel, type DiscoveryDataSource } from './device-discovery-panel.js';
|
|
import type { DeviceProbe, DiscoverySession } from './discovery-api-data-source.js';
|
|
|
|
afterEach(cleanup);
|
|
|
|
const completed: DiscoverySession = {
|
|
sessionId: '7a1f0c22-0000-4000-8000-000000000001',
|
|
status: 'completed',
|
|
createdAt: '2026-09-05T02:00:00.000Z',
|
|
expiresAt: '2026-09-05T02:01:00.000Z',
|
|
scanned: 508,
|
|
total: 508,
|
|
ranges: ['192.168.68.0/24'],
|
|
devices: [
|
|
{
|
|
origin: 'http://192.168.68.1:3000',
|
|
address: '192.168.68.1',
|
|
port: 3000,
|
|
secure: false,
|
|
httpStatus: 200,
|
|
identity: { model: 'iPhone 14', firmware_version: '16.1' },
|
|
knownInstanceId: null,
|
|
},
|
|
{
|
|
origin: 'http://192.168.68.2:3000',
|
|
address: '192.168.68.2',
|
|
port: 3000,
|
|
secure: false,
|
|
httpStatus: 401,
|
|
identity: {},
|
|
knownInstanceId: null,
|
|
},
|
|
{
|
|
origin: 'http://192.168.68.3:3000',
|
|
address: '192.168.68.3',
|
|
port: 3000,
|
|
secure: false,
|
|
httpStatus: 200,
|
|
identity: { brand: 'Google' },
|
|
knownInstanceId: 'inst-9',
|
|
},
|
|
],
|
|
};
|
|
|
|
const reachable: DeviceProbe = {
|
|
origin: 'http://192.168.68.9:3000',
|
|
reachable: true,
|
|
httpStatus: 200,
|
|
identity: { model: 'Pixel 7' },
|
|
knownInstanceId: null,
|
|
};
|
|
|
|
function source(overrides: Partial<DiscoveryDataSource> = {}): DiscoveryDataSource {
|
|
return {
|
|
start: vi.fn(async () => completed),
|
|
status: vi.fn(async () => completed),
|
|
renew: vi.fn(async () => completed),
|
|
stop: vi.fn(async () => undefined),
|
|
probe: vi.fn(async () => reachable),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function mount(dataSource: DiscoveryDataSource): Promise<HTMLElement> {
|
|
render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={vi.fn()} />);
|
|
return screen.findByRole('region', { name: '局域网设备发现' });
|
|
}
|
|
|
|
describe('DeviceDiscoveryPanel', () => {
|
|
it('shows discovered devices with a status tag per row', async () => {
|
|
const region = await mount(source());
|
|
const table = await within(region).findByRole('table', { name: '发现的设备' });
|
|
expect(within(table).getByText('http://192.168.68.1:3000')).toBeTruthy();
|
|
expect(within(table).getByText('型号 iPhone 14 · 固件版本 16.1')).toBeTruthy();
|
|
expect(within(table).getByText('未公开身份信息')).toBeTruthy();
|
|
expect(within(table).getAllByText('可接入')).toHaveLength(1);
|
|
expect(within(table).getByText('需要登录')).toBeTruthy();
|
|
expect(within(table).getByText('已接入')).toBeTruthy();
|
|
expect(within(region).getByText(/扫描完成 · 已检查 508 个地址 · 发现 3 台设备/)).toBeTruthy();
|
|
});
|
|
|
|
it('hands the chosen device to the parent and locks a device already added', async () => {
|
|
const onSelect = vi.fn();
|
|
const dataSource = source();
|
|
render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={onSelect} />);
|
|
const table = await screen.findByRole('table', { name: '发现的设备' });
|
|
|
|
const rows = within(table).getAllByRole('button', { name: '接入' });
|
|
expect(rows).toHaveLength(3);
|
|
expect((rows[2] as HTMLButtonElement).disabled).toBe(true);
|
|
await userEvent.click(rows[0] as HTMLButtonElement);
|
|
expect(onSelect).toHaveBeenCalledWith({
|
|
origin: 'http://192.168.68.1:3000',
|
|
name: 'iPhone 14',
|
|
});
|
|
});
|
|
|
|
it('restarts the scan and stops the lease when unmounted', async () => {
|
|
const dataSource = source();
|
|
const { unmount } = render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={vi.fn()} />);
|
|
await screen.findByRole('table', { name: '发现的设备' });
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: '重新扫描' }));
|
|
await waitFor(() => expect(dataSource.start).toHaveBeenCalledTimes(2));
|
|
|
|
unmount();
|
|
await waitFor(() => expect(dataSource.stop).toHaveBeenCalledWith(completed.sessionId));
|
|
});
|
|
|
|
it('probes a manual address and offers to fill the form', async () => {
|
|
const onSelect = vi.fn();
|
|
const dataSource = source();
|
|
render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={onSelect} />);
|
|
const region = await screen.findByRole('region', { name: '局域网设备发现' });
|
|
|
|
const field = within(region).getByLabelText('设备地址');
|
|
expect((field as HTMLInputElement).disabled).toBe(false);
|
|
await userEvent.type(field, '192.168.68.9:3000');
|
|
await userEvent.click(within(region).getByRole('button', { name: '检测地址' }));
|
|
|
|
expect(dataSource.probe).toHaveBeenCalledWith('192.168.68.9:3000');
|
|
await userEvent.click(await within(region).findByRole('button', { name: '填入添加表单' }));
|
|
expect(onSelect).toHaveBeenCalledWith({
|
|
origin: 'http://192.168.68.9:3000',
|
|
name: 'Pixel 7',
|
|
});
|
|
});
|
|
|
|
it('reports an unusable address without throwing', async () => {
|
|
const dataSource = source({
|
|
probe: vi.fn(async () => {
|
|
throw new Error('Discovery request failed.');
|
|
}),
|
|
});
|
|
render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={vi.fn()} />);
|
|
const region = await screen.findByRole('region', { name: '局域网设备发现' });
|
|
|
|
await userEvent.type(within(region).getByLabelText('设备地址'), 'http://example.com');
|
|
await userEvent.click(within(region).getByRole('button', { name: '检测地址' }));
|
|
expect(await within(region).findByText(/设备地址无法识别/)).toBeTruthy();
|
|
});
|
|
|
|
it('falls back to manual entry when discovery cannot start', async () => {
|
|
const dataSource = source({
|
|
start: vi.fn(async () => {
|
|
throw new Error('Discovery request failed.');
|
|
}),
|
|
});
|
|
render(<DeviceDiscoveryPanel dataSource={dataSource} onSelect={vi.fn()} />);
|
|
const region = await screen.findByRole('region', { name: '局域网设备发现' });
|
|
|
|
expect(await within(region).findByRole('alert')).toBeTruthy();
|
|
expect(within(region).getByRole('alert').textContent).toBe(
|
|
'无法启动局域网发现,请改用设备地址接入。',
|
|
);
|
|
expect(within(region).queryByRole('table', { name: '发现的设备' })).toBeNull();
|
|
expect(within(region).getByLabelText('设备地址')).toBeTruthy();
|
|
});
|
|
});
|