Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

389 lines
31 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, 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 <output data-testid="access-location">{location.pathname}{location.search}</output>;
}
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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?keyword=粤A&accessLimit=20&accessPage=2&accessVin=VIN021']}><AccessPage /><AccessLocationProbe /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><AccessPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?accessGovernance=overview']}><AccessPage /><AccessLocationProbe /></MemoryRouter></QueryClientProvider>);
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<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 } } });
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?keyword=OLDVIN']}><AccessPage /></MemoryRouter></QueryClientProvider>);
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<HTMLElement>('.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<HTMLElement>('.v2-access-table-scroll-v3');
expect(desktopAccessScroll).toHaveAttribute('tabindex', '0');
expect(desktopAccessScroll).toHaveAttribute('aria-label', '车辆接入表格,可使用左右方向键浏览协议来源');
const desktopTableScroller = desktopAccessScroll?.querySelector<HTMLElement>('.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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><AccessPage /></MemoryRouter></QueryClientProvider>);
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<HTMLElement>('.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<HTMLElement>('.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(<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();
});
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(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><AccessPage /></MemoryRouter></QueryClientProvider>);
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');
});