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' })); const layout = vi.hoisted(() => ({ mobile: false })); vi.mock('../../api/client', () => ({ api: mocks })); vi.mock('../auth/AuthGate', () => ({ usePlatformSession: () => ({ session: { role: auth.role } }) })); vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile })); afterEach(() => { cleanup(); auth.role = 'admin'; layout.mobile = false; 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: [], actualProtocols: ['JT808'], missingProtocols: [], masterDataIssues: [], connectionState: 'healthy', expectationEvidence: '尚未接业务口径', protocolStatuses: [{ protocol: 'JT808', expected: false, 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, masterDataIncompleteVehicles: 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) => void; mocks.accessVehicles.mockImplementation((query: { keyword?: string }) => query.keyword === 'OLDVIN' ? Promise.resolve({ items: [accessRow('OLDVIN', '旧接入车牌')], total: 1, limit: 50, offset: 0 }) : new Promise>((resolve) => { resolveNew = resolve; })); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); expect(await screen.findByText('旧接入车牌')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '真实来源与接入健康', level: 5 })).toBeInTheDocument(); expect(screen.getByLabelText('接入管理操作')).toHaveClass('v2-workspace-command-bar', 'v2-access-command-bar'); for (const className of ['v2-access-filter-card-v3', 'v2-access-kpis-card-v3', 'v2-access-table-v3']) { expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument(); } expect(view.container.querySelector('.v2-access-semi-table.semi-table-wrapper')).toBeInTheDocument(); expect(view.container.querySelector('.v2-access-table-scroll-v3 > table')).not.toBeInTheDocument(); const desktopRow = screen.getByTestId('access-row-OLDVIN'); expect(desktopRow).toHaveAttribute('role', 'button'); expect(desktopRow).toHaveAttribute('aria-expanded', 'false'); fireEvent.keyDown(desktopRow, { key: 'Enter' }); expect(await screen.findByRole('button', { name: '关闭车辆接入详情' })).toBeInTheDocument(); expect(desktopRow).toHaveAttribute('aria-expanded', 'true'); const inspectorHeader = screen.getByRole('heading', { level: 5, name: '旧接入车牌' }).closest('.v2-workspace-panel-header'); expect(inspectorHeader).toBeInTheDocument(); expect(within(inspectorHeader!).getByText('OLDVIN')).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(); fireEvent.click(await screen.findByRole('button', { name: /接入治理/ })); expect(await screen.findByRole('dialog', { name: '接入治理配置' })).toBeInTheDocument(); 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: /重试/ })); const identityTitle = await screen.findByText('来源身份待绑定'); const identityCollapse = identityTitle.closest('.semi-collapse'); expect(identityCollapse).toHaveClass('v2-access-identity-queue-v3'); expect(within(identityCollapse!).getByText('1 条').closest('.semi-tag')).toBeTruthy(); expect(within(identityCollapse!).getByText('138****0001').closest('.semi-card')).toHaveClass('v2-access-identity-card'); const thresholdTitle = await screen.findByText('在线判定阈值'); const thresholdCollapse = thresholdTitle.closest('.semi-collapse'); expect(thresholdCollapse).toHaveClass('v2-access-settings'); expect(within(thresholdCollapse!).getByText('v1').closest('.semi-tag')).toBeTruthy(); 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(); 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(); }); test('renders mobile access vehicles as selectable Semi cards', async () => { layout.mobile = true; prepareBaseData(); mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const action = await screen.findByRole('button', { name: '查看 粤A00001 接入详情' }); expect(action).toHaveClass('semi-button', 'v2-access-mobile-action'); expect(action.closest('.semi-card')).toHaveClass('v2-access-mobile-card'); expect(action).toHaveAttribute('aria-pressed', 'false'); expect(action).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(action); expect(action).toHaveAttribute('aria-pressed', 'true'); expect(action).toHaveAttribute('aria-expanded', 'true'); const dialog = await screen.findByRole('dialog', { name: '车辆接入详情' }); expect(within(dialog).getByRole('button', { name: '关闭车辆接入详情' })).toBeInTheDocument(); expect(dialog.querySelector('.v2-access-inspector-v3.semi-card')).toBeInTheDocument(); expect(dialog.querySelector('.v2-access-inspector-summary.semi-descriptions')).toBeInTheDocument(); expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument(); expect(dialog.querySelectorAll('.v2-access-protocol-detail.semi-card')).toHaveLength(3); });