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 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(); 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 setCenter = vi.fn(() => mapHandlers.get('moveend')?.({})); 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> => ({})); const mapHandlers = new Map void>(); const markerOptions: Record[] = []; const markerInstances: TestMarker[] = []; const mapOptions: Record[] = []; const labelLayerOptions: Record[] = []; 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) { 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; setCenter = setCenter; 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 = {}) { labelLayerOptions.push(options); } add = addLabels; remove = removeLabels; clear = clearLabels; setMap = setLabelsMap; } class TestLabelMarker { setPosition = labelSetPosition; constructor(public options: Record) {} } class TestMarker { setMap = markerSetMap; setPosition = markerSetPosition; handlers = new Map 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) { markerOptions.push(options); markerInstances.push(this); } } 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' }] }; 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], 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('does not interpolate across an authoritative location source switch', () => { const previous: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.2, 23.1], sourceToken: 'JT808:terminal-a' }]; const sameSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.3, 23.2], sourceToken: 'JT808:terminal-a' }]; const switchedSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.4, 23.3], sourceToken: 'GB32960:canonical' }]; expect(movingPointIDs(previous, sameSource).has('vehicle')).toBe(true); expect(movingPointIDs(previous, switchedSource).has('vehicle')).toBe(false); }); 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, LabelMarker: TestLabelMarker as unknown as NonNullable }; } afterEach(() => { cleanup(); delete window.__LINGNIU_APP_CONFIG__; delete window.AMapLoader; delete window.AMapUI; setData.mockReset(); setStyle.mockReset(); addLabels.mockReset(); removeLabels.mockReset(); clearLabels.mockReset(); setLabelsMap.mockReset(); markerSetMap.mockReset(); markerSetPosition.mockReset(); labelSetPosition.mockReset(); setZoomAndCenter.mockReset(); setCenter.mockReset(); panTo.mockReset(); mapOff.mockReset(); mapDestroy.mockReset(); massOff.mockReset(); getZoom.mockReset(); getZoom.mockReturnValue(5); getBounds.mockReset(); 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((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( undefined} /> ); expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument(); const retry = screen.getByRole('button', { name: /重新加载地图/ }); expect(retry).toHaveClass('semi-button', 'v2-map-retry-action'); fireEvent.click(retry); 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'); expect(decodeURIComponent(clusterStyles[5].url)).not.toContain('10+'); expect(load).toHaveBeenCalledTimes(2); expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument(); }); test('restores the saved monitor viewport without forcing the selected vehicle zoom', async () => { window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; window.AMapLoader = { load: vi.fn(async () => amapMock()) }; const bounds = '113.100000,23.000000,113.400000,23.300000'; render( undefined} />); await waitFor(() => expect(mapOptions).toHaveLength(1)); expect(mapOptions[0]).toEqual(expect.objectContaining({ zoom: 13, center: wgs84ToGcj02(113.25, 23.15) })); await waitFor(() => expect(markerSetMap).toHaveBeenCalled()); expect(setZoomAndCenter).not.toHaveBeenCalled(); 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( 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()) }; const view = render( 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( undefined} />); await waitFor(() => expect(setData).toHaveBeenCalled()); view.rerender( 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( 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( 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( 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( undefined} />); await waitFor(() => expect(addLabels).toHaveBeenCalled()); pointReads = 0; await act(async () => { view.rerender( 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( 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('drops a wrapped world viewport instead of emitting invalid WGS-84 bounds', async () => { getZoom.mockReturnValue(3); getBounds.mockReturnValue({ getSouthWest: () => ({ getLng: () => -80, getLat: () => -20 }), getNorthEast: () => ({ getLng: () => 260, getLat: () => 70 }) }); window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; window.AMapLoader = { load: vi.fn(async () => amapMock()) }; const onViewportChange = vi.fn(); render( 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).toEqual({ zoom: 3, bounds: '' }); }); 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( 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 }).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(''); 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( undefined} /> ); await waitFor(() => expect(markerSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 }); expect(setCenter).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)); expect(panTo).not.toHaveBeenCalled(); expect(setZoomAndCenter).toHaveBeenCalledTimes(1); const follow = screen.getByRole('button', { name: '跟随车辆' }); expect(follow).toHaveClass('semi-button', 'v2-map-control-button', 'v2-map-follow-control', 'is-active'); expect(follow).toHaveTextContent('跟随中'); expect(follow).toHaveAttribute('aria-pressed', 'true'); act(() => mapHandlers.get('dragstart')?.({})); expect(follow).toHaveAttribute('aria-pressed', 'false'); expect(follow).toHaveTextContent('已暂停'); const panCountAfterDrag = panTo.mock.calls.length; view.rerender( 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( 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).toHaveClass('semi-button', 'v2-map-control-button', 'v2-map-layer-control', 'is-active'); expect(toggle).toHaveTextContent('已显示'); expect(toggle).toHaveAttribute('aria-pressed', 'true'); fireEvent.click(toggle); await waitFor(() => expect(toggle).toHaveAttribute('aria-pressed', 'false')); expect(toggle).toHaveTextContent('已隐藏'); 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('does not treat intermediate programmatic follow frames as user viewport changes', async () => { useFastAnimationFrames(); window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; window.AMapLoader = { load: vi.fn(async () => amapMock()) }; const onViewportChange = vi.fn(); const view = render( undefined} onViewportChange={onViewportChange} />); await waitFor(() => expect(onViewportChange).toHaveBeenCalled()); onViewportChange.mockReset(); view.rerender( undefined} onViewportChange={onViewportChange} />); await waitFor(() => expect(setCenter).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 }); await waitFor(() => expect(onViewportChange).toHaveBeenCalledTimes(1)); }); 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( 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( 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(); }); 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( 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('3'); expect(String(markerOptions[provinceIndex]?.content)).toContain('广东'); expect(String(markerOptions[provinceIndex]?.content)).not.toContain('在线'); markerInstances[provinceIndex]?.handlers.get('click')?.(); expect(setZoomAndCenter).toHaveBeenLastCalledWith(7, [113.266, 23.132]); });