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( undefined} onFollowChange={() => undefined} />); expect(load).not.toHaveBeenCalled(); expect(mapInstances).toHaveLength(0); expect(view.container.querySelector('.v2-map-state')).not.toBeInTheDocument(); view.rerender( 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( 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( 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( undefined} onFollowChange={() => undefined} />); await waitFor(() => expect(mapInstances).toHaveLength(1)); view.rerender( 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( 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); });