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