import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import { MemoryRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { VehicleRealtimeRow } from '../../api/types'; import { api } from '../../api/client'; import MonitorPage from './MonitorPage'; import { ROUTER_FUTURE } from '../routing/routerConfig'; const vehicles = [{ vin: 'LTEST000000000001', plate: '粤A12345', protocols: ['JT808'], primaryProtocol: 'JT808', locationAvailable: true, longitude: 113.26, latitude: 23.13, speedAvailable: true, speedKmh: 42, socAvailable: false, socPercent: 0, mileageAvailable: true, totalMileageKm: 1234, todayMileageAvailable: true, todayMileageKm: 18.6, lastSeen: '2026-07-14T01:00:00Z', online: true, sourceCount: 1, onlineSourceCount: 1 }, { vin: 'LTEST000000000002', plate: '粤B67890', protocols: ['JT808'], primaryProtocol: 'JT808', locationAvailable: true, longitude: 113.28, latitude: 23.15, speedAvailable: true, speedKmh: 0, socAvailable: false, socPercent: 0, mileageAvailable: true, totalMileageKm: 2234, todayMileageAvailable: false, todayMileageKm: 0, lastSeen: '2026-07-14T01:00:00Z', online: true, sourceCount: 1, onlineSourceCount: 1 }] as VehicleRealtimeRow[]; const monitorMap = { clusters: [], points: [], total: 2 }; const fleetMapRenderSpy = vi.hoisted(() => vi.fn()); const vehicleCardArgsSpy = vi.hoisted(() => vi.fn()); const monitorDataArgsSpy = vi.hoisted(() => vi.fn()); const qrToDataURLSpy = vi.hoisted(() => vi.fn()); const monitorQueryFlags = vi.hoisted(() => ({ isLoading: false, isFetching: false, isPlaceholderData: false })); const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard'); vi.mock('../map/FleetMap', () => ({ FleetMap: ({ selectedVin, onSelectVin, initialViewport }: { selectedVin?: string; onSelectVin?: (vin: string) => void; initialViewport?: { zoom: number; bounds: string } }) => { fleetMapRenderSpy(selectedVin, initialViewport); return
; } })); vi.mock('qrcode', () => ({ default: { toDataURL: qrToDataURLSpy } })); vi.mock('../hooks/useMonitorData', () => ({ MAX_MONITOR_SEARCH_TERMS: 100, MONITOR_REFRESH: { selected: 10_000, fleet: 15_000, summary: 30_000 }, parseMonitorSearchTerms: (value: string) => Array.from(new Set(value.split(/[\s,,、;;]+/).map((item) => item.trim().toLocaleUpperCase()).filter(Boolean))).slice(0, 100), monitorQueryParams: (filters: { keyword: string; protocol: string; status: string }, limit: number) => { const params = new URLSearchParams({ limit: String(limit) }); const terms = Array.from(new Set(filters.keyword.split(/[\s,,、;;]+/).map((item) => item.trim().toLocaleUpperCase()).filter(Boolean))).slice(0, 100); if (terms.length === 1) params.set('keyword', terms[0]); if (terms.length > 1) params.set('keywords', terms.join(',')); if (filters.protocol) params.set('protocol', filters.protocol); if (filters.status) params.set('status', filters.status); return params; }, monitorFilterScope: (filters: { keyword: string; protocol: string; status: string }) => { const params = new URLSearchParams(); if (filters.keyword) params.set('keyword', filters.keyword); if (filters.protocol) params.set('protocol', filters.protocol); if (filters.status) params.set('status', filters.status); return params.toString(); }, useMonitorData: (...args: unknown[]) => { monitorDataArgsSpy(...args); return { summary: { data: { totalVehicles: 2, locationVehicles: 2, noLocationVehicles: 0, onlineVehicles: 2, offlineVehicles: 0, drivingVehicles: 1, idleVehicles: 1, frameToday: 10 } }, vehicles: { data: { items: vehicles, total: 2 }, isError: false, ...monitorQueryFlags }, map: { data: monitorMap, isPlaceholderData: monitorQueryFlags.isPlaceholderData }, selectedVehicle: { data: { items: [] } } }; }, useMonitorVehicleCard: (...args: unknown[]) => { vehicleCardArgsSpy(...args); return { detail: {}, activeAlerts: {}, address: {} }; } })); afterEach(() => { cleanup(); monitorQueryFlags.isLoading = false; monitorQueryFlags.isFetching = false; monitorQueryFlags.isPlaceholderData = false; vehicleCardArgsSpy.mockClear(); monitorDataArgsSpy.mockClear(); qrToDataURLSpy.mockReset(); if (originalClipboard) Object.defineProperty(navigator, 'clipboard', originalClipboard); else Reflect.deleteProperty(navigator, 'clipboard'); vi.restoreAllMocks(); }); test('starts without a selection and supports expand, collapse, reselection, and clear', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); const workspace = view.container.querySelector('.v2-monitor-workspace')!; const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ }); const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ }); expect(workspace).not.toHaveClass('is-detail-open'); expect(workspace).not.toHaveClass('is-detail-collapsed'); expect(firstVehicle).not.toHaveClass('is-selected'); expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument(); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', ''); expect(screen.getByText('车辆总数')).toBeInTheDocument(); expect(screen.getAllByText('无实时位置').length).toBeGreaterThanOrEqual(2); expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效' })).toHaveTextContent('实时筛选'); expect(screen.queryByRole('button', { name: '筛选' })).not.toBeInTheDocument(); expect(screen.queryByText('接入车辆')).not.toBeInTheDocument(); fireEvent.click(firstVehicle); expect(workspace).toHaveClass('is-detail-open'); expect(firstVehicle).toHaveClass('is-selected'); expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument(); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', 'LTEST000000000001'); expect(vehicleCardArgsSpy).toHaveBeenLastCalledWith('LTEST000000000001', vehicles[0], true); expect(screen.getByText('SOC').parentElement).toHaveTextContent('SOC—'); const mapRendersAfterSelection = fleetMapRenderSpy.mock.calls.length; const cardCallsAfterSelection = vehicleCardArgsSpy.mock.calls.length; fireEvent.click(screen.getByRole('button', { name: '收起车辆详情' })); expect(workspace).toHaveClass('is-detail-collapsed'); expect(firstVehicle).toHaveClass('is-selected'); expect(screen.getByRole('button', { name: '展开车辆详情' })).toBeInTheDocument(); expect(fleetMapRenderSpy).toHaveBeenCalledTimes(mapRendersAfterSelection); expect(vehicleCardArgsSpy).toHaveBeenCalledTimes(cardCallsAfterSelection); fireEvent.click(secondVehicle); expect(workspace).toHaveClass('is-detail-open'); expect(secondVehicle).toHaveClass('is-selected'); expect(screen.queryByRole('button', { name: '展开车辆详情' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '取消选择车辆' })); expect(workspace).not.toHaveClass('is-detail-open'); expect(workspace).not.toHaveClass('is-detail-collapsed'); expect(secondVehicle).not.toHaveClass('is-selected'); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', ''); fireEvent.click(screen.getByRole('button', { name: '选择地图车辆' })); expect(workspace).toHaveClass('is-detail-open'); expect(secondVehicle).toHaveClass('is-selected'); }); test('restores URL-backed monitor context and carries it into every vehicle workflow', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const entry = '/monitor?keyword=%E7%B2%A4A12345&protocol=JT808&status=online&selectedVin=LTEST000000000001&detail=collapsed&zoom=13&bounds=113.100000%2C23.000000%2C113.400000%2C23.300000'; render(); expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue('粤A12345'); expect(screen.getByRole('combobox', { name: '协议' })).toHaveValue('JT808'); expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveValue('online'); expect(screen.getByRole('button', { name: '展开车辆详情' })).toBeInTheDocument(); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-initial-zoom', '13'); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-initial-bounds', '113.100000,23.000000,113.400000,23.300000'); fireEvent.click(screen.getByRole('button', { name: '展开车辆详情' })); for (const name of ['单车详情', '轨迹回放', '历史数据', '里程查询']) { const target = new URL(screen.getByRole('link', { name }).getAttribute('href')!, 'https://vehicle-platform.invalid'); const monitorReturn = target.searchParams.get('monitorReturn'); expect(monitorReturn).toContain('/monitor?'); expect(monitorReturn).toContain('selectedVin=LTEST000000000001'); expect(monitorReturn).toContain('zoom=13'); } }); test('switches to a lightweight realtime list and resolves addresses only on demand', async () => { const row = vehicles[0]; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [row], total: 1, limit: 50, offset: 0 }); const reverseGeocode = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市天河区测试路' }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(screen.getByRole('button', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(screen.getByText('覆盖全部授权车辆;实时字段缺失时显示“—”,文字地址按需解析')).toBeInTheDocument(); expect(await screen.findAllByText('粤A12345')).toHaveLength(1); expect(screen.getAllByText('42')).toHaveLength(1); expect(screen.getAllByText(/1,234/)).toHaveLength(1); expect(screen.getAllByText(/18\.6/)).toHaveLength(1); expect(screen.getAllByText(/113\.260000/)).toHaveLength(1); expect(reverseGeocode).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '解析粤A12345位置' })); expect(await screen.findByText('广东省广州市天河区测试路')).toBeInTheDocument(); expect(screen.getByText(/解析于 \d{2}:\d{2}/)).toBeInTheDocument(); expect(reverseGeocode).toHaveBeenCalledTimes(1); expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(1); expect(screen.queryByText('综合状态')).not.toBeInTheDocument(); expect(screen.queryByText('三路正常')).not.toBeInTheDocument(); expect(screen.queryByTestId('fleet-map')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /地图/ })); await waitFor(() => expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(0)); view.unmount(); }); test('keeps authorized vehicles without realtime coordinates in the list without geocoding them', async () => { const row = { ...vehicles[0], vin: 'LNOLOCATION0000001', plate: '粤A无定位', protocols: [], primaryProtocol: '', locationAvailable: false, longitude: 0, latitude: 0, speedAvailable: false, speedKmh: 0, mileageAvailable: false, totalMileageKm: 0, todayMileageAvailable: false, todayMileageKm: 0, lastSeen: '', online: false, sourceCount: 0, onlineSourceCount: 0 } satisfies VehicleRealtimeRow; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [row], total: 1, limit: 50, offset: 0 }); const reverseGeocode = vi.spyOn(api, 'reverseGeocode'); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /列表/ })); expect(await screen.findByText('粤A无定位')).toBeInTheDocument(); expect(screen.getByText('暂无实时位置')).toBeInTheDocument(); expect(screen.getByText('从未上报')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /解析粤A无定位位置/ })).not.toBeInTheDocument(); expect(reverseGeocode).not.toHaveBeenCalled(); }); test('pauses selected-vehicle polling in list mode and resumes it when returning to the map', () => { vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), 'LTEST000000000001', true, true); fireEvent.click(screen.getByRole('button', { name: /列表/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), '', false, false); fireEvent.click(screen.getByRole('button', { name: /地图/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), 'LTEST000000000001', true, true); }); test('loads the QR generator only after opening the mobile entry', async () => { qrToDataURLSpy.mockResolvedValue('data:image/png;base64,vehicle-monitor'); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); expect(qrToDataURLSpy).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: /手机端/ })); expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toHaveAttribute('src', 'data:image/png;base64,vehicle-monitor'); expect(qrToDataURLSpy).toHaveBeenCalledTimes(1); expect(qrToDataURLSpy.mock.calls[0][0]).toMatch(/\/monitor$/); expect(qrToDataURLSpy.mock.calls[0][0]).not.toContain('token'); }); test('closes the mobile entry with Escape and reports clipboard success', async () => { qrToDataURLSpy.mockResolvedValue('data:image/png;base64,vehicle-monitor'); const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /手机端/ })); expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制访问地址' })); expect(await screen.findByRole('button', { name: '已复制访问地址' })).toBeInTheDocument(); expect(writeText).toHaveBeenCalledWith(expect.stringMatching(/\/monitor$/)); fireEvent.keyDown(window, { key: 'Escape' }); expect(screen.queryByRole('dialog', { name: '手机端入口' })).not.toBeInTheDocument(); }); test('shows a retry action when on-demand QR generation fails', async () => { qrToDataURLSpy.mockRejectedValueOnce(new Error('canvas unavailable')).mockResolvedValue('data:image/png;base64,recovered'); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /手机端/ })); expect(await screen.findByRole('alert')).toHaveTextContent('二维码生成失败'); fireEvent.click(screen.getByRole('button', { name: '重新生成' })); expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toHaveAttribute('src', 'data:image/png;base64,recovered'); expect(qrToDataURLSpy).toHaveBeenCalledTimes(2); }); test('mounts only the mobile list representation and removes its viewport listener', async () => { const addEventListener = vi.fn(); const removeEventListener = vi.fn(); vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true, media: '(max-width: 760px)', onchange: null, addEventListener, removeEventListener, addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(() => false) } as unknown as MediaQueryList); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [vehicles[0]], total: 1, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(screen.getByRole('button', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards article')).toHaveLength(1)); expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument(); expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function)); view.unmount(); expect(removeEventListener).toHaveBeenCalledWith('change', expect.any(Function)); }); test('pastes, deduplicates, and submits multiple plates as one batch search', async () => { const vehicleRealtime = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const input = screen.getByRole('textbox', { name: '搜索车辆' }); fireEvent.paste(input, { clipboardData: { getData: () => '粤a12345\n粤B67890,粤A12345' } }); expect(input).toHaveValue('粤A12345,粤B67890'); expect(screen.getAllByText('已找到 2/2')).toHaveLength(3); fireEvent.click(screen.getByRole('button', { name: /列表/ })); await waitFor(() => { const params = vehicleRealtime.mock.calls[vehicleRealtime.mock.calls.length - 1]?.[0]; expect(params?.get('keywords')).toBe('粤A12345,粤B67890'); expect(params?.has('keyword')).toBe(false); }); }); test('opens an explicit batch editor and applies copied plate rows', async () => { const vehicleRealtime = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: '批量' })); const editor = screen.getByRole('textbox', { name: /每行一个车牌/ }); fireEvent.change(editor, { target: { value: '粤a12345\n粤B67890\n粤A12345' } }); expect(screen.getByText('2', { selector: '.v2-batch-search-summary strong' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '应用搜索(2)' })); expect(screen.queryByRole('dialog', { name: '批量搜索车辆' })).not.toBeInTheDocument(); expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue('粤A12345,粤B67890'); fireEvent.click(screen.getByRole('button', { name: /列表/ })); await waitFor(() => { const params = vehicleRealtime.mock.calls[vehicleRealtime.mock.calls.length - 1]?.[0]; expect(params?.get('keywords')).toBe('粤A12345,粤B67890'); }); }); test('hides stale fleet rows while a pasted batch scope is still loading', () => { monitorQueryFlags.isLoading = true; monitorQueryFlags.isPlaceholderData = true; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.paste(screen.getByRole('textbox', { name: '搜索车辆' }), { clipboardData: { getData: () => '粤A12345\n粤B67890' } }); expect(screen.getByText('正在查找 2 辆车')).toBeInTheDocument(); expect(screen.getByText('查询中')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /粤A12345 LTEST000000000001/ })).not.toBeInTheDocument(); }); test('keeps same-filter fleet rows visible while a viewport placeholder refreshes', () => { monitorQueryFlags.isPlaceholderData = true; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); expect(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })).toBeInTheDocument(); expect(screen.queryByText('加载车辆')).not.toBeInTheDocument(); expect(screen.getByTestId('fleet-map')).toBeInTheDocument(); }); test('reports pasted plates that have no exact realtime match', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.paste(screen.getByRole('textbox', { name: '搜索车辆' }), { clipboardData: { getData: () => '粤A12345\n粤Z99999' } }); expect(screen.getByText('已找到 1/2')).toBeInTheDocument(); expect(screen.getAllByText('已找到 1/2 · 未找到 1 辆')).toHaveLength(2); expect(screen.getAllByTitle('未找到:粤Z99999')).toHaveLength(2); });