100 lines
7.3 KiB
TypeScript
100 lines
7.3 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import type { AccessVehicleRow, Page } from '../../api/types';
|
|
import { ROUTER_FUTURE } from '../routing/routerConfig';
|
|
import AccessPage from './AccessPage';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
accessSummary: vi.fn(), accessVehicles: vi.fn(), accessUnresolvedIdentities: vi.fn(),
|
|
accessThresholds: vi.fn(), updateAccessThresholds: vi.fn()
|
|
}));
|
|
const auth = vi.hoisted(() => ({ role: 'admin' }));
|
|
vi.mock('../../api/client', () => ({ api: mocks }));
|
|
vi.mock('../auth/AuthGate', () => ({ usePlatformSession: () => ({ session: { role: auth.role } }) }));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
auth.role = 'admin';
|
|
Object.values(mocks).forEach((mock) => mock.mockReset());
|
|
});
|
|
|
|
function accessRow(vin: string, plate: string): AccessVehicleRow {
|
|
return {
|
|
vin, plate, oem: '测试品牌', model: '测试车型', company: '测试公司', protocol: 'JT808', provider: '测试厂家', source: 'production',
|
|
firstSeenAt: '2026-07-16T00:00:00Z', latestEventAt: '2026-07-16T04:00:00Z', latestReceivedAt: '2026-07-16T04:00:01Z', reportIntervalSec: 10,
|
|
dataDelaySec: 1, freshnessSec: 3, onlineState: 'online', thresholdSec: 300, latestMessageType: 'location', latestEventId: `${vin}-event`, latestError: '', delayAbnormal: false,
|
|
firstSeenEvidence: 'test first seen', firstSeenSource: 'test', reportIntervalEvidence: 'test interval', reportSampleCount: 10,
|
|
expectedProtocols: ['JT808'], actualProtocols: ['JT808'], missingProtocols: [], connectionState: 'healthy', expectationEvidence: 'test expectation',
|
|
protocolStatuses: [{ protocol: 'JT808', expected: true, connected: true, provider: '测试厂家', firstSeenAt: '2026-07-16T00:00:00Z', latestEventAt: '2026-07-16T04:00:00Z', latestReceivedAt: '2026-07-16T04:00:01Z', reportIntervalSec: 10, dataDelaySec: 1, freshnessSec: 3, onlineState: 'online', thresholdSec: 300, delayAbnormal: false, firstSeenEvidence: 'test first seen', reportIntervalEvidence: 'test interval' }]
|
|
};
|
|
}
|
|
|
|
function prepareBaseData() {
|
|
mocks.accessSummary.mockResolvedValue({ totalVehicles: 2, onlineVehicles: 2, offlineVehicles: 0, longOfflineVehicles: 0, neverReported: 0, unknownVehicles: 0, delayAbnormal: 0, reportedToday: 2, onlineRate: 100, healthyVehicles: 2, incompleteVehicles: 0, degradedVehicles: 0, protocols: [], oems: [{ name: '测试品牌', total: 2, online: 2, onlineRate: 100 }], asOf: '2026-07-16T04:00:00Z', thresholdVersion: 1 });
|
|
mocks.accessUnresolvedIdentities.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
|
|
mocks.accessThresholds.mockResolvedValue({ version: 1, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: 'test', updatedAt: '2026-07-16T04:00:00Z', audit: [] });
|
|
}
|
|
|
|
test('removes old access rows immediately when the vehicle filter scope changes', async () => {
|
|
prepareBaseData();
|
|
let resolveNew!: (value: Page<AccessVehicleRow>) => void;
|
|
mocks.accessVehicles.mockImplementation((query: { keyword?: string }) => query.keyword === 'OLDVIN'
|
|
? Promise.resolve({ items: [accessRow('OLDVIN', '旧接入车牌')], total: 1, limit: 50, offset: 0 })
|
|
: new Promise<Page<AccessVehicleRow>>((resolve) => { resolveNew = resolve; }));
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?keyword=OLDVIN']}><AccessPage /></MemoryRouter></QueryClientProvider>);
|
|
|
|
expect(await screen.findByText('旧接入车牌')).toBeInTheDocument();
|
|
fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
|
|
|
expect(await screen.findByText('正在更新车辆接入状态…')).toBeInTheDocument();
|
|
expect(screen.queryByText('旧接入车牌')).not.toBeInTheDocument();
|
|
|
|
await act(async () => resolveNew({ items: [accessRow('NEWVIN', '新接入车牌')], total: 1, limit: 50, offset: 0 }));
|
|
expect(await screen.findByText('新接入车牌')).toBeInTheDocument();
|
|
await waitFor(() => expect(screen.queryByText('正在更新车辆接入状态…')).not.toBeInTheDocument());
|
|
});
|
|
|
|
test('reports and independently retries unresolved identity and threshold failures', async () => {
|
|
prepareBaseData();
|
|
mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 });
|
|
mocks.accessUnresolvedIdentities.mockRejectedValueOnce(new Error('待绑定身份服务超时')).mockResolvedValueOnce({ items: [{ id: 'identity-1', identifierMasked: '138****0001', protocol: 'JT808', plate: '粤A待核', latestSeenAt: '2026-07-16T04:00:00Z', recommendedAction: '核对 VIN' }], total: 1, limit: 20, offset: 0 });
|
|
mocks.accessThresholds.mockRejectedValueOnce(new Error('阈值配置读取失败')).mockResolvedValueOnce({ version: 1, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: 'test', updatedAt: '2026-07-16T04:00:00Z', audit: [] });
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><AccessPage /></MemoryRouter></QueryClientProvider>);
|
|
|
|
const identityError = await screen.findByText('待绑定身份服务超时');
|
|
const thresholdError = await screen.findByText('阈值配置读取失败');
|
|
fireEvent.click(within(identityError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ }));
|
|
fireEvent.click(within(thresholdError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ }));
|
|
|
|
expect(await screen.findByText('另有 1 条来源身份待绑定')).toBeInTheDocument();
|
|
expect(await screen.findByText('在线判定阈值 · v1')).toBeInTheDocument();
|
|
expect(screen.queryByText('待绑定身份服务超时')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('阈值配置读取失败')).not.toBeInTheDocument();
|
|
expect(mocks.accessUnresolvedIdentities).toHaveBeenCalledTimes(2);
|
|
expect(mocks.accessThresholds).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test('does not request or render admin-only thresholds for a read-only session', async () => {
|
|
auth.role = 'viewer';
|
|
prepareBaseData();
|
|
mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 });
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><AccessPage /></MemoryRouter></QueryClientProvider>);
|
|
|
|
expect(await screen.findByText('粤A00001')).toBeInTheDocument();
|
|
expect(mocks.accessUnresolvedIdentities).not.toHaveBeenCalled();
|
|
expect(mocks.accessThresholds).not.toHaveBeenCalled();
|
|
expect(screen.queryByText(/在线判定阈值/)).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /刷新/ }));
|
|
await waitFor(() => expect(mocks.accessSummary).toHaveBeenCalledTimes(2));
|
|
expect(mocks.accessUnresolvedIdentities).not.toHaveBeenCalled();
|
|
expect(mocks.accessThresholds).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
});
|