feat: expand vehicle data platform capabilities

This commit is contained in:
lingniu
2026-07-27 16:46:15 +08:00
parent e3a1f80f86
commit 3c4bece72c
650 changed files with 62155 additions and 2552 deletions
@@ -1,14 +1,14 @@
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 { 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()
accessThresholds: vi.fn(), updateAccessThresholds: vi.fn(), vehicles: vi.fn(), claimAccessIdentity: vi.fn()
}));
const auth = vi.hoisted(() => ({ role: 'admin' }));
const layout = vi.hoisted(() => ({ mobile: false }));
@@ -40,6 +40,81 @@ function prepareBaseData() {
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;