Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/map/TrackMap.test.tsx
2026-07-16 07:13:23 +08:00

221 lines
6.5 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import type { HistoryLocationRow, TrackStop } from '../../api/types';
import type { AMapLike } from '../../integrations/amap';
import { TrackMap } from './TrackMap';
const overlayOff = vi.fn();
const overlaySetMap = vi.fn();
const mapOff = vi.fn();
const mapDestroy = vi.fn();
const markerListeners: Array<{ overlay: TestOverlay; event: string; handler: () => void }> = [];
const overlayInstances: TestOverlay[] = [];
const mapInstances: TestMap[] = [];
class TestOverlay {
constructor() { overlayInstances.push(this); }
on = vi.fn((event: string, handler: () => void) => markerListeners.push({ overlay: this, event, handler }));
off = overlayOff;
setMap = overlaySetMap;
setPosition = vi.fn();
setPath = vi.fn();
}
class TestMap {
constructor() { mapInstances.push(this); }
add = vi.fn();
addControl = vi.fn();
setFitView = vi.fn();
destroy = mapDestroy;
on = vi.fn();
off = mapOff;
panTo = vi.fn();
}
class TestScale {}
class TestToolBar {}
class TestSize {}
class TestPixel {}
class TestMassMarks {}
function amapMock(): AMapLike {
return {
Map: TestMap as unknown as AMapLike['Map'],
Marker: TestOverlay as unknown as AMapLike['Marker'],
Polyline: TestOverlay as unknown as AMapLike['Polyline'],
Scale: TestScale,
ToolBar: TestToolBar,
Size: TestSize as unknown as AMapLike['Size'],
Pixel: TestPixel as unknown as AMapLike['Pixel'],
MassMarks: TestMassMarks as unknown as AMapLike['MassMarks']
};
}
const points = [{
vin: 'LTEST000000000001',
plate: '粤A12345',
protocol: 'JT808',
deviceTime: '2026-07-16T00:00:00Z',
serverTime: '2026-07-16T00:00:01Z',
longitude: 113.26,
latitude: 23.13,
speedKmh: 0,
totalMileageKm: 100
}, {
vin: 'LTEST000000000001',
plate: '粤A12345',
protocol: 'JT808',
deviceTime: '2026-07-16T00:10:00Z',
serverTime: '2026-07-16T00:10:01Z',
longitude: 113.28,
latitude: 23.15,
speedKmh: 30,
totalMileageKm: 105
}] as HistoryLocationRow[];
const stops = [{
index: 0,
startTime: '2026-07-16T00:00:00Z',
endTime: '2026-07-16T00:05:00Z',
durationSeconds: 300,
pointCount: 1,
longitude: 113.26,
latitude: 23.13,
sampledIndex: 0,
evidence: 'GPS 推断'
}] as TrackStop[];
afterEach(() => {
cleanup();
delete window.__LINGNIU_APP_CONFIG__;
delete window.AMapLoader;
overlayOff.mockReset();
overlaySetMap.mockReset();
mapOff.mockReset();
mapDestroy.mockReset();
markerListeners.length = 0;
overlayInstances.length = 0;
mapInstances.length = 0;
});
test('defers the map runtime until valid data exists, retries a transient failure, and releases it when cleared', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
const load = vi.fn()
.mockRejectedValueOnce(new Error('temporary map network failure'))
.mockResolvedValueOnce(amapMock());
window.AMapLoader = { load };
const view = render(<TrackMap
points={[]}
stops={[]}
activeIndex={0}
showStops={false}
follow
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
expect(load).not.toHaveBeenCalled();
expect(mapInstances).toHaveLength(0);
expect(view.container.querySelector('.v2-map-state')).not.toBeInTheDocument();
view.rerender(<TrackMap
points={points}
stops={[]}
activeIndex={0}
showStops={false}
follow
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
await waitFor(() => expect(mapInstances).toHaveLength(1));
expect(load).toHaveBeenCalledTimes(2);
expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();
view.rerender(<TrackMap
points={[]}
stops={[]}
activeIndex={0}
showStops={false}
follow
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapDestroy).toHaveBeenCalledTimes(1));
expect(mapInstances).toHaveLength(1);
});
test('does not initialize the map for rows without a valid coordinate', () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
const load = vi.fn(async () => amapMock());
window.AMapLoader = { load };
render(<TrackMap
points={[{ ...points[0], longitude: 0, latitude: 0 }]}
stops={[]}
activeIndex={0}
showStops={false}
follow
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
expect(load).not.toHaveBeenCalled();
expect(mapInstances).toHaveLength(0);
expect(document.body).toHaveTextContent('当前轨迹没有可展示的有效坐标');
});
test('uses the playback cadence for follow animation and advances the passed path', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
const view = render(<TrackMap
points={points}
stops={[]}
activeIndex={0}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances).toHaveLength(1));
view.rerender(<TrackMap
points={points}
stops={[]}
activeIndex={1}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances[0].panTo).toHaveBeenLastCalledWith(expect.any(Array), 65));
expect(overlayInstances.some((overlay) => overlay.setPath.mock.calls.some(([path]) => Array.isArray(path) && path.length === 2))).toBe(true);
});
test('detaches every marker listener and map listener on unmount', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
const view = render(<TrackMap
points={points}
stops={stops}
activeIndex={0}
showStops
follow
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(markerListeners).toHaveLength(3));
view.unmount();
for (const listener of markerListeners) {
expect(overlayOff).toHaveBeenCalledWith(listener.event, listener.handler);
}
expect(mapOff).toHaveBeenCalledWith('dragstart', expect.any(Function));
expect(overlaySetMap).toHaveBeenCalled();
expect(mapDestroy).toHaveBeenCalledTimes(1);
});