Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx
2026-07-19 07:58:51 +08:00

328 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, useNavigate } from 'react-router-dom';
import type { TrackPlaybackResponse } from '../../api/types';
import TrackPage from './TrackPage';
import { buildMonitorPath, withMonitorReturn } from '../routing/monitorContext';
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, points }: { activeIndex: number; followDurationMs: number; points: unknown[] }) => <div data-testid="track-map" data-active-index={activeIndex} data-follow-duration={followDurationMs} data-point-count={points.length} /> }));
vi.mock('@douyinfe/semi-ui', async (importOriginal) => {
const actual = await importOriginal<typeof import('@douyinfe/semi-ui')>();
return {
...actual,
Slider: ({ value, onChange, min = 0, max = 100, disabled, ...props }: {
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
disabled?: boolean;
'aria-label'?: string;
}) => <input
type="range"
aria-label={props['aria-label']}
min={min}
max={max}
value={value ?? min}
disabled={disabled}
onChange={(event) => onChange?.(Number(event.target.value))}
/>,
Select: ({ value, onChange, optionList = [], ...props }: {
value?: string | number;
onChange?: (value: string) => void;
optionList?: Array<{ value: string | number; label: React.ReactNode }>;
'aria-label'?: string;
'aria-labelledby'?: string;
}) => <select
aria-label={props['aria-label']}
aria-labelledby={props['aria-labelledby']}
value={String(value ?? '')}
onChange={(event) => onChange?.(event.target.value)}
>
{optionList.map((option) => <option key={String(option.value)} value={String(option.value)}>{option.label}</option>)}
</select>
};
});
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()); });
function TrackNavigationHarness() {
const navigate = useNavigate();
return <><button type="button" onClick={() => navigate('/tracks')}></button><button type="button" onClick={() => navigate(-1)}>退</button><button type="button" onClick={() => navigate(1)}></button><TrackPage /></>;
}
test('preserves the exact monitor return after querying another track range', async () => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const monitorPath = buildMonitorPath({
mode: 'map', keyword: '粤A12345', protocol: '', status: '', selectedVin: 'LTEST000000000001', detailOpen: true,
viewport: { zoom: 14, bounds: '' }, listOffset: 0, listLimit: 50, hasViewport: true
});
const initialEntry = withMonitorReturn('/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59', monitorPath);
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={[initialEntry]}><TrackPage /></MemoryRouter></QueryClientProvider>);
expect(view.container.querySelector('.v2-track-page')).toHaveClass('has-monitor-return');
expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath);
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
fireEvent.change(screen.getByRole('combobox', { name: '数据来源' }), { target: { value: 'JT808' } });
fireEvent.click(screen.getByRole('button', { name: /查询轨迹/ }));
expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath);
});
test('keeps committed track criteria authoritative to same-route navigation and browser history', 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']}><TrackNavigationHarness /></MemoryRouter></QueryClientProvider>);
expect(view.container.querySelector('.v2-track-page')).not.toHaveClass('has-monitor-return');
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '3');
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
expect(screen.getByText(/07-15 00:00.*自动来源/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
fireEvent.click(screen.getByRole('button', { name: '清空轨迹路由' }));
expect(await screen.findByText('先选择车辆,再开始轨迹回放')).toBeInTheDocument();
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '0');
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('');
fireEvent.click(screen.getByRole('button', { name: '后退轨迹路由' }));
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '3');
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(screen.getByRole('button', { name: '前进轨迹路由' }));
expect(await screen.findByText('先选择车辆,再开始轨迹回放')).toBeInTheDocument();
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '0');
});
test('distinguishes a failed vehicle lookup from an empty result and retries in place', async () => {
mocks.vehicles.mockRejectedValueOnce(new Error('车辆目录暂时不可用')).mockResolvedValueOnce({ items: [{ vin: 'LTEST000000000001', plate: '粤A12345' }], total: 1, limit: 10, offset: 0 });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
fireEvent.focus(screen.getByRole('textbox', { name: '搜索轨迹车辆' }));
expect(await screen.findByRole('alert')).toHaveTextContent('车辆目录暂时不可用');
expect(screen.queryByText('没有匹配车辆')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '重试' }));
expect(await screen.findByRole('option', { name: /粤A12345/ })).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(mocks.vehicles).toHaveBeenCalledTimes(2);
});
test('merges duplicate vehicle sources and keeps the active date shortcut visible', async () => {
mocks.vehicles.mockResolvedValue({
items: [
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808' },
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'GB32960' }
],
total: 2,
limit: 10,
offset: 0
});
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
fireEvent.click(screen.getByRole('button', { name: '昨天' }));
expect(screen.getByRole('button', { name: '昨天' })).toHaveAttribute('aria-pressed', 'true');
const search = screen.getByRole('textbox', { name: '搜索轨迹车辆' });
fireEvent.focus(search);
fireEvent.change(search, { target: { value: '粤A12345' } });
const option = await screen.findByRole('option', { name: '粤A12345 LTEST000000000001 JT/T 808 GB/T 32960 选择' });
expect(document.querySelectorAll('.v2-track-vehicle-options [role="option"]')).toHaveLength(1);
expect(option).toHaveTextContent('粤A12345LTEST000000000001JT/T 808GB/T 32960选择');
});
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(view.container.querySelector('.v2-track-rail-shell.semi-card')).toBeInTheDocument();
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveClass('v2-workspace-command-bar', 'v2-track-query-header');
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveTextContent('粤A12345');
expect(screen.getByLabelText('轨迹查询任务栏')).toHaveTextContent('3 点');
expect(view.container.querySelector('.v2-track-query-panel')).toHaveClass('is-collapsed');
expect(view.container.querySelector('.v2-track-query-summary')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-result-header')).toHaveTextContent('轨迹证据粤A12345 · LTEST0000000000018 km');
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-current-coverage')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-track-current-metrics > span')).toHaveLength(4);
expect(view.container.querySelector('.v2-track-current-evidence')).toHaveTextContent('JT/T 808时间窗完整3 → 3 点报警 —');
expect(view.container.querySelector('.v2-track-current-evidence .v2-track-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(view.container.querySelector('.v2-track-coverage-float')).not.toBeInTheDocument();
expect(screen.getByRole('toolbar', { name: '地图轨迹工具' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-playback-dock.semi-card')).toBeInTheDocument();
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /停留 00:05:00/ })).toHaveClass('semi-button', 'v2-track-list-action');
expect(screen.getByRole('button', { name: /停留 00:05:00/ }).closest('.semi-list-item')).toHaveClass('v2-track-evidence-item');
expect(view.container.querySelector('.v2-track-evidence-list.semi-list')).toBeInTheDocument();
expect(screen.getByRole('slider', { name: '轨迹播放进度' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-dock-summary')).toHaveTextContent('行程里程8 km00:20:00 · 1 次停留');
expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent('已暂停 · 数据点 1 / 3');
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('tab', { name: /事件点/ }));
const acceleration = screen.getByRole('button', { name: /急加速/ });
expect(acceleration).toHaveClass('semi-button', 'v2-track-list-action');
expect(acceleration.closest('.semi-list-item')).toHaveClass('v2-track-evidence-item');
fireEvent.click(acceleration);
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.click(screen.getByRole('tab', { name: '概览' }));
expect(view.container.querySelectorAll('.v2-track-overview-section')).toHaveLength(3);
expect(view.container.querySelectorAll('.v2-track-overview-section.semi-card')).toHaveLength(0);
expect(view.container.querySelector('.v2-track-overview-descriptions.semi-descriptions')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-source-list.semi-list')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-track-source-item.semi-list-item')).toHaveLength(1);
expect(view.container.querySelector('.v2-track-source-item .v2-track-protocol-tag')).toHaveTextContent('JT/T 808');
expect(view.container.querySelector('.v2-track-source-item .v2-track-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(screen.getByText('完整点集').closest('.semi-tag')).toBeInTheDocument();
expect(screen.getByText('通过').closest('.semi-tag')).toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } });
fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' }));
expect(view.container.querySelector('.v2-track-progress-meta')).toHaveTextContent(/播放中 · 数据点 [12] \/ 3/);
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('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
expect(screen.getByRole('option', { name: 'GB/T 32960 · 仪表盘里程' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'JT/T 808 · GPS 里程' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: '宇通 MQTT · 仪表盘里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '收起条件' })).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByRole('button', { name: '收起条件' }));
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '隐藏查询与明细' }));
const expandRail = screen.getByRole('button', { name: '打开轨迹详情' });
expect(expandRail).toBeInTheDocument();
expect(expandRail).toHaveTextContent('轨迹详情8 km · 1 个停留');
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('uses the shared Semi editor SideSheet for mobile track criteria and evidence', async () => {
Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })) });
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(view.container.querySelector('.v2-track-page')).toHaveClass('is-mobile-layout', 'is-rail-collapsed');
expect(view.container.querySelector('.v2-track-page > .v2-track-stage')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '打开轨迹详情' }));
const sheet = await waitFor(() => {
const element = document.querySelector('.v2-track-detail-sidesheet .semi-sidesheet-inner');
expect(element).toHaveAttribute('aria-label', '轨迹查询与明细');
return element;
});
expect(sheet).toHaveClass('semi-sidesheet-inner');
expect(sheet).toHaveAttribute('id', 'v2-track-mobile-workbench');
expect(document.querySelector('.v2-track-detail-sidesheet')).toHaveClass('v2-workspace-editor-sidesheet', 'semi-sidesheet-bottom');
expect(document.querySelector('.v2-track-detail-sidesheet .v2-track-rail')).toBeInTheDocument();
expect(document.querySelector('.v2-track-detail-sidesheet .v2-track-query-summary')).not.toBeInTheDocument();
expect(screen.queryByLabelText('轨迹查询任务栏')).not.toBeInTheDocument();
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
expect(screen.getAllByText('07-15 00:00 07-15 23:59 · 自动来源')).toHaveLength(2);
expect(screen.getByRole('list', { name: '轨迹查询与明细摘要' })).toHaveTextContent('行程里程8 km00:20:00停留 / 事件1 / 3地图证据点数据点3时间窗完整');
expect(screen.getByRole('button', { name: '修改轨迹查询' })).toHaveTextContent('修改查询');
expect(screen.getByRole('button', { name: /查看地图$/ })).toBeInTheDocument();
expect(sheet).toHaveStyle({ height: 'min(88dvh, 394px)' });
fireEvent.click(screen.getByRole('tab', { name: /事件点/ }));
await waitFor(() => expect(sheet).toHaveStyle({ height: 'min(88dvh, 518px)' }));
fireEvent.click(screen.getByRole('tab', { name: /停留点/ }));
fireEvent.click(screen.getByRole('button', { name: /停留 00:05:00/ }));
expect(view.container.querySelector('.v2-track-page')).toHaveClass('is-rail-collapsed');
fireEvent.click(screen.getByRole('button', { name: '打开轨迹详情' }));
await screen.findByRole('dialog', { name: '轨迹查询与明细' });
fireEvent.click(screen.getByRole('button', { name: '修改轨迹查询' }));
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
expect(screen.getByRole('button', { name: '收起轨迹查询' })).toHaveTextContent('收起查询');
expect(sheet).toHaveStyle({ height: 'min(88dvh, 780px)' });
expect(screen.getByRole('button', { name: '关闭轨迹查询与明细' })).toBeInTheDocument();
});
test('keeps one clear mobile query entry before a vehicle is selected', async () => {
Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })) });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
expect(await screen.findByRole('button', { name: /选择车辆/ })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '打开轨迹详情' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /选择车辆/ }));
expect(await screen.findByRole('dialog', { name: '轨迹查询与明细' })).toBeInTheDocument();
expect(screen.getByRole('list', { name: '轨迹查询与明细摘要' })).toHaveTextContent('查询状态待查询请先选择车辆');
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toBeInTheDocument();
});
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: '轨迹播放进度' });
expect(progress).toHaveAttribute('max', '2');
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');
});