94 lines
7.7 KiB
TypeScript
94 lines
7.7 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import type { TrackPlaybackResponse } from '../../api/types';
|
|
import TrackPage from './TrackPage';
|
|
import { ROUTER_FUTURE } from '../routing/routerConfig';
|
|
|
|
const mocks = vi.hoisted(() => ({ trackPlayback: vi.fn(), reverseGeocode: vi.fn(), vehicles: vi.fn() }));
|
|
vi.mock('../../api/client', () => ({ api: mocks }));
|
|
vi.mock('../map/TrackMap', () => ({ TrackMap: ({ activeIndex, followDurationMs }: { activeIndex: number; followDurationMs: number }) => <div data-testid="track-map" data-active-index={activeIndex} data-follow-duration={followDurationMs} /> }));
|
|
|
|
const track = {
|
|
vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z',
|
|
points: [
|
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:00:00Z', serverTime: '2026-07-15T00:00:01Z', longitude: 113.1, latitude: 23.1, speedKmh: 0, totalMileageKm: 100, socPercent: 80, socAvailable: true },
|
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:10:00Z', serverTime: '2026-07-15T00:10:01Z', longitude: 113.2, latitude: 23.2, speedKmh: 35, totalMileageKm: 104, socPercent: 79, socAvailable: true },
|
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:20:00Z', serverTime: '2026-07-15T00:20:01Z', longitude: 113.3, latitude: 23.3, speedKmh: 10, totalMileageKm: 108, socPercent: 78, socAvailable: true }
|
|
],
|
|
events: [
|
|
{ index: 0, sampledIndex: 0, type: 'start', title: '开始行驶', time: '2026-07-15T00:00:00Z', speedKmh: 0, longitude: 113.1, latitude: 23.1 },
|
|
{ index: 1, sampledIndex: 1, type: 'acceleration', title: '急加速', time: '2026-07-15T00:10:00Z', speedKmh: 35, longitude: 113.2, latitude: 23.2 },
|
|
{ index: 2, sampledIndex: 2, type: 'end', title: '结束行驶', time: '2026-07-15T00:20:00Z', speedKmh: 10, longitude: 113.3, latitude: 23.3 }
|
|
],
|
|
sources: [{ protocol: 'JT808', pointCount: 3, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z' }],
|
|
segments: [{ index: 0, type: 'moving', title: '行驶', startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', durationSeconds: 1200, distanceKm: 8, pointCount: 3, startIndex: 0, endIndex: 2, sampledStartIndex: 0, sampledEndIndex: 2 }],
|
|
stops: [{ index: 0, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:05:00Z', durationSeconds: 300, pointCount: 1, longitude: 113.1, latitude: 23.1, sampledIndex: 0, evidence: 'GPS 推断' }],
|
|
summary: { startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', distanceKm: 8, durationSeconds: 1200, averageSpeedKmh: 24, maximumSpeedKmh: 35, pointCount: 3, movingSeconds: 900, stoppedSeconds: 300, stopCount: 1, segmentCount: 1 },
|
|
coverage: { requestedStart: '', requestedEnd: '', actualStart: '', actualEnd: '', totalPoints: 3, fetchedPoints: 3, processedPoints: 3, returnedPoints: 3, complete: true, limitReasons: [], evidence: '时间窗完整' },
|
|
quality: { status: 'good', selectedProtocol: 'JT808', rawPoints: 3, validPoints: 3, alternateSourcePoints: 0, invalidCoordinatePoints: 0, duplicatePoints: 0, driftPoints: 0, sourceSwitches: 0, largeGapCount: 0, maximumGapSeconds: 0, evidence: '轨迹质量正常' }
|
|
} as unknown as TrackPlaybackResponse;
|
|
|
|
beforeEach(() => {
|
|
Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })) });
|
|
mocks.trackPlayback.mockResolvedValue(track);
|
|
mocks.reverseGeocode.mockResolvedValue({ formattedAddress: '广东省广州市测试道路' });
|
|
mocks.vehicles.mockResolvedValue({ items: [], total: 0, limit: 10, offset: 0 });
|
|
});
|
|
afterEach(() => { cleanup(); Object.values(mocks).forEach((mock) => mock.mockReset()); });
|
|
|
|
test('renders a map-first replay workspace and connects stop, event, and panel interactions', async () => {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
|
|
|
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
|
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
|
|
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument();
|
|
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
|
|
expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /事件点/ }));
|
|
fireEvent.click(screen.getByRole('button', { name: /急加速/ }));
|
|
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '1');
|
|
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2));
|
|
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
|
|
|
|
fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' }));
|
|
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '65');
|
|
fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' }));
|
|
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '收起查询面板' }));
|
|
expect(screen.getByRole('button', { name: /查询与明细/ })).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: /查询与明细/ }));
|
|
expect(screen.getByRole('button', { name: '收起查询面板' })).toBeInTheDocument();
|
|
|
|
await waitFor(() => expect(mocks.trackPlayback).toHaveBeenCalledTimes(1));
|
|
expect(mocks.trackPlayback.mock.calls[0][0].get('maxPoints')).toBe('1600');
|
|
expect(mocks.trackPlayback.mock.calls[0][1]).toBeInstanceOf(AbortSignal);
|
|
expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(1);
|
|
view.unmount();
|
|
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(0));
|
|
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(0));
|
|
});
|
|
|
|
test('coalesces rapid track scrubbing into one address lookup for the final point', async () => {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
|
|
|
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
|
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
|
|
|
|
const progress = screen.getByRole('slider', { name: '轨迹播放进度' });
|
|
fireEvent.change(progress, { target: { value: '1' } });
|
|
fireEvent.change(progress, { target: { value: '2' } });
|
|
|
|
expect(screen.getByText('位置已变化,等待地址解析…')).toBeInTheDocument();
|
|
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2));
|
|
expect(mocks.reverseGeocode.mock.calls[1][0].get('longitude')).toBe('113.3000');
|
|
expect(mocks.reverseGeocode.mock.calls[1][0].get('latitude')).toBe('23.3000');
|
|
});
|