feat(monitor): aggregate nationwide fleet by province
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
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, movingPointIDs, pointMoveDurationMs, pointMoveFrameMs } from './FleetMap';
|
||||
import { wgs84ToGcj02, type AMapAreaNode, type AMapLike, type AMapMap, type AMapMassPoint } from '../../integrations/amap';
|
||||
import { advancePointMotions, FleetMap, groupProvincePoints, interpolateMassPoints, massPointFingerprint, movingPointIDs, pointMoveDurationMs, pointMoveFrameMs, provinceMarkerPlacements } from './FleetMap';
|
||||
|
||||
const setData = vi.fn<(data: AMapMassPoint[]) => void>();
|
||||
const setStyle = vi.fn();
|
||||
@@ -23,6 +23,7 @@ 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 markerInstances: TestMarker[] = [];
|
||||
const mapOptions: Record<string, unknown>[] = [];
|
||||
const labelLayerOptions: Record<string, unknown>[] = [];
|
||||
|
||||
@@ -78,8 +79,12 @@ class TestLabelMarker {
|
||||
class TestMarker {
|
||||
setMap = markerSetMap;
|
||||
setPosition = markerSetPosition;
|
||||
handlers = new Map<string, (event?: unknown) => void>();
|
||||
on = vi.fn((event: string, handler: (value?: unknown) => void) => this.handlers.set(event, handler));
|
||||
off = vi.fn((event: string) => this.handlers.delete(event));
|
||||
constructor(options: Record<string, unknown>) {
|
||||
markerOptions.push(options);
|
||||
markerInstances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +143,35 @@ const pointMap: MonitorMapResponse = {
|
||||
}]
|
||||
};
|
||||
|
||||
const provinceMap: MonitorMapResponse = {
|
||||
...monitorMap,
|
||||
mode: 'provinces',
|
||||
total: 3,
|
||||
provincePoints: [
|
||||
{ longitude: 113.26, latitude: 23.13, status: 'driving' },
|
||||
{ longitude: 113.27, latitude: 23.14, status: 'idle' },
|
||||
{ longitude: 113.28, latitude: 23.15, status: 'offline' }
|
||||
]
|
||||
};
|
||||
|
||||
const provinceAreaNode: AMapAreaNode = {
|
||||
getSubFeatures: () => [],
|
||||
groupByPosition: (points) => [
|
||||
{
|
||||
subFeatureIndex: 0,
|
||||
subFeature: { properties: { adcode: 440000, name: '广东省', center: [113.266, 23.132] } },
|
||||
pointsIndexes: points.map((_, index) => index),
|
||||
points
|
||||
},
|
||||
{
|
||||
subFeatureIndex: 1,
|
||||
subFeature: { properties: { adcode: 110000, name: '北京市', center: [116.405, 39.905] } },
|
||||
pointsIndexes: [],
|
||||
points: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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],
|
||||
@@ -232,6 +266,7 @@ afterEach(() => {
|
||||
cleanup();
|
||||
delete window.__LINGNIU_APP_CONFIG__;
|
||||
delete window.AMapLoader;
|
||||
delete window.AMapUI;
|
||||
setData.mockReset();
|
||||
setStyle.mockReset();
|
||||
addLabels.mockReset();
|
||||
@@ -253,11 +288,42 @@ afterEach(() => {
|
||||
getBounds.mockReturnValue({});
|
||||
mapHandlers.clear();
|
||||
markerOptions.length = 0;
|
||||
markerInstances.length = 0;
|
||||
mapOptions.length = 0;
|
||||
labelLayerOptions.length = 0;
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1024 });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('groups nationwide seeds by official province features and preserves status counts', () => {
|
||||
const grouped = groupProvincePoints(provinceAreaNode, provinceMap.provincePoints ?? []);
|
||||
expect(grouped.unlocated).toBe(0);
|
||||
expect(grouped.aggregates).toEqual([expect.objectContaining({
|
||||
adcode: '440000',
|
||||
name: '广东省',
|
||||
longitude: 113.266,
|
||||
latitude: 23.132,
|
||||
count: 3,
|
||||
online: 2,
|
||||
offline: 1,
|
||||
driving: 1,
|
||||
idle: 1,
|
||||
unknown: 0
|
||||
})]);
|
||||
});
|
||||
|
||||
test('keeps dense province totals visible with deterministic pixel-space avoidance', () => {
|
||||
const base = groupProvincePoints(provinceAreaNode, provinceMap.provincePoints ?? []).aggregates[0];
|
||||
const placements = provinceMarkerPlacements([
|
||||
base,
|
||||
{ ...base, adcode: '310000', name: '上海市', count: 2 },
|
||||
{ ...base, adcode: '320000', name: '江苏省', count: 1 }
|
||||
], 5);
|
||||
expect(placements.get(base.adcode)).toEqual({ dx: 0, dy: 0 });
|
||||
expect(new Set([...placements.values()].map((placement) => `${placement.dx}:${placement.dy}`)).size).toBe(3);
|
||||
expect(provinceMarkerPlacements([base], 5)).toEqual(new Map([[base.adcode, { dx: 0, dy: 0 }]]));
|
||||
});
|
||||
|
||||
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) => {
|
||||
@@ -319,6 +385,17 @@ test('restores the saved monitor viewport without forcing the selected vehicle z
|
||||
expect(panTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('starts a fresh mobile nationwide map one zoom level wider', async () => {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 390 });
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
|
||||
render(<FleetMap vehicles={[]} monitorMap={monitorMap} onSelect={() => undefined} />);
|
||||
|
||||
await waitFor(() => expect(mapOptions).toHaveLength(1));
|
||||
expect(mapOptions[0]).toEqual(expect.objectContaining({ zoom: 4 }));
|
||||
});
|
||||
|
||||
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()) };
|
||||
@@ -604,3 +681,29 @@ test('renders every nearby plate with staggered positions at maximum zoom', asyn
|
||||
expect(selectedDenseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
|
||||
expect(selectedDenseMarkers.find((marker) => marker.options.rank === 100)).toBeDefined();
|
||||
});
|
||||
|
||||
test('renders crisp province totals and drills into the existing cluster layer', async () => {
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
const loadUI = vi.fn((_modules: string[], callback: (...constructors: unknown[]) => void) => {
|
||||
callback(class TestDistrictExplorer {
|
||||
loadAreaNode(_adcode: number, callbackArea: (error: Error | null, areaNode?: AMapAreaNode) => void) {
|
||||
callbackArea(null, provinceAreaNode);
|
||||
}
|
||||
});
|
||||
});
|
||||
window.AMapUI = { loadUI };
|
||||
|
||||
render(<FleetMap vehicles={[]} monitorMap={provinceMap} onSelect={() => undefined} />);
|
||||
|
||||
await waitFor(() => expect(markerOptions.some((options) => String(options.content).includes('v2-province-marker'))).toBe(true));
|
||||
expect(loadUI).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText(/1 个省级区域 · 3 辆/)).toBeInTheDocument();
|
||||
expect(setData).toHaveBeenLastCalledWith([]);
|
||||
const provinceIndex = markerOptions.findIndex((options) => String(options.content).includes('广东省'));
|
||||
expect(String(markerOptions[provinceIndex]?.content)).toContain('<strong>3</strong>');
|
||||
expect(String(markerOptions[provinceIndex]?.content)).toContain('在线 2');
|
||||
|
||||
markerInstances[provinceIndex]?.handlers.get('click')?.();
|
||||
expect(setZoomAndCenter).toHaveBeenLastCalledWith(7, [113.266, 23.132]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user