142 lines
7.9 KiB
TypeScript
142 lines
7.9 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
import { api } from '../../api/client';
|
|
import type { VehicleDetail, VehicleRealtimeRow } from '../../api/types';
|
|
import { buildMonitorPath, withMonitorReturn } from '../routing/monitorContext';
|
|
import { ROUTER_FUTURE } from '../routing/routerConfig';
|
|
import VehiclePage from './VehiclePage';
|
|
|
|
const fleetMapVehicles = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('../map/FleetMap', () => ({
|
|
FleetMap: ({ vehicles }: { vehicles: VehicleRealtimeRow[] }) => {
|
|
fleetMapVehicles(vehicles);
|
|
return <div data-testid="fleet-map" />;
|
|
}
|
|
}));
|
|
|
|
vi.mock('../auth/AuthGate', () => ({
|
|
usePlatformSession: () => ({ session: { name: 'test-viewer', role: 'viewer', authMode: 'enforce' } })
|
|
}));
|
|
|
|
const initialRealtime = {
|
|
vin: 'LTEST000000000001', plate: '粤A12345', phone: '', oem: '', protocols: ['JT808'], sourceStatus: [],
|
|
sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'JT808',
|
|
longitude: 113.26, latitude: 23.13, speedKmh: 20, socPercent: 80, totalMileageKm: 1000,
|
|
lastSeen: '2026-07-16 10:00:00', reportIntervalMs: 30_000
|
|
} satisfies VehicleRealtimeRow;
|
|
|
|
const detail = {
|
|
vin: initialRealtime.vin,
|
|
lookupKey: initialRealtime.vin,
|
|
lookupResolved: true,
|
|
realtimeSummary: initialRealtime,
|
|
sources: ['JT808'],
|
|
sourceStatus: [],
|
|
realtime: [],
|
|
history: { items: [], total: 0, limit: 20, offset: 0 },
|
|
raw: { items: [], total: 0, limit: 20, offset: 0 },
|
|
mileage: { items: [], total: 0, limit: 20, offset: 0 },
|
|
quality: { items: [], total: 0, limit: 20, offset: 0 }
|
|
} satisfies VehicleDetail;
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
fleetMapVehicles.mockReset();
|
|
});
|
|
|
|
test('polls lightweight single-vehicle realtime data and passes its report interval to the map', async () => {
|
|
const movedRealtime = { ...initialRealtime, longitude: 113.28, latitude: 23.15, lastSeen: '2026-07-16 10:00:30' };
|
|
vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail);
|
|
const realtimeSpy = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [movedRealtime], total: 1, limit: 1, offset: 0 });
|
|
vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never);
|
|
const sourceEvidence = vi.spyOn(api, 'vehicleSourceEvidence').mockResolvedValue({
|
|
vin: initialRealtime.vin, plate: initialRealtime.plate, mileageDate: '2026-07-16',
|
|
recommendedLocationProtocol: 'JT808', recommendedLocationLabel: 'JT808', locationConflict: false,
|
|
locationSources: [], mileageSources: [],
|
|
comparison: { locationMaxDistanceM: 0, totalMileageDeltaKm: 0, dailyMileageDeltaKm: 0, reportTimeDeltaSeconds: 0 },
|
|
asOf: '2026-07-16T10:00:00+08:00'
|
|
});
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
|
|
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[`/vehicles/${initialRealtime.vin}`]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>);
|
|
|
|
await waitFor(() => expect(realtimeSpy).toHaveBeenCalledTimes(1));
|
|
const realtimeQuery = client.getQueryCache().find({ queryKey: ['vehicle-detail-realtime', initialRealtime.vin] });
|
|
expect((realtimeQuery?.options as { refetchInterval?: number }).refetchInterval).toBe(10_000);
|
|
const params = realtimeSpy.mock.calls[0][0];
|
|
expect(params).toBeDefined();
|
|
expect(params!.get('keywords')).toBe(initialRealtime.vin);
|
|
expect(params!.get('limit')).toBe('1');
|
|
await waitFor(() => expect(fleetMapVehicles).toHaveBeenLastCalledWith([
|
|
expect.objectContaining({ longitude: 113.28, latitude: 23.15, reportIntervalMs: 30_000 })
|
|
]));
|
|
expect(sourceEvidence).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByTitle('展开全部位置来源'));
|
|
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
test('keeps the monitor return on the vehicle page and its nested investigation links', async () => {
|
|
vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail);
|
|
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [initialRealtime], total: 1, limit: 1, offset: 0 });
|
|
vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never);
|
|
const addressSpy = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'amap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市测试道路' });
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
const monitorPath = buildMonitorPath({
|
|
mode: 'map', keyword: '', protocol: '', status: '', selectedVin: 'LTEST000000000001', detailOpen: true,
|
|
viewport: { zoom: 13, bounds: '113.1,23.0,113.4,23.3' }, listOffset: 0, listLimit: 50, hasViewport: true
|
|
});
|
|
const initialEntry = withMonitorReturn(`/vehicles/${initialRealtime.vin}`, monitorPath);
|
|
|
|
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[initialEntry]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>);
|
|
|
|
expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath);
|
|
const trackLink = screen.getByRole('link', { name: /轨迹回放/ });
|
|
const historyLink = screen.getByRole('link', { name: /历史数据/ });
|
|
const mileageLink = screen.getByRole('link', { name: /里程查询/ });
|
|
expect(new URL(trackLink.getAttribute('href')!, 'http://localhost').searchParams.get('monitorReturn')).toBe(monitorPath);
|
|
expect(new URL(historyLink.getAttribute('href')!, 'http://localhost').searchParams.get('monitorReturn')).toBe(monitorPath);
|
|
expect(new URL(mileageLink.getAttribute('href')!, 'http://localhost').searchParams.get('monitorReturn')).toBe(monitorPath);
|
|
|
|
const liveOverview = screen.getByText('最新上报').closest('section');
|
|
const archive = screen.getByText('车辆主档').closest('section');
|
|
expect(liveOverview).not.toBeNull();
|
|
expect(archive).not.toBeNull();
|
|
expect(liveOverview!.compareDocumentPosition(archive!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
|
|
|
expect(addressSpy).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole('button', { name: '解析当前位置' }));
|
|
expect(await screen.findByText('广东省广州市测试道路')).toBeInTheDocument();
|
|
expect(addressSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('shows unavailable live fields as dashes instead of fabricated zeroes', async () => {
|
|
const unavailable = {
|
|
...initialRealtime,
|
|
speedAvailable: false,
|
|
speedKmh: 0,
|
|
socAvailable: false,
|
|
socPercent: 0,
|
|
mileageAvailable: false,
|
|
totalMileageKm: 0,
|
|
todayMileageAvailable: false,
|
|
todayMileageKm: 0
|
|
};
|
|
vi.spyOn(api, 'vehicleDetail').mockResolvedValue({ ...detail, realtimeSummary: unavailable });
|
|
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [unavailable], total: 1, limit: 1, offset: 0 });
|
|
vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
|
|
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[`/vehicles/${initialRealtime.vin}`]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>);
|
|
|
|
const liveOverview = (await screen.findByText('最新上报')).closest('section')!;
|
|
for (const label of ['速度', 'SOC', '总里程', '当日里程']) {
|
|
const metric = Array.from(liveOverview.querySelectorAll('.v2-live-grid > div')).find((item) => item.querySelector('small')?.textContent === label);
|
|
expect(metric).toHaveTextContent('—');
|
|
expect(metric).not.toHaveTextContent(/^0/);
|
|
}
|
|
});
|