diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx index 05d68058..9fe7a2f8 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx @@ -1,14 +1,14 @@ 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 { MemoryRouter, useNavigate } 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 }) =>
})); +vi.mock('../map/TrackMap', () => ({ TrackMap: ({ activeIndex, followDurationMs, points }: { activeIndex: number; followDurationMs: number; points: unknown[] }) =>
})); const track = { vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z', @@ -38,6 +38,34 @@ beforeEach(() => { }); afterEach(() => { cleanup(); Object.values(mocks).forEach((mock) => mock.mockReset()); }); +function TrackNavigationHarness() { + const navigate = useNavigate(); + return <>; +} + +test('keeps committed track criteria authoritative to same-route navigation and browser history', async () => { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0); + expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '3'); + 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.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'); +}); + 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(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx index 41933c72..84966778 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx @@ -188,14 +188,17 @@ const SegmentRail = memo(function SegmentRail({ track, onSelectIndex }: { track: export default function TrackPage() { const [searchParams, setSearchParams] = useSearchParams(); const fallback = useMemo(defaultTrackWindow, []); - const initialDraft = useMemo(() => ({ - keyword: searchParams.get('vin') || searchParams.get('keyword') || '', - dateFrom: searchParams.get('dateFrom') || fallback.dateFrom, - dateTo: searchParams.get('dateTo') || fallback.dateTo, - protocol: searchParams.get('protocol') || '' - }), []); - const [draft, setDraft] = useState(initialDraft); - const [criteria, setCriteria] = useState(initialDraft); + const routeKey = searchParams.toString(); + const criteria = useMemo(() => { + const params = new URLSearchParams(routeKey); + return { + keyword: params.get('vin') || params.get('keyword') || '', + dateFrom: params.get('dateFrom') || fallback.dateFrom, + dateTo: params.get('dateTo') || fallback.dateTo, + protocol: params.get('protocol') || '' + }; + }, [fallback, routeKey]); + const [draft, setDraft] = useState(criteria); const [activeIndex, setActiveIndex] = useState(0); const [playing, setPlaying] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); @@ -224,6 +227,7 @@ export default function TrackPage() { const currentAddressPoint = useMemo(() => trackAddressCoordinate(current?.longitude, current?.latitude), [current?.latitude, current?.longitude]); const [addressPoint, setAddressPoint] = useState(); + useEffect(() => { setDraft(criteria); }, [criteria]); useEffect(() => { if (playing || !currentAddressPoint) return; const timer = window.setTimeout(() => setAddressPoint(currentAddressPoint), TRACK_ADDRESS_SETTLE_MS); @@ -264,7 +268,6 @@ export default function TrackPage() { const keyword = draft.keyword.trim(); if (!keyword) return; const next = { ...draft, keyword }; - setCriteria(next); const url = new URLSearchParams({ keyword }); if (next.dateFrom) url.set('dateFrom', next.dateFrom); if (next.dateTo) url.set('dateTo', next.dateTo);