221 lines
6.5 KiB
TypeScript
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);
|
|
});
|