feat(monitor): animate live vehicle positions
This commit is contained in:
@@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-libra
|
||||
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 { FleetMap, massPointFingerprint } from './FleetMap';
|
||||
import { FleetMap, interpolateMassPoints, massPointFingerprint } from './FleetMap';
|
||||
|
||||
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
|
||||
const setStyle = vi.fn();
|
||||
@@ -12,6 +12,7 @@ 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();
|
||||
@@ -61,6 +62,7 @@ class TestLabelsLayer {
|
||||
setMap = setLabelsMap;
|
||||
}
|
||||
class TestLabelMarker {
|
||||
setPosition = labelSetPosition;
|
||||
constructor(public options: Record<string, unknown>) {}
|
||||
}
|
||||
class TestMarker {
|
||||
@@ -141,6 +143,21 @@ test('keeps large fleet redraw fingerprints constant-size and sensitive to point
|
||||
expect(massPointFingerprint('points', [], manyPoints, [])).toBe(fingerprint);
|
||||
});
|
||||
|
||||
test('interpolates existing mass points with easing 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: [13.5, 23.5] });
|
||||
expect(interpolateMassPoints(previous, target, 1)).toEqual(target);
|
||||
});
|
||||
|
||||
function amapMock(): AMapLike {
|
||||
return {
|
||||
Map: TestMap as unknown as AMapLike['Map'],
|
||||
@@ -168,6 +185,7 @@ afterEach(() => {
|
||||
setLabelsMap.mockReset();
|
||||
markerSetMap.mockReset();
|
||||
markerSetPosition.mockReset();
|
||||
labelSetPosition.mockReset();
|
||||
setZoomAndCenter.mockReset();
|
||||
panTo.mockReset();
|
||||
mapOff.mockReset();
|
||||
@@ -237,7 +255,25 @@ test('detaches AMap listeners and destroys the map on unmount', async () => {
|
||||
expect(mapDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('skips unchanged refresh redraws and replaces only a moved vehicle label', async () => {
|
||||
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 () => {
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
||||
@@ -263,13 +299,15 @@ test('skips unchanged refresh redraws and replaces only a moved vehicle label',
|
||||
asOf: '2026-07-14T01:00:30Z',
|
||||
points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
|
||||
}} onSelect={() => undefined} />);
|
||||
await waitFor(() => expect(setData).toHaveBeenCalledTimes(dataCalls + 1));
|
||||
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_500 });
|
||||
expect(setData.mock.calls.length).toBeGreaterThan(dataCalls + 1);
|
||||
expect(setStyle).toHaveBeenCalledTimes(styleCalls);
|
||||
expect(removeLabels).toHaveBeenCalledTimes(removeCalls + 1);
|
||||
expect(addLabels).toHaveBeenCalledTimes(addCalls + 1);
|
||||
const replacementLabels = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
|
||||
expect(replacementLabels).toHaveLength(1);
|
||||
expect((replacementLabels[0].options.text as { content: string }).content).toBe('粤A12345');
|
||||
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 () => {
|
||||
@@ -362,6 +400,7 @@ test('renders one selected plate and smoothly follows it until the map is dragge
|
||||
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);
|
||||
@@ -418,6 +457,7 @@ test('renders one selected plate and smoothly follows it until the map is dragge
|
||||
);
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user