553 lines
21 KiB
TypeScript
553 lines
21 KiB
TypeScript
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
import type { MonitorMapResponse } from '../../api/types';
|
|
import { wgs84ToGcj02, type AMapLike, type AMapMap, type AMapMassPoint } from '../../integrations/amap';
|
|
import { advancePointMotions, FleetMap, interpolateMassPoints, massPointFingerprint, pointMoveDurationMs, pointMoveFrameMs } from './FleetMap';
|
|
|
|
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
|
|
const setStyle = vi.fn();
|
|
const addLabels = vi.fn();
|
|
const removeLabels = vi.fn();
|
|
const clearLabels = vi.fn();
|
|
const setLabelsMap = vi.fn();
|
|
const markerSetMap = vi.fn();
|
|
const markerSetPosition = vi.fn();
|
|
const labelSetPosition = vi.fn();
|
|
const setZoomAndCenter = vi.fn();
|
|
const panTo = vi.fn();
|
|
const mapOff = vi.fn();
|
|
const mapDestroy = vi.fn();
|
|
const massOff = vi.fn();
|
|
const getZoom = vi.fn(() => 5);
|
|
const getBounds = vi.fn((): ReturnType<NonNullable<AMapMap['getBounds']>> => ({}));
|
|
const mapHandlers = new Map<string, (event: unknown) => void>();
|
|
const markerOptions: Record<string, unknown>[] = [];
|
|
const mapOptions: Record<string, unknown>[] = [];
|
|
const labelLayerOptions: Record<string, unknown>[] = [];
|
|
|
|
function useFastAnimationFrames(stepMs = 2_500) {
|
|
let timestamp = 0;
|
|
return vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
|
timestamp += stepMs;
|
|
return window.setTimeout(() => callback(timestamp), 0);
|
|
});
|
|
}
|
|
|
|
class TestMap {
|
|
constructor(_container: HTMLDivElement, options: Record<string, unknown>) {
|
|
mapOptions.push(options);
|
|
}
|
|
add = vi.fn();
|
|
addControl = vi.fn();
|
|
destroy = mapDestroy;
|
|
on = vi.fn((event: string, handler: (value: unknown) => void) => mapHandlers.set(event, handler));
|
|
off = mapOff;
|
|
getZoom = getZoom;
|
|
getBounds = getBounds;
|
|
setZoomAndCenter = setZoomAndCenter;
|
|
panTo = panTo;
|
|
}
|
|
|
|
class TestMassMarks {
|
|
on = vi.fn();
|
|
off = massOff;
|
|
setMap = vi.fn();
|
|
setData = setData;
|
|
setStyle = setStyle;
|
|
}
|
|
|
|
class TestScale {}
|
|
class TestToolBar {}
|
|
class TestSize {}
|
|
class TestPixel {}
|
|
class TestLabelsLayer {
|
|
constructor(options: Record<string, unknown> = {}) {
|
|
labelLayerOptions.push(options);
|
|
}
|
|
add = addLabels;
|
|
remove = removeLabels;
|
|
clear = clearLabels;
|
|
setMap = setLabelsMap;
|
|
}
|
|
class TestLabelMarker {
|
|
setPosition = labelSetPosition;
|
|
constructor(public options: Record<string, unknown>) {}
|
|
}
|
|
class TestMarker {
|
|
setMap = markerSetMap;
|
|
setPosition = markerSetPosition;
|
|
constructor(options: Record<string, unknown>) {
|
|
markerOptions.push(options);
|
|
}
|
|
}
|
|
|
|
const monitorMap: MonitorMapResponse = {
|
|
mode: 'clusters',
|
|
zoom: 5,
|
|
total: 12,
|
|
truncated: false,
|
|
points: [],
|
|
clusters: [{
|
|
id: 'cluster-1',
|
|
longitude: 121.1,
|
|
latitude: 30.6,
|
|
count: 12,
|
|
online: 8,
|
|
offline: 4,
|
|
driving: 3,
|
|
idle: 5,
|
|
unknown: 0
|
|
}],
|
|
asOf: '2026-07-14T01:00:00Z'
|
|
};
|
|
|
|
const pointMap: MonitorMapResponse = {
|
|
...monitorMap,
|
|
mode: 'points',
|
|
zoom: 13,
|
|
total: 2,
|
|
clusters: [],
|
|
points: [{
|
|
vin: 'LTEST000000000001',
|
|
plate: '粤A12345',
|
|
protocol: 'JT808',
|
|
protocols: ['JT808'],
|
|
longitude: 113.26,
|
|
latitude: 23.13,
|
|
speedKmh: 42,
|
|
socPercent: 80,
|
|
totalMileageKm: 1234,
|
|
lastSeen: '2026-07-14T01:00:00Z',
|
|
reportIntervalMs: 10_000,
|
|
status: 'driving'
|
|
}, {
|
|
vin: 'LTEST000000000002',
|
|
plate: '粤B67890',
|
|
protocol: 'JT808',
|
|
protocols: ['JT808'],
|
|
longitude: 113.28,
|
|
latitude: 23.15,
|
|
speedKmh: 0,
|
|
socPercent: 72,
|
|
totalMileageKm: 2234,
|
|
lastSeen: '2026-07-14T01:00:00Z',
|
|
reportIntervalMs: 30_000,
|
|
status: 'idle'
|
|
}]
|
|
};
|
|
|
|
test('keeps large fleet redraw fingerprints constant-size and sensitive to point changes', () => {
|
|
const manyPoints = Array.from({ length: 10_000 }, (_, index) => ({
|
|
...pointMap.points[index % pointMap.points.length],
|
|
vin: `LARGE-FLEET-${index}`,
|
|
longitude: 113.26 + index / 1_000_000
|
|
}));
|
|
const fingerprint = massPointFingerprint('points', [], manyPoints, []);
|
|
const changed = massPointFingerprint('points', [], [
|
|
...manyPoints.slice(0, -1),
|
|
{ ...manyPoints[manyPoints.length - 1], longitude: 114.5 }
|
|
], []);
|
|
|
|
expect(fingerprint.length).toBeLessThan(40);
|
|
expect(changed).not.toBe(fingerprint);
|
|
expect(massPointFingerprint('points', [], manyPoints, [])).toBe(fingerprint);
|
|
});
|
|
|
|
test('interpolates existing mass points at constant speed while keeping new points stable', () => {
|
|
const previous: AMapMassPoint[] = [{ id: 'moving', label: 'A', style: 0, lnglat: [10, 20] }];
|
|
const target: AMapMassPoint[] = [
|
|
{ id: 'moving', label: 'A', style: 2, lnglat: [14, 24] },
|
|
{ id: 'new', label: 'B', style: 1, lnglat: [30, 40] }
|
|
];
|
|
|
|
expect(interpolateMassPoints(previous, target, 0)).toEqual([
|
|
{ ...target[0], lnglat: [10, 20] },
|
|
target[1]
|
|
]);
|
|
expect(interpolateMassPoints(previous, target, .5)[0]).toEqual({ ...target[0], lnglat: [12, 22] });
|
|
expect(interpolateMassPoints(previous, target, 1)).toEqual(target);
|
|
});
|
|
|
|
test('uses each protocol report interval with bounded fallbacks for point motion', () => {
|
|
expect(pointMoveDurationMs(10_000)).toBe(10_000);
|
|
expect(pointMoveDurationMs(30_000)).toBe(30_000);
|
|
expect(pointMoveDurationMs(60_000)).toBe(60_000);
|
|
expect(pointMoveDurationMs(undefined)).toBe(15_000);
|
|
expect(pointMoveDurationMs(100)).toBe(1_000);
|
|
expect(pointMoveDurationMs(300_000)).toBe(120_000);
|
|
});
|
|
|
|
test('advances each vehicle independently according to its report interval', () => {
|
|
const points: AMapMassPoint[] = [
|
|
{ id: 'JT808', label: 'A', style: 0, lnglat: [0, 0] },
|
|
{ id: 'GB32960', label: 'B', style: 0, lnglat: [0, 0] }
|
|
];
|
|
const pointArray = points;
|
|
const pointObjects = [...points];
|
|
advancePointMotions([
|
|
{ point: points[0], start: [0, 0], target: [30, 30], durationMs: 10_000 },
|
|
{ point: points[1], start: [0, 0], target: [30, 30], durationMs: 30_000 }
|
|
], 10_000);
|
|
|
|
expect(points[0].lnglat).toEqual([30, 30]);
|
|
expect(points[1].lnglat).toEqual([10, 10]);
|
|
expect(points).toBe(pointArray);
|
|
expect(points[0]).toBe(pointObjects[0]);
|
|
expect(points[1]).toBe(pointObjects[1]);
|
|
});
|
|
|
|
test('reduces animation frame frequency as fleet redraw pressure grows', () => {
|
|
expect(pointMoveFrameMs(1, 1)).toBe(100);
|
|
expect(pointMoveFrameMs(200, 10)).toBeLessThan(pointMoveFrameMs(800, 120));
|
|
expect(pointMoveFrameMs(2_000, 1_000)).toBe(220);
|
|
});
|
|
|
|
function amapMock(): AMapLike {
|
|
return {
|
|
Map: TestMap as unknown as AMapLike['Map'],
|
|
Marker: TestMarker as unknown as AMapLike['Marker'],
|
|
Polyline: class {} 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'],
|
|
LabelsLayer: TestLabelsLayer as unknown as NonNullable<AMapLike['LabelsLayer']>,
|
|
LabelMarker: TestLabelMarker as unknown as NonNullable<AMapLike['LabelMarker']>
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
delete window.__LINGNIU_APP_CONFIG__;
|
|
delete window.AMapLoader;
|
|
setData.mockReset();
|
|
setStyle.mockReset();
|
|
addLabels.mockReset();
|
|
removeLabels.mockReset();
|
|
clearLabels.mockReset();
|
|
setLabelsMap.mockReset();
|
|
markerSetMap.mockReset();
|
|
markerSetPosition.mockReset();
|
|
labelSetPosition.mockReset();
|
|
setZoomAndCenter.mockReset();
|
|
panTo.mockReset();
|
|
mapOff.mockReset();
|
|
mapDestroy.mockReset();
|
|
massOff.mockReset();
|
|
getZoom.mockReset();
|
|
getZoom.mockReturnValue(5);
|
|
getBounds.mockReset();
|
|
getBounds.mockReturnValue({});
|
|
mapHandlers.clear();
|
|
markerOptions.length = 0;
|
|
mapOptions.length = 0;
|
|
labelLayerOptions.length = 0;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
test('recovers in place after a transient AMap failure and renders data that arrived while retrying', async () => {
|
|
let resolveAMap!: (value: AMapLike) => void;
|
|
const delayedAMap = new Promise<AMapLike>((resolve) => {
|
|
resolveAMap = resolve;
|
|
});
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
const load = vi.fn()
|
|
.mockRejectedValueOnce(new Error('temporary map network failure'))
|
|
.mockImplementationOnce(() => delayedAMap);
|
|
window.AMapLoader = { load };
|
|
|
|
render(
|
|
<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={monitorMap}
|
|
onSelect={() => undefined}
|
|
/>
|
|
);
|
|
|
|
expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
|
|
expect(setData).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
resolveAMap(amapMock());
|
|
await delayedAMap;
|
|
});
|
|
|
|
await waitFor(() => expect(setData).toHaveBeenCalledWith([
|
|
expect.objectContaining({ id: 'cluster-1', lnglat: wgs84ToGcj02(121.1, 30.6), label: '12 辆' })
|
|
]));
|
|
const clusterStyles = setStyle.mock.calls[setStyle.mock.calls.length - 1]?.[0] as Array<{ url: string }>;
|
|
expect(decodeURIComponent(clusterStyles[5].url)).toContain('>12</text>');
|
|
expect(decodeURIComponent(clusterStyles[5].url)).not.toContain('10+');
|
|
expect(load).toHaveBeenCalledTimes(2);
|
|
expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('detaches AMap listeners and destroys the map on unmount', async () => {
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
|
|
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
|
await waitFor(() => expect(setData).toHaveBeenCalled());
|
|
view.unmount();
|
|
|
|
expect(massOff).toHaveBeenCalledWith('click', expect.any(Function));
|
|
expect(mapOff).toHaveBeenCalledWith('moveend', expect.any(Function));
|
|
expect(mapOff).toHaveBeenCalledWith('zoomend', expect.any(Function));
|
|
expect(mapOff).toHaveBeenCalledWith('dragstart', expect.any(Function));
|
|
expect(mapDestroy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('cancels an in-flight point animation when the map unmounts', async () => {
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
|
|
const cancelFrame = vi.spyOn(window, 'cancelAnimationFrame');
|
|
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
|
await waitFor(() => expect(setData).toHaveBeenCalled());
|
|
|
|
view.rerender(<FleetMap vehicles={[]} monitorMap={{
|
|
...pointMap,
|
|
points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
|
|
}} onSelect={() => undefined} />);
|
|
await waitFor(() => expect(requestFrame).toHaveBeenCalled());
|
|
view.unmount();
|
|
|
|
expect(cancelFrame).toHaveBeenCalledWith(expect.any(Number));
|
|
});
|
|
|
|
test('skips unchanged refresh redraws and smoothly moves points without replacing their labels', async () => {
|
|
useFastAnimationFrames();
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
|
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
|
const dataCalls = setData.mock.calls.length;
|
|
const styleCalls = setStyle.mock.calls.length;
|
|
const addCalls = addLabels.mock.calls.length;
|
|
const removeCalls = removeLabels.mock.calls.length;
|
|
const clearCalls = clearLabels.mock.calls.length;
|
|
|
|
await act(async () => {
|
|
view.rerender(<FleetMap vehicles={[]} monitorMap={{ ...pointMap, asOf: '2026-07-14T01:00:15Z' }} onSelect={() => undefined} />);
|
|
await Promise.resolve();
|
|
});
|
|
expect(setData).toHaveBeenCalledTimes(dataCalls);
|
|
expect(setStyle).toHaveBeenCalledTimes(styleCalls);
|
|
expect(addLabels).toHaveBeenCalledTimes(addCalls);
|
|
expect(removeLabels).toHaveBeenCalledTimes(removeCalls);
|
|
expect(clearLabels).toHaveBeenCalledTimes(clearCalls);
|
|
|
|
view.rerender(<FleetMap vehicles={[]} monitorMap={{
|
|
...pointMap,
|
|
asOf: '2026-07-14T01:00:30Z',
|
|
points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
|
|
}} onSelect={() => undefined} />);
|
|
await waitFor(() => {
|
|
const latest = setData.mock.calls[setData.mock.calls.length - 1]?.[0];
|
|
expect(latest?.[0].lnglat).toEqual(wgs84ToGcj02(113.27, 23.13));
|
|
}, { timeout: 1_800 });
|
|
expect(setData.mock.calls.length).toBeGreaterThan(dataCalls + 1);
|
|
const animationFrameBuffers = setData.mock.calls.slice(dataCalls).map(([frameData]) => frameData);
|
|
expect(new Set(animationFrameBuffers).size).toBe(1);
|
|
expect(setStyle).toHaveBeenCalledTimes(styleCalls);
|
|
expect(removeLabels).toHaveBeenCalledTimes(removeCalls);
|
|
expect(addLabels).toHaveBeenCalledTimes(addCalls);
|
|
expect(labelSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.13));
|
|
});
|
|
|
|
test('does not traverse unchanged point arrays for an asOf-only refresh', async () => {
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
let pointReads = 0;
|
|
const observedPoints = new Proxy(pointMap.points, {
|
|
get(target, property, receiver) {
|
|
if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1;
|
|
return Reflect.get(target, property, receiver);
|
|
}
|
|
});
|
|
const observedMap = { ...pointMap, points: observedPoints };
|
|
const view = render(<FleetMap vehicles={[]} monitorMap={observedMap} onSelect={() => undefined} />);
|
|
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
|
pointReads = 0;
|
|
|
|
await act(async () => {
|
|
view.rerender(<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={{ ...observedMap, asOf: '2026-07-14T01:00:15Z' }}
|
|
onSelect={() => undefined}
|
|
/>);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(pointReads).toBe(0);
|
|
});
|
|
|
|
test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
|
|
const [west, south] = wgs84ToGcj02(113, 22);
|
|
const [east, north] = wgs84ToGcj02(114, 24);
|
|
getZoom.mockReturnValue(13);
|
|
getBounds.mockReturnValue({
|
|
getSouthWest: () => ({ getLng: () => west, getLat: () => south }),
|
|
getNorthEast: () => ({ getLng: () => east, getLat: () => north })
|
|
});
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
const onViewportChange = vi.fn();
|
|
|
|
render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} onViewportChange={onViewportChange} />);
|
|
|
|
await waitFor(() => expect(onViewportChange).toHaveBeenCalled());
|
|
const viewport = onViewportChange.mock.calls[onViewportChange.mock.calls.length - 1]?.[0] as { zoom: number; bounds: string };
|
|
expect(viewport.zoom).toBe(13);
|
|
const bounds = viewport.bounds.split(',').map(Number);
|
|
expect(Math.abs(bounds[0] - 113)).toBeLessThan(0.0005);
|
|
expect(Math.abs(bounds[1] - 22)).toBeLessThan(0.0005);
|
|
expect(Math.abs(bounds[2] - 114)).toBeLessThan(0.0005);
|
|
expect(Math.abs(bounds[3] - 24)).toBeLessThan(0.0005);
|
|
});
|
|
|
|
test('renders one selected plate and smoothly follows it until the map is dragged', async () => {
|
|
useFastAnimationFrames();
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
|
|
const view = render(
|
|
<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={pointMap}
|
|
selectedVin="LTEST000000000001"
|
|
onSelect={() => undefined}
|
|
/>
|
|
);
|
|
|
|
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
|
const renderedLabels = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
|
const selectedPlate = renderedLabels.find((marker) => (marker.options.text as { content: string }).content === '粤A12345');
|
|
const floatingPlate = renderedLabels.find((marker) => (marker.options.text as { content: string }).content === '粤B67890');
|
|
expect(selectedPlate).toBeDefined();
|
|
expect(floatingPlate).toBeDefined();
|
|
expect((selectedPlate!.options.text as { style: unknown }).style).toEqual((floatingPlate!.options.text as { style: unknown }).style);
|
|
expect(selectedPlate!.options).toEqual(expect.objectContaining({ rank: 100, zIndex: 10 }));
|
|
expect(floatingPlate!.options).toEqual(expect.objectContaining({ rank: 1, zIndex: 1 }));
|
|
expect((floatingPlate!.options.text as { style: Record<string, unknown> }).style).toEqual(expect.objectContaining({
|
|
fillColor: '#174d9f',
|
|
backgroundColor: '#eef5ff',
|
|
borderColor: '#7fb0fa',
|
|
borderWidth: 1,
|
|
borderRadius: 6,
|
|
padding: [5, 9],
|
|
fontSize: 11,
|
|
shadowColor: 'rgba(18, 104, 243, 0.18)',
|
|
shadowBlur: 14,
|
|
shadowOffsetY: 5
|
|
}));
|
|
expect(mapOptions).toContainEqual(expect.objectContaining({ mapStyle: 'amap://styles/whitesmoke' }));
|
|
await waitFor(() => expect(markerOptions).toContainEqual(expect.objectContaining({
|
|
content: expect.stringContaining('粤A12345')
|
|
})));
|
|
expect(markerOptions[markerOptions.length - 1]?.content).not.toContain('<span>');
|
|
expect(markerOptions[markerOptions.length - 1]?.content).toContain('is-driving');
|
|
expect(markerSetMap).toHaveBeenCalled();
|
|
expect(renderedLabels).toHaveLength(2);
|
|
expect(setZoomAndCenter).toHaveBeenCalledTimes(1);
|
|
expect(setZoomAndCenter).toHaveBeenLastCalledWith(15, wgs84ToGcj02(113.26, 23.13));
|
|
|
|
view.rerender(
|
|
<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={{
|
|
...pointMap,
|
|
asOf: '2026-07-14T01:00:15Z',
|
|
points: [{ ...pointMap.points[0], longitude: 113.27, latitude: 23.14 }]
|
|
}}
|
|
selectedVin="LTEST000000000001"
|
|
onSelect={() => undefined}
|
|
/>
|
|
);
|
|
await waitFor(() => expect(markerSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 });
|
|
expect(panTo).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14), 10_000);
|
|
expect(setZoomAndCenter).toHaveBeenCalledTimes(1);
|
|
|
|
const follow = screen.getByRole('button', { name: '跟随车辆' });
|
|
expect(follow).toHaveAttribute('aria-pressed', 'true');
|
|
act(() => mapHandlers.get('dragstart')?.({}));
|
|
expect(follow).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
const panCountAfterDrag = panTo.mock.calls.length;
|
|
view.rerender(
|
|
<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={{
|
|
...pointMap,
|
|
asOf: '2026-07-14T01:00:30Z',
|
|
points: [{ ...pointMap.points[0], longitude: 113.29, latitude: 23.16 }, pointMap.points[1]]
|
|
}}
|
|
selectedVin="LTEST000000000001"
|
|
onSelect={() => undefined}
|
|
/>
|
|
);
|
|
await waitFor(() => expect(markerSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.29, 23.16)), { timeout: 1_800 });
|
|
expect(panTo).toHaveBeenCalledTimes(panCountAfterDrag);
|
|
fireEvent.click(follow);
|
|
await waitFor(() => expect(follow).toHaveAttribute('aria-pressed', 'true'));
|
|
expect(panTo).toHaveBeenLastCalledWith(wgs84ToGcj02(113.29, 23.16), 650);
|
|
|
|
getZoom.mockReturnValue(20);
|
|
view.rerender(
|
|
<FleetMap
|
|
vehicles={[]}
|
|
monitorMap={{ ...pointMap, asOf: '2026-07-14T01:00:45Z' }}
|
|
selectedVin="LTEST000000000002"
|
|
onSelect={() => undefined}
|
|
/>
|
|
);
|
|
await waitFor(() => expect(panTo).toHaveBeenLastCalledWith(wgs84ToGcj02(113.28, 23.15), 650));
|
|
expect(setZoomAndCenter).toHaveBeenCalledTimes(1);
|
|
expect(markerOptions[markerOptions.length - 1]?.content).toContain('is-idle');
|
|
|
|
const toggle = screen.getByRole('button', { name: '悬浮车牌' });
|
|
expect(toggle).toHaveAttribute('aria-pressed', 'true');
|
|
fireEvent.click(toggle);
|
|
await waitFor(() => expect(toggle).toHaveAttribute('aria-pressed', 'false'));
|
|
expect(removeLabels).toHaveBeenCalled();
|
|
const hiddenFloatingLabels = removeLabels.mock.calls[removeLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
|
expect(hiddenFloatingLabels).toHaveLength(1);
|
|
expect((hiddenFloatingLabels[0].options.text as { content: string }).content).toBe('粤A12345');
|
|
});
|
|
|
|
test('renders every nearby plate with staggered positions at maximum zoom', async () => {
|
|
getZoom.mockReturnValue(20);
|
|
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
|
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
|
const crowdedMap: MonitorMapResponse = {
|
|
...pointMap,
|
|
zoom: 20,
|
|
total: 3,
|
|
points: [
|
|
pointMap.points[0],
|
|
{ ...pointMap.points[1], longitude: 113.26001, latitude: 23.13001 },
|
|
{ ...pointMap.points[1], vin: 'LTEST000000000003', plate: '粤C24680', longitude: 113.26002, latitude: 23.13002 }
|
|
]
|
|
};
|
|
|
|
const view = render(<FleetMap vehicles={[]} monitorMap={crowdedMap} onSelect={() => undefined} />);
|
|
|
|
await waitFor(() => expect(addLabels).toHaveBeenCalled());
|
|
expect(labelLayerOptions).toContainEqual(expect.objectContaining({ zooms: [19, 20], zIndex: 110, collision: false, allowCollision: true }));
|
|
const denseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
|
expect(denseMarkers).toHaveLength(3);
|
|
const textOffsets = denseMarkers.map((marker) => JSON.stringify((marker.options.text as { offset: [number, number] }).offset));
|
|
expect(new Set(textOffsets).size).toBe(3);
|
|
expect(denseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
|
|
|
|
const labelRenderCount = addLabels.mock.calls.length;
|
|
view.rerender(<FleetMap vehicles={[]} monitorMap={crowdedMap} selectedVin="LTEST000000000002" onSelect={() => undefined} />);
|
|
await waitFor(() => expect(addLabels.mock.calls.length).toBeGreaterThan(labelRenderCount));
|
|
const selectedDenseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
|
expect(selectedDenseMarkers).toHaveLength(1);
|
|
expect(selectedDenseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
|
|
expect(selectedDenseMarkers.find((marker) => marker.options.rank === 100)).toBeDefined();
|
|
});
|