// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AppShell, type InstanceContext } from './app-shell.js'; import type { AuditDataSource } from './audit/audit-page.js'; import type { EventStreamClient } from './events/event-stream-client.js'; import { type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js'; import type { FleetMessagesDataSource } from './fleet/fleet-messages-api-data-source.js'; import type { InstanceDataSource } from './instances/instance-api-data-source.js'; import type { JobsDataSource } from './jobs/jobs-page.js'; afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); const snapshot: FleetSnapshot = { instances: [ { id: 'bravo', name: 'Bravo', url: 'http://user:***@bravo.example:8080/private', tags: ['west'], }, { id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin' }, ], statuses: new Map([ [ 'bravo', { reachable: true, authenticated: false, latencyMs: 40, summary: { version: '2.0', capabilities: ['sms'], freshness: 'stale', anomalies: ['clock drift'], resources: { cpuPercent: 18.4, memoryPercent: 63.2, maxTemperatureCelsius: 46.7, phoneNumbers: ['13800138000'], }, }, }, ], [ 'alpha', { reachable: true, authenticated: true, latencyMs: 10, summary: { version: '1.0', capabilities: ['ussd'], freshness: 'fresh' }, }, ], ]), }; function source(load: FleetDataSource['load']): FleetDataSource { return { load }; } const quietEventStreamClient: EventStreamClient = { subscribe: () => () => undefined, }; const emptyPage = { items: [], page: { page: 1, pageSize: 25, total: 0 } }; describe('React AppShell and Fleet vertical slice', () => { it('loads real injected data and renders canonical origins and owner routes without React key warnings', async () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); let resolve!: (value: FleetSnapshot) => void; const dataSource = source(() => new Promise((done) => (resolve = done))); render(); expect(screen.getByRole('status', { name: '实例加载状态' }).textContent).toContain( '正在加载实例', ); resolve(snapshot); const alpha = await screen.findByRole('row', { name: /Alpha/ }); expect(within(alpha).getByRole('link', { name: 'Alpha' }).getAttribute('href')).toBe( '/instances/alpha/overview', ); expect( within(screen.getByRole('row', { name: /Bravo/ })) .getByRole('link', { name: '打开 Bravo 的源站' }) .getAttribute('href'), ).toBe('http://bravo.example:8080'); expect(screen.getByText('版本 0.1.0')).toBeTruthy(); expect( consoleError.mock.calls.some((call) => call.some((argument) => String(argument).includes('unique "key" prop')), ), ).toBe(false); }); it('makes cards, add, detail, edit, and safe deletion available from the overview', async () => { const user = userEvent.setup(); const remove = vi.fn().mockResolvedValue(undefined); const instanceDataSource: InstanceDataSource = { get: vi.fn().mockResolvedValue({ id: 'bravo', name: 'Bravo', origin: 'http://bravo.example:8080', tags: ['west'], revision: 7, credentialConfigured: true, }), create: vi.fn(), update: vi.fn(), testConnection: vi.fn(), delete: remove, }; const fleetMessagesDataSource: FleetMessagesDataSource = { load: vi.fn(async (instanceId) => instanceId === 'bravo' ? { latest: { id: 'sms-1', direction: 'incoming', phoneNumber: '13900139000', content: '这是一条用于聚合页展示的最新短信内容', timestamp: '2026-07-19T08:30:00.000Z', }, } : {}, ), }; render( snapshot)} fleetMessagesDataSource={fleetMessagesDataSource} instanceDataSource={instanceDataSource} />, ); expect(await screen.findByRole('region', { name: '实例卡片' })).toBeTruthy(); expect(screen.getByRole('link', { name: '添加实例' }).getAttribute('href')).toBe( '/instances/new', ); const card = screen.getByRole('article', { name: 'Bravo 实例概览' }); expect(within(card).queryByText('延迟')).toBeNull(); expect(within(card).queryByText('版本')).toBeNull(); expect(within(card).queryByText('新鲜度')).toBeNull(); expect(within(card).queryByText('40 ms')).toBeNull(); expect(within(card).queryByText('2.0')).toBeNull(); expect(within(card).queryByText('可能过期')).toBeNull(); expect(within(card).getByText('18.4%')).toBeTruthy(); expect(within(card).getByText('63.2%')).toBeTruthy(); expect(within(card).getByText('46.7 °C')).toBeTruthy(); expect(within(card).getByText('13800138000')).toBeTruthy(); expect(await within(card).findByText('收到')).toBeTruthy(); expect(within(card).getByText('13900139000')).toBeTruthy(); expect(within(card).getByText(/这是一条用于聚合页展示/)).toBeTruthy(); expect(within(card).getByText(/2026/)).toBeTruthy(); expect(within(card).queryByText('能力未知')).toBeNull(); expect( within(card).getByRole('link', { name: '打开 Bravo 实例仪表盘' }).getAttribute('href'), ).toBe('/instances/bravo/overview'); expect(within(card).queryByRole('link', { name: /查看.*短信/ })).toBeNull(); expect(within(card).getByRole('link', { name: '编辑 Bravo' }).getAttribute('href')).toBe( '/settings/instances/bravo', ); await user.click(within(card).getByRole('button', { name: '删除 Bravo' })); expect(within(card).getByText('删除后无法撤销。')).toBeTruthy(); await user.type(within(card).getByRole('textbox', { name: '输入 bravo 以确认删除' }), 'bravo'); await user.click(within(card).getByRole('button', { name: '确认删除 Bravo' })); expect(remove).toHaveBeenCalledWith('bravo', 7); expect(within(card).getByRole('status').textContent).toContain('删除请求已提交'); }); it('keeps message read failures separate from instance reachability and reports each once', async () => { const messages: FleetMessagesDataSource = { load: vi.fn().mockRejectedValue(new Error('no')) }; render( snapshot)} fleetMessagesDataSource={messages} />, ); const bravo = await screen.findByRole('article', { name: 'Bravo 实例概览' }); expect(await within(bravo).findAllByText('短信暂不可用')).toHaveLength(1); expect(within(bravo).getByText('需要认证')).toBeTruthy(); }); it('keeps Jobs and Audit routes compatible without advertising them in global navigation', async () => { const jobsDataSource: JobsDataSource = { load: vi.fn().mockResolvedValue(emptyPage) }; const { rerender } = render( , ); expect(await screen.findByText(/没有任务符合当前查询/i)).toBeTruthy(); const navigation = screen.getByRole('navigation', { name: '全局导航' }); expect(within(navigation).queryByRole('link', { name: '任务' })).toBeNull(); expect(within(navigation).queryByRole('link', { name: '审计' })).toBeNull(); expect(within(navigation).getByRole('link', { name: '实例总览' })).toBeTruthy(); expect(within(navigation).getByRole('link', { name: '设置' })).toBeTruthy(); const auditDataSource: AuditDataSource = { load: vi.fn().mockResolvedValue(emptyPage) }; rerender( , ); expect(await screen.findByText(/没有审计事件符合当前查询/i)).toBeTruthy(); }); it('loads route-owned instance context so overview-card navigation opens detail management', async () => { const instanceDataSource: InstanceDataSource = { get: vi.fn().mockResolvedValue({ id: 'bravo', name: 'Bravo', origin: 'http://bravo.example:8080', tags: ['west'], revision: 7, credentialConfigured: true, }), create: vi.fn(), update: vi.fn(), testConnection: vi.fn(), delete: vi.fn(), }; render( , ); expect(screen.getByRole('status').textContent).toContain('正在加载实例'); expect(await screen.findByText('Bravo')).toBeTruthy(); expect(screen.getByRole('heading', { name: '实例仪表盘' })).toBeTruthy(); expect(screen.getByRole('link', { name: '编辑实例' }).getAttribute('href')).toBe( '/settings/instances/bravo', ); }); it('supports accessible search, status filtering, sorting, and visible selection', async () => { const user = userEvent.setup(); render( snapshot)} />); await screen.findByRole('row', { name: /Alpha/ }); await user.click(screen.getByRole('button', { name: /按延迟排序/i })); let rows = screen.getAllByRole('row').slice(1); expect(rows[0]?.textContent).toContain('Alpha'); await user.click(screen.getByRole('button', { name: /按延迟排序/i })); rows = screen.getAllByRole('row').slice(1); expect(rows[0]?.textContent).toContain('Bravo'); await user.selectOptions(screen.getByRole('combobox', { name: '状态' }), 'auth'); expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull(); await user.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' })); expect((screen.getByRole('checkbox', { name: '选择 Bravo' }) as HTMLInputElement).checked).toBe( true, ); expect(screen.getByText('已选择 1 项')).toBeTruthy(); await user.type(screen.getByRole('searchbox', { name: '搜索实例' }), 'missing'); expect(screen.getByText(/没有实例符合搜索与筛选条件/i)).toBeTruthy(); }); it('exposes error, retry, configured-empty, and unsafe-origin states without claiming a backend', async () => { const user = userEvent.setup(); const load = vi .fn() .mockRejectedValueOnce(new Error('collector unavailable')) .mockResolvedValueOnce({ instances: [{ id: 'unsafe', name: 'Unsafe', url: 'javascript:alert(1)' }], statuses: new Map(), }); const first = render(); expect((await screen.findByRole('alert')).textContent).toContain('实例加载失败'); await user.click(screen.getByRole('button', { name: '重试加载实例' })); expect(await screen.findByRole('row', { name: /Unsafe/ })).toBeTruthy(); expect(screen.queryByRole('link', { name: '打开 Unsafe 的源站' })).toBeNull(); first.unmount(); const { unmount } = render( ({ instances: [], statuses: new Map() }))} />, ); expect(await screen.findByText(/尚未配置实例/i)).toBeTruthy(); unmount(); }); it('supports metadata filters, column visibility, pagination, and a batch-action entry', async () => { const user = userEvent.setup(); render( snapshot)} />); await screen.findByRole('row', { name: /Alpha/ }); await user.selectOptions(screen.getByRole('combobox', { name: '能力' }), 'sms'); expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull(); expect(screen.getByRole('row', { name: /Bravo/ }).textContent).toContain('clock drift'); await user.click(screen.getByRole('button', { name: '选择列' })); await user.click(screen.getByRole('checkbox', { name: '显示异常列' })); expect(screen.queryByRole('columnheader', { name: '异常' })).toBeNull(); await user.click(screen.getByRole('checkbox', { name: '选择 Bravo' })); expect((screen.getByRole('button', { name: '批量操作' }) as HTMLButtonElement).disabled).toBe( false, ); await user.click(screen.getByRole('button', { name: '批量操作' })); expect(screen.getByText('请为已选择的 1 项选择操作。')).toBeTruthy(); }); it('paginates fleet rows and selects only the current page', async () => { const many: FleetSnapshot = { instances: Array.from({ length: 11 }, (_, index) => ({ id: `instance-${String(index + 1).padStart(2, '0')}`, name: `Instance ${String(index + 1).padStart(2, '0')}`, url: `https://instance-${index + 1}.example`, })), statuses: new Map(), }; render( many)} />); await screen.findByRole('row', { name: /Instance 01/ }); expect(screen.queryByRole('row', { name: /Instance 11/ })).toBeNull(); expect(screen.getByText('第 1 页,共 2 页')).toBeTruthy(); fireEvent.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' })); expect(screen.getByText('已选择 10 项')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '下一页' })); expect(await screen.findByRole('row', { name: /Instance 11/ })).toBeTruthy(); expect(screen.getByText('第 2 页,共 2 页')).toBeTruthy(); }); it('binds instance context to the route owner and never leaks mismatched context', () => { const instance: InstanceContext = { id: 'owner', name: 'Owner modem', origin: 'https://owner.example/path', status: 'online', authentication: 'authenticated', freshness: 'fresh', }; const { rerender } = render( , ); expect(screen.getByRole('heading', { name: '短信管理' })).toBeTruthy(); expect(screen.getByText('Owner modem')).toBeTruthy(); expect(screen.getByRole('link', { name: '打开源站' }).getAttribute('href')).toBe( 'https://owner.example', ); rerender(); expect(screen.queryByText('Owner modem')).toBeNull(); expect(screen.getByRole('status').textContent).toContain('正在加载实例'); }); it.each([ { pathname: '/jobs', endpoint: '/api/v1/jobs?page=1&pageSize=25&sort=createdAt&direction=desc', emptyMessage: /没有任务符合当前查询/i, }, { pathname: '/audit', endpoint: '/api/v1/audit?sort=occurredAt&direction=desc&page=1&pageSize=25', emptyMessage: /没有审计事件符合当前查询/i, }, ])( 'uses the default HTTP data source for $pathname', async ({ pathname, endpoint, emptyMessage }) => { const fetcher = vi.fn().mockResolvedValue( new Response(JSON.stringify(emptyPage), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetcher); render(); expect(await screen.findByText(emptyMessage)).toBeTruthy(); expect(fetcher).toHaveBeenCalledTimes(1); expect(fetcher).toHaveBeenCalledWith(endpoint, { method: 'GET', credentials: 'same-origin', headers: { accept: 'application/json' }, signal: expect.any(AbortSignal), }); }, ); it.each([ { pathname: '/jobs', sourceProp: 'jobsDataSource' as const, emptyMessage: /没有任务符合当前查询/i, }, { pathname: '/audit', sourceProp: 'auditDataSource' as const, emptyMessage: /没有审计事件符合当前查询/i, }, ])( 'uses an explicitly injected data source instead of global fetch for $pathname', async ({ pathname, sourceProp, emptyMessage }) => { const fetcher = vi.fn(); vi.stubGlobal('fetch', fetcher); const load = vi .fn() .mockResolvedValue(emptyPage); const injectedSource = { load }; render( , ); expect(await screen.findByText(emptyMessage)).toBeTruthy(); expect(load).toHaveBeenCalledTimes(1); expect(fetcher).not.toHaveBeenCalled(); }, ); });