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, useLocation } 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(), vehicles: vi.fn(), claimAccessIdentity: 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: [] }); } function AccessLocationProbe() { const location = useLocation(); return {location.pathname}{location.search}; } test('restores filter, pagination and selected vehicle from a shareable access URL', async () => { prepareBaseData(); mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN021', '粤A00021')], total: 45, limit: 20, offset: 20 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const detailDialog = await screen.findByRole('dialog', { name: '车辆接入详情' }); expect(await within(detailDialog).findByText('粤A00021 · VIN021')).toBeInTheDocument(); expect(mocks.accessVehicles).toHaveBeenCalledWith(expect.objectContaining({ keyword: '粤A', limit: 20, offset: 20 }), expect.anything()); expect(screen.getByRole('textbox', { name: '车辆' })).toHaveValue('粤A'); expect(screen.getByTestId('access-location').textContent).toMatch(/accessLimit=20.*accessPage=2.*accessVin=VIN021/); fireEvent.click(within(detailDialog).getByRole('button', { name: '关闭车辆接入详情' })); await waitFor(() => expect(screen.getByTestId('access-location').textContent).toMatch(/accessLimit=20.*accessPage=2/)); expect(screen.getByTestId('access-location')).not.toHaveTextContent('accessVin'); }); test('keeps governance open and confirms the threshold version after saving', async () => { prepareBaseData(); mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 }); mocks.updateAccessThresholds.mockResolvedValue({ version: 2, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: '平台管理员', updatedAt: '2026-07-23T01:00:00Z', audit: [] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(await screen.findByRole('button', { name: /接入治理/ })); const dialog = await screen.findByRole('dialog', { name: '接入治理配置' }); const thresholdTitle = within(dialog).getByText('在线判定阈值'); fireEvent.click(thresholdTitle.closest('.semi-collapse-header')!); fireEvent.click(within(dialog).getByText('保存阈值').closest('button')!); expect(await within(dialog).findByRole('status')).toHaveTextContent('阈值已保存,v2 已生效'); expect(within(dialog).getByText('接入治理工作台')).toBeInTheDocument(); expect(mocks.updateAccessThresholds).toHaveBeenCalledWith(expect.objectContaining({ version: 1, defaultThresholdSec: 300 }), expect.anything()); }); test('turns an unresolved source into a deep-linked, audited vehicle claim and exposes profile follow-up', async () => { prepareBaseData(); const unresolved = { id: 'identity-1', identifierMasked: '138****0001', protocol: 'JT808', plate: '粤A00001', manufacturer: '测试终端', sourceEndpoint: 'gateway-a', firstRegisteredAt: '', latestRegisteredAt: '', latestAuthenticatedAt: '', latestSeenAt: '2026-07-16T04:00:00Z', freshnessSec: 10, issueCode: 'missing_vin_jt808', recommendedAction: '核对 VIN 后绑定' }; mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 }); mocks.accessUnresolvedIdentities.mockReset().mockResolvedValueOnce({ items: [unresolved], total: 1, limit: 20, offset: 0 }).mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 }); mocks.vehicles.mockResolvedValue({ items: [{ vin: 'VIN001', plate: '粤A00001', phone: '', oem: '测试品牌', protocol: 'JT808', online: true, lastSeen: '2026-07-16T04:00:00Z', locationText: '', bindingScore: 100 }], total: 1, limit: 10, offset: 0 }); mocks.claimAccessIdentity.mockResolvedValue({ identityId: 'identity-1', protocol: 'JT808', identifierMasked: '138****0001', vin: 'VIN001', plate: '粤A00001', profileComplete: false, profileMissingFields: ['所属企业', '首次接入'], claimedBy: 'admin-test', claimedAt: '2026-07-23T08:00:00+08:00', auditId: 12 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const governance = await screen.findByRole('dialog', { name: '接入治理配置' }); fireEvent.click(await within(governance).findByRole('button', { name: '认领来源 138****0001' })); expect(await screen.findByRole('dialog', { name: '来源身份认领' })).toBeInTheDocument(); expect(screen.getByTestId('access-location')).toHaveTextContent('accessGovernance=claim'); expect(screen.getByTestId('access-location')).toHaveTextContent('accessIdentity=identity-1'); const candidate = await screen.findByRole('option', { name: '粤A00001 VIN001 JT/T 808 选择' }); fireEvent.click(candidate); const confirmAction = screen.getByRole('button', { name: '确认认领' }); expect(confirmAction).toBeDisabled(); fireEvent.change(screen.getByRole('textbox', { name: '认领说明' }), { target: { value: '设备交付单与车牌核对通过' } }); fireEvent.click(screen.getByRole('checkbox', { name: /我已核对来源证据与目标车辆/ })); expect(confirmAction).toBeEnabled(); fireEvent.click(confirmAction); expect(await screen.findByRole('status', { name: '来源身份认领成功' })).toHaveTextContent('138****0001 已绑定至 粤A00001'); expect(screen.getByText('所属企业、首次接入')).toBeInTheDocument(); expect(screen.getByRole('link', { name: /继续补全车辆档案/ })).toHaveAttribute('href', '/vehicles/VIN001#vehicle-archive-panel'); expect(mocks.claimAccessIdentity).toHaveBeenCalledWith('identity-1', { vin: 'VIN001', note: '设备交付单与车牌核对通过' }); fireEvent.click(screen.getByRole('button', { name: /返回治理/ })); expect(await screen.findByRole('dialog', { name: '接入治理配置' })).toBeInTheDocument(); expect(screen.getByTestId('access-location')).toHaveTextContent('accessGovernance=overview'); expect(screen.getByTestId('access-location')).not.toHaveTextContent('accessIdentity'); }); 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', 'has-identity'); expect(screen.getByLabelText('接入管理操作').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument(); expect(view.container.querySelector('.v2-access-discovery-shell')).toBeInTheDocument(); expect(screen.getByRole('region', { name: '接入筛选' })).toHaveClass('governance-query-bar'); for (const className of ['v2-access-table-v3']) { expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument(); } expect(screen.getByRole('navigation', { name: '接入差异筛选' })).toHaveClass('v2-access-status-tabs'); const accessMetrics = screen.getByRole('list', { name: '接入差异概览' }); expect(accessMetrics.closest('.v2-access-metric-rail')).toHaveClass('governance-status-strip'); expect(within(accessMetrics).getAllByRole('listitem')).toHaveLength(6); expect(within(accessMetrics).getByRole('button', { name: '筛选需关注,共 0 辆' })).toHaveAttribute('aria-pressed', 'false'); expect(within(accessMetrics).getByRole('button', { name: '筛选全部车辆,共 2 辆' })).toHaveAttribute('aria-pressed', 'true'); const protocolCoverage = screen.getByLabelText('真实协议来源概览'); expect(protocolCoverage).toHaveTextContent('真实来源覆盖'); expect(protocolCoverage).toHaveTextContent('32960'); expect(protocolCoverage).toHaveTextContent('808'); expect(protocolCoverage).toHaveTextContent('宇通'); expect(screen.getByRole('heading', { name: '接入差异清单', level: 5 })).toBeInTheDocument(); expect(screen.getByText('当前范围:全部主车辆')).toBeInTheDocument(); const differenceHeader = screen.getByRole('columnheader', { name: '差异结论' }); expect(differenceHeader).toHaveClass('semi-table-cell-fixed-left', 'v2-access-difference-column'); expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument(); expect(screen.queryByText('已接来源状态正常')).not.toBeInTheDocument(); expect(screen.getByText('1 个来源')).toBeInTheDocument(); const filterActions = view.container.querySelector('.v2-access-filter-actions'); expect(filterActions).toBeInTheDocument(); expect(within(filterActions!).getByRole('button', { name: '查询' })).toBeInTheDocument(); expect(within(filterActions!).getByRole('button', { name: '重置' })).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 desktopAccessScroll = view.container.querySelector('.v2-access-table-scroll-v3'); expect(desktopAccessScroll).toHaveAttribute('tabindex', '0'); expect(desktopAccessScroll).toHaveAttribute('aria-label', '车辆接入表格,可使用左右方向键浏览协议来源'); const desktopTableScroller = desktopAccessScroll?.querySelector('.semi-table-body'); expect(desktopTableScroller).toBeInTheDocument(); Object.defineProperty(desktopAccessScroll!, 'clientWidth', { configurable: true, value: 760 }); Object.defineProperty(desktopAccessScroll!, 'scrollWidth', { configurable: true, value: 760 }); const scrollBy = vi.fn(); Object.defineProperty(desktopTableScroller!, 'scrollBy', { configurable: true, value: scrollBy }); fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowRight' }); expect(scrollBy).toHaveBeenCalledWith({ left: 260, behavior: 'smooth' }); fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowLeft' }); expect(scrollBy).toHaveBeenCalledWith({ left: -260, behavior: 'smooth' }); expect(screen.getByRole('columnheader', { name: '车辆' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-vehicle-column'); expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('v2-access-model-column'); expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).not.toHaveClass('semi-table-cell-fixed-left'); const protocolHeaders = [ { label: 'GB/T 32960', description: '国标远程服务' }, { label: 'JT/T 808', description: '道路运输终端' }, { label: '宇通 MQTT', description: '宇通车辆平台' } ]; for (const protocol of protocolHeaders) { const header = screen.getByRole('columnheader', { name: new RegExp(protocol.label.replace('/', '\\/')) }); expect(within(header).getByText(protocol.label).closest('.semi-tag')).toHaveClass('v2-protocol-tag', 'is-compact'); expect(within(header).getByText(protocol.description)).toBeInTheDocument(); } const desktopRow = screen.getByTestId('access-row-OLDVIN'); expect(desktopRow).toHaveClass('is-state-healthy'); expect(desktopRow).toHaveAttribute('role', 'button'); expect(desktopRow).toHaveAttribute('aria-expanded', 'false'); fireEvent.keyDown(desktopRow, { key: 'Enter' }); expect(await screen.findByRole('button', { name: '关闭车辆接入详情' })).toBeInTheDocument(); const detailDialog = screen.getByRole('dialog', { name: '车辆接入详情' }); expect(detailDialog).toBeInTheDocument(); expect(desktopRow).toHaveAttribute('aria-expanded', 'true'); expect(within(detailDialog).getByText('旧接入车牌 · OLDVIN')).toBeInTheDocument(); expect(within(detailDialog).getByText('接入结论')).toBeInTheDocument(); expect(within(detailDialog).getByText('已接来源状态正常')).toBeInTheDocument(); expect(within(detailDialog).getByText('处置建议 · 无需处理,持续监测来源上报')).toBeInTheDocument(); const healthOverview = within(detailDialog).getByRole('group', { name: '接入健康概览' }); expect(healthOverview).toHaveTextContent('已接入1 / 3'); expect(healthOverview).toHaveTextContent('当前在线1'); expect(healthOverview).toHaveTextContent('异常来源0'); expect(healthOverview).toHaveTextContent('资料状态已维护'); expect(detailDialog.querySelectorAll('.v2-access-protocol-title .v2-protocol-tag')).toHaveLength(3); expect(within(detailDialog).getByText('GB/T 32960')).toBeInTheDocument(); expect(within(detailDialog).getByText('JT/T 808')).toBeInTheDocument(); expect(within(detailDialog).getByText('宇通 MQTT')).toBeInTheDocument(); expect(detailDialog.querySelector('.v2-access-inspector-header')).not.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(identityTitle.closest('.semi-collapse-header')).toHaveAttribute('aria-expanded', 'true'); expect(within(identityCollapse!).getByText('1 条').closest('.semi-tag')).toBeTruthy(); expect(within(identityCollapse!).getByText('138****0001').closest('.semi-card')).toHaveClass('v2-access-identity-card'); expect(within(identityCollapse!).getByText('来源标识')).toBeInTheDocument(); expect(within(identityCollapse!).getByText('关联车牌')).toBeInTheDocument(); expect(within(identityCollapse!).getByText('最后出现')).toBeInTheDocument(); expect(within(identityCollapse!).getByText('建议动作')).toBeInTheDocument(); 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(thresholdTitle.closest('.semi-collapse-header')).toHaveAttribute('aria-expanded', 'false'); expect(within(thresholdCollapse!).queryByRole('spinbutton')).not.toBeInTheDocument(); fireEvent.click(thresholdTitle.closest('.semi-collapse-header')!); expect(thresholdTitle.closest('.semi-collapse-header')).toHaveAttribute('aria-expanded', 'true'); expect(within(thresholdCollapse!).getAllByRole('spinbutton')).toHaveLength(6); expect(within(thresholdCollapse!).getByText('通用判定')).toBeInTheDocument(); expect(within(thresholdCollapse!).getByText('协议在线阈值')).toBeInTheDocument(); expect(within(thresholdCollapse!).getAllByText('5 分 0 秒').length).toBeGreaterThan(0); expect(within(thresholdCollapse!).getByText('1 分 0 秒')).toBeInTheDocument(); expect(within(thresholdCollapse!).getByText('24 小时')).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(); 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(); const delayedRow = accessRow('VIN001', '粤A00001'); delayedRow.connectionState = 'degraded'; delayedRow.protocolStatuses[0].delayAbnormal = true; delayedRow.protocolStatuses[0].dataDelaySec = 128; mocks.accessVehicles.mockResolvedValue({ items: [delayedRow], total: 1, limit: 50, offset: 0 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); let 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(view.container.querySelector('.v2-access-table-scroll-v3.is-mobile-scroll')).toHaveAttribute('tabindex', '0'); expect(view.container.querySelector('.v2-access-table-scroll-v3.is-mobile-scroll')).toHaveAttribute('aria-label', '车辆接入列表,可上下滚动'); expect(within(action).getByText('32960')).toBeInTheDocument(); expect(within(action).getByText('808')).toBeInTheDocument(); expect(within(action).getByText('宇通')).toBeInTheDocument(); expect(screen.queryByLabelText('每页车辆数')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '打开接入治理' })).toHaveTextContent('治理'); expect(screen.getByRole('button', { name: '打开接入治理' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); const filterTrigger = screen.getByRole('button', { name: /修改接入筛选/ }); expect(filterTrigger).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(filterTrigger); const filterDialog = await screen.findByRole('dialog', { name: '接入车辆筛选' }); expect(document.querySelector('.v2-access-mobile-filter-sidesheet')).toHaveClass('business-sheet-bottom'); expect(document.querySelector('.v2-access-mobile-filter-sidesheet')).toHaveStyle({ height: 'min(76dvh, 640px)' }); expect(within(filterDialog).getByRole('textbox', { name: '车辆' })).toBeInTheDocument(); expect(within(filterDialog).getByRole('combobox', { name: '接入状态' })).toBeInTheDocument(); expect(within(filterDialog).getByRole('combobox', { name: '真实协议' })).toBeInTheDocument(); expect(within(filterDialog).getByRole('combobox', { name: '车辆品牌' })).toBeInTheDocument(); fireEvent.click(within(filterDialog).getByRole('button', { name: '重置条件' })); expect(screen.getByRole('dialog', { name: '接入车辆筛选' })).toBeInTheDocument(); fireEvent.click(within(filterDialog).getByRole('combobox', { name: '接入状态' })); const attentionOption = (await screen.findAllByText('需关注')).find((element) => element.classList.contains('semi-select-option-text')); expect(attentionOption).toBeInTheDocument(); fireEvent.click(attentionOption!); fireEvent.click(within(filterDialog).getByRole('button', { name: '应用并查询' })); await waitFor(() => expect(mocks.accessVehicles).toHaveBeenLastCalledWith(expect.objectContaining({ connectionState: 'attention' }), expect.anything())); expect(screen.getByRole('button', { name: /修改接入筛选:已启用 1 项/ })).toHaveAttribute('aria-expanded', 'false'); expect(screen.queryByRole('dialog', { name: '接入车辆筛选' })).not.toBeInTheDocument(); action = await screen.findByRole('button', { name: '查看 粤A00001 接入详情' }); const toolsButton = screen.getByRole('button', { name: '打开接入管理工具' }); expect(toolsButton).toHaveTextContent('工具'); expect(toolsButton).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); expect(screen.queryByRole('menuitem', { name: /导出当前页/ })).not.toBeInTheDocument(); fireEvent.click(toolsButton); await waitFor(() => expect(toolsButton).toHaveAttribute('aria-expanded', 'true')); expect(screen.getByRole('menuitem', { name: /导出当前页/ })).toBeInTheDocument(); fireEvent.click(screen.getByRole('menuitem', { name: /刷新数据/ })); await waitFor(() => expect(mocks.accessSummary).toHaveBeenCalledTimes(2)); expect(toolsButton).toHaveAttribute('aria-expanded', 'false'); 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-focus.semi-card')).toBeInTheDocument(); expect(within(dialog).getByRole('group', { name: '接入健康概览' })).toBeInTheDocument(); expect(dialog.querySelector('.v2-access-mobile-profile')).toHaveTextContent('车辆档案测试品牌 / 测试车型'); expect(within(dialog).getByText('协议证据')).toBeInTheDocument(); expect(within(dialog).getByText('1 / 3 已接入').closest('.semi-tag')).toBeInTheDocument(); expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument(); expect(dialog.querySelectorAll('.v2-access-protocol-detail.semi-card')).toHaveLength(3); const evidenceToggle = within(dialog).getByRole('button', { name: '收起 JT808 完整证据' }); expect(evidenceToggle).toHaveAttribute('aria-expanded', 'true'); expect(within(dialog).getByRole('region', { name: 'JT808 完整接入证据' })).toBeInTheDocument(); expect(within(dialog).getByText('test first seen')).toBeInTheDocument(); expect(within(dialog).getAllByText('当前未发现真实接入来源')).toHaveLength(2); expect(within(dialog).getByRole('navigation', { name: '粤A00001 数据操作' })).toHaveTextContent('车辆档案轨迹回放历史数据'); expect(within(dialog).getByRole('link', { name: '车辆档案' })).toHaveAttribute('href', '/vehicles/VIN001'); expect(within(dialog).getByRole('link', { name: '轨迹回放' })).toHaveAttribute('href', '/tracks?vin=VIN001'); const vehicleRequestsBeforeEvidenceToggle = mocks.accessVehicles.mock.calls.length; const summaryRequestsBeforeEvidenceToggle = mocks.accessSummary.mock.calls.length; fireEvent.click(evidenceToggle); expect(within(dialog).getByRole('button', { name: '查看 JT808 完整证据' })).toHaveAttribute('aria-expanded', 'false'); expect(within(dialog).queryByRole('region', { name: 'JT808 完整接入证据' })).not.toBeInTheDocument(); fireEvent.click(within(dialog).getByRole('button', { name: '查看 JT808 完整证据' })); expect(within(dialog).getByRole('button', { name: '收起 JT808 完整证据' })).toHaveAttribute('aria-expanded', 'true'); expect(mocks.accessVehicles).toHaveBeenCalledTimes(vehicleRequestsBeforeEvidenceToggle); expect(mocks.accessSummary).toHaveBeenCalledTimes(summaryRequestsBeforeEvidenceToggle); expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('business-sheet-bottom', 'v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveStyle({ height: 'min(84dvh, 740px)' }); expect(document.querySelector('.v2-access-detail-sidesheet .business-dialog-heading > [data-slot="badge"]')).toHaveTextContent('部分来源异常'); fireEvent.click(within(dialog).getByRole('button', { name: '关闭车辆接入详情' })); expect(action).toHaveAttribute('aria-pressed', 'false'); expect(action).toHaveAttribute('aria-expanded', 'false'); expect(document.body.style.overflow).not.toBe('hidden'); const governanceButton = screen.getByRole('button', { name: /接入治理/ }); fireEvent.click(governanceButton); const governanceDialog = await screen.findByRole('dialog', { name: '接入治理配置' }); expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveClass('business-sheet-bottom', 'v2-workspace-config-sidesheet'); expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveStyle({ height: 'min(88dvh, 780px)' }); expect(within(governanceDialog).getByText('接入治理工作台')).toBeInTheDocument(); expect(within(governanceDialog).getByRole('list', { name: '接入治理配置摘要' })).toBeInTheDocument(); fireEvent.click(within(governanceDialog).getByRole('button', { name: '关闭接入治理配置' })); expect(governanceButton).toHaveAttribute('aria-expanded', 'false'); expect(document.body.style.overflow).not.toBe('hidden'); });