feat: expand vehicle data platform capabilities
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user