// @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 { 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 { render(); 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(); 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(); 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(); 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(); 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(); 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(); }); });