import { cleanup, fireEvent, render, screen, waitFor, within } 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 = { mode: 'provinces', clusters: [], points: [], total: 2, truncated: false }; 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 monitorFleetFixture = vi.hoisted(() => ({ total: 2 })); const monitorSummaryFixture = vi.hoisted(() => ({ totalVehicles: 2, locationVehicles: 2, noLocationVehicles: 0, onlineVehicles: 2, offlineVehicles: 0, drivingVehicles: 1, idleVehicles: 1, frameToday: 10, truncated: false })); const addressRefetchSpy = vi.hoisted(() => vi.fn()); const vehicleCardFixture = vi.hoisted(() => ({ detail: undefined as unknown, addressData: undefined as unknown, addressError: false, addressFetching: false })); const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard'); function mockMobileViewport() { 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); return { addEventListener, removeEventListener }; } vi.mock('../map/FleetMap', () => ({ FleetMap: ({ selectedVin, onSelectVin, initialViewport, onOpenList }: { selectedVin?: string; onSelectVin?: (vin: string) => void; initialViewport?: { zoom: number; bounds: string }; onOpenList?: () => void }) => { 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: monitorSummaryFixture }, vehicles: { data: { items: vehicles, total: monitorFleetFixture.total }, isError: false, ...monitorQueryFlags }, map: { data: monitorMap, isPlaceholderData: monitorQueryFlags.isPlaceholderData }, selectedVehicle: { data: { items: [] } } }; }, useMonitorVehicleCard: (...args: unknown[]) => { vehicleCardArgsSpy(...args); return { detail: { data: vehicleCardFixture.detail }, activeAlerts: {}, address: { data: vehicleCardFixture.addressData, isError: vehicleCardFixture.addressError, isFetching: vehicleCardFixture.addressFetching, refetch: addressRefetchSpy } }; } })); afterEach(() => { cleanup(); monitorQueryFlags.isLoading = false; monitorQueryFlags.isFetching = false; monitorQueryFlags.isPlaceholderData = false; monitorSummaryFixture.frameToday = 10; monitorSummaryFixture.totalVehicles = 2; monitorSummaryFixture.truncated = false; monitorFleetFixture.total = 2; monitorMap.truncated = false; monitorMap.total = 2; vehicleCardFixture.detail = undefined; vehicleCardFixture.addressData = undefined; vehicleCardFixture.addressError = false; vehicleCardFixture.addressFetching = false; addressRefetchSpy.mockReset(); vehicleCardArgsSpy.mockClear(); monitorDataArgsSpy.mockClear(); qrToDataURLSpy.mockReset(); if (originalClipboard) Object.defineProperty(navigator, 'clipboard', originalClipboard); else Reflect.deleteProperty(navigator, 'clipboard'); vi.restoreAllMocks(); }); test('never labels the latest historical mileage row as today when today has no mileage evidence', async () => { vehicleCardFixture.detail = { sources: ['JT808'], sourceStatus: [], mileage: { items: [{ vin: vehicles[1].vin, plate: vehicles[1].plate, date: '2026-07-16', startMileageKm: 2100, endMileageKm: 2234, dailyMileageKm: 134, source: 'JT808' }], total: 1, limit: 20, offset: 0 } }; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ })); const todayMetric = (await screen.findByText('今日里程')).parentElement; expect(todayMetric).toHaveTextContent('今日里程—'); expect(todayMetric).not.toHaveTextContent('134'); }); test('keeps a large daily report count readable in the desktop summary rail', () => { monitorSummaryFixture.frameToday = 2_861_323; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const dailyReports = screen.getByTitle('今日上报 2,861,323 条'); expect(dailyReports).toHaveTextContent('今日上报286.1万条数据活跃度'); expect(dailyReports).not.toHaveTextContent('2,861,323'); }); test('makes a truncated ten-thousand-vehicle map scope explicit and offers the complete paged list', async () => { monitorSummaryFixture.totalVehicles = 10_000; monitorSummaryFixture.truncated = true; monitorFleetFixture.total = 12_480; monitorMap.total = 8_720; monitorMap.truncated = true; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 12_480, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); const coverage = await screen.findByRole('status', { name: '地图与统计载荷已达到上限' }); expect(coverage).toHaveTextContent('已处理前 10,000 辆'); expect(coverage).toHaveTextContent('当前筛选共 12,480 辆'); expect(screen.getByLabelText('实时数据状态')).toHaveTextContent('地图覆盖8,720 / 12,480 辆有定位 · 省级聚合'); expect(screen.getByText('车辆总数').parentElement).toHaveTextContent('12,480辆'); fireEvent.click(screen.getByRole('button', { name: '查看完整车辆列表' })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(screen.queryByRole('status', { name: '地图与统计载荷已达到上限' })).not.toBeInTheDocument(); }); test('keeps coordinates usable and exposes retry when selected-vehicle address lookup fails', async () => { vehicleCardFixture.addressError = true; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })); expect(await screen.findByRole('alert')).toHaveTextContent('地址解析失败,坐标仍可用'); expect(screen.getByRole('button', { name: '查看全部位置来源' })).toHaveTextContent('113.260000, 23.130000'); fireEvent.click(screen.getByRole('button', { name: '重试' })); expect(addressRefetchSpy).toHaveBeenCalledTimes(1); }); test('starts without a selection and supports expand, collapse, reselection, and clear', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); const workspace = view.container.querySelector('.v2-monitor-workspace')!; const monitorPage = view.container.querySelector('.v2-monitor-page')!; const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ }); const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ }); expect(firstVehicle).toHaveClass('semi-button', 'v2-vehicle-row'); expect(view.container.querySelector('.v2-filterbar')).toHaveClass('semi-card'); const kpis = screen.getByRole('list', { name: '车辆整体统计' }); expect(kpis.closest('.v2-monitor-summary-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue'); expect(Array.from(kpis.querySelectorAll(':scope > [role="listitem"] small')).map((item) => item.textContent)).toEqual([ '车辆总数', '当前在线', '行驶车辆', '当前离线', '静止车辆', '告警车辆' ]); expect(kpis.querySelectorAll(':scope > [role="listitem"]')).toHaveLength(6); expect(kpis.querySelector(':scope > [role="listitem"]:nth-child(1)')).toHaveClass('is-primary'); expect(kpis.querySelector(':scope > [role="listitem"]:nth-child(2)')).toHaveClass('is-primary', 'is-success'); expect(kpis.querySelector(':scope > [role="listitem"]:nth-child(4)')).toHaveClass('is-secondary'); const summarySupport = view.container.querySelector('.v2-monitor-summary-support'); expect(summarySupport).toHaveTextContent('今日上报10条数据活跃度'); expect(summarySupport).toHaveTextContent('无实时位置0辆辅助排查'); expect(view.container.querySelector('.v2-kpis')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-event-strip')).toHaveClass('semi-card'); expect(view.container.querySelector('.v2-monitor-live-tag')).toHaveClass('semi-tag'); const liveStatus = screen.getByLabelText('实时数据状态'); expect(liveStatus).toHaveTextContent('数据已同步'); expect(liveStatus).toHaveTextContent('实时数据2 辆已载入'); expect(liveStatus).toHaveTextContent('地图覆盖2 / 2 辆有定位 · 省级聚合'); expect(liveStatus).toHaveTextContent('智能刷新重点 10s · 车队 15s · 统计 30s'); expect(liveStatus).toHaveTextContent('最近同步等待数据'); expect(workspace).not.toHaveClass('is-detail-open'); expect(workspace).not.toHaveClass('is-detail-collapsed'); expect(monitorPage).toHaveClass('is-map-mode'); 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.getByText('无实时位置')).toBeInTheDocument(); expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 0 个条件' })).toHaveTextContent('自动生效'); expect(screen.getByRole('button', { name: /清空/ })).toBeDisabled(); expect(screen.queryByRole('button', { name: '筛选' })).not.toBeInTheDocument(); expect(screen.queryByText('接入车辆')).not.toBeInTheDocument(); fireEvent.click(firstVehicle); expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument(); expect(workspace).toHaveClass('is-detail-open'); expect(monitorPage).toHaveClass('is-map-mode'); expect(firstVehicle).toHaveClass('is-selected'); expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument(); expect(view.container.querySelector('.v2-vehicle-detail')).toHaveClass('semi-card'); expect(screen.getByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument(); expect(screen.getByRole('navigation', { name: '车辆快捷操作' })).toBeInTheDocument(); for (const name of ['最新上报', '实时状态', '车辆信息']) { expect(screen.getByRole('heading', { name, level: 5 })).toBeInTheDocument(); } expect(screen.getByText('身份、车型与来源覆盖')).toBeInTheDocument(); expect(screen.getByText('1/1 在线').closest('.semi-tag')).toBeInTheDocument(); expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('平台接收时间'); expect(view.container.querySelector('.v2-report-summary')).toHaveTextContent('主协议JT/T 808'); expect(view.container.querySelector('.v2-report-summary .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light'); expect(view.container.querySelectorAll('.v2-detail-actions a')).toHaveLength(6); expect(Array.from(view.container.querySelectorAll('.v2-detail-actions a')).map((item) => item.textContent)).toEqual([ '单车详情', '轨迹回放', '历史数据', '里程查询', '事件流', '质量差异' ]); expect(new URL(screen.getByRole('link', { name: '事件流' }).getAttribute('href')!, 'https://vehicle-platform.invalid').searchParams.get('keyword')).toBe('LTEST000000000001'); expect(new URL(screen.getByRole('link', { name: '质量差异' }).getAttribute('href')!, 'https://vehicle-platform.invalid').searchParams.get('reconcileKeyword')).toBe('LTEST000000000001'); expect(view.container.querySelector('.v2-detail-protocols .v2-protocol-tag')).toHaveTextContent('JT/T 808'); expect(screen.getByRole('list', { name: '车辆实时状态指标' })).toHaveClass('v2-workspace-metric-list'); expect(view.container.querySelector('.v2-monitor-detail-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue'); expect(view.container.querySelectorAll('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > [role="listitem"]')).toHaveLength(4); expect(view.container.querySelector('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度42'); expect(view.container.querySelector('.v2-monitor-detail-metric-rail .v2-workspace-metric-list > span:nth-child(2)')).toHaveTextContent('今日里程18.6km'); expect(view.container.querySelector('.v2-monitor-detail-state')).toHaveTextContent('车辆状态行驶当前告警0条'); expect(view.container.querySelector('.v2-status-text')).toHaveClass('semi-tag'); expect(screen.getByRole('button', { name: '取消选择车辆' })).toHaveClass('semi-button'); expect(screen.getByRole('button', { name: '查看总里程全部来源' })).toHaveClass('semi-button', 'v2-workspace-metric-action'); expect(screen.getByRole('button', { name: '查看全部位置来源' })).toHaveClass('semi-button', 'v2-detail-source-link'); 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(view.container.querySelector('.v2-detail-peek')).toHaveClass('semi-card'); 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('returns the monitor scroll owner to the command bar when changing views', async () => { vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(
); const scrollOwner = screen.getByTestId('monitor-scroll-owner'); const monitorPage = view.container.querySelector('.v2-monitor-page')!; scrollOwner.scrollTop = 180; fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(scrollOwner.scrollTop).toBe(0); expect(monitorPage).toHaveClass('is-list-mode'); scrollOwner.scrollTop = 120; fireEvent.click(screen.getByRole('tab', { name: /地图/ })); expect(scrollOwner.scrollTop).toBe(0); expect(monitorPage).toHaveClass('is-map-mode'); }); test('restores URL-backed monitor context and carries it into every vehicle workflow', async () => { 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: '协议' })).toHaveTextContent('JT808'); expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('在线'); 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: '展开车辆详情' })); expect(await screen.findByRole('navigation', { name: '车辆快捷操作' })).toBeInTheDocument(); 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('turns the fleet summary into consistent status filters and clears stale vehicle selection', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })); expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument(); const drivingFilter = screen.getByRole('button', { name: '筛选行驶车辆,共 1 辆' }); fireEvent.click(drivingFilter); expect(screen.queryByRole('region', { name: '粤A12345车辆详情' })).not.toBeInTheDocument(); expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('行驶'); expect(drivingFilter).toHaveAttribute('aria-pressed', 'true'); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', ''); fireEvent.click(screen.getByRole('button', { name: '筛选无实时位置车辆,共 0 辆' })); expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('无实时位置'); expect(screen.getByRole('button', { name: '筛选无实时位置车辆,共 0 辆' })).toHaveAttribute('aria-pressed', 'true'); }); test('keeps the current map selection visible when reviewing the realtime list', async () => { vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByTitle('当前地图选中,点击返回地图')).toHaveClass('is-selected'); expect(view.container.querySelector('.v2-monitor-table .semi-table-row.is-selected')).toBeInTheDocument(); }); test('shows active filter count and clears all filter fields in one action', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 3 个条件' })).toHaveTextContent('自动生效3'); const clear = screen.getByRole('button', { name: /清空/ }); expect(clear).toBeEnabled(); fireEvent.click(clear); expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue(''); expect(screen.getByRole('combobox', { name: '协议' })).toHaveTextContent('全部协议'); expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('全部状态'); expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效,已启用 0 个条件' })).toHaveTextContent('自动生效'); expect(clear).toBeDisabled(); }); test('switches to a lightweight realtime list and resolves addresses only on demand', async () => { const row = vehicles[0]; const realtime = 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(); const modeGroup = screen.getByRole('tablist', { name: '监控视图' }); const mapMode = within(modeGroup).getByRole('tab', { name: /地图/ }); const listMode = within(modeGroup).getByRole('tab', { name: /列表/ }); expect(mapMode).toHaveAttribute('aria-selected', 'true'); expect(mapMode).toHaveClass('is-active'); expect(listMode).toHaveAttribute('aria-selected', 'false'); fireEvent.click(listMode); expect(mapMode).toHaveAttribute('aria-selected', 'false'); expect(listMode).toHaveAttribute('aria-selected', 'true'); expect(listMode).toHaveClass('is-active'); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '车辆实时列表', level: 5 })).toBeInTheDocument(); expect(view.container.querySelector('.v2-monitor-table-panel')).toHaveClass('semi-card'); expect(view.container.querySelector('.v2-monitor-table.semi-table-wrapper')).toBeInTheDocument(); expect(view.container.querySelector('.v2-monitor-table-scroll > table')).not.toBeInTheDocument(); expect(screen.getByText('覆盖全部授权车辆;缺失值显示“—”,地址按需解析')).toBeInTheDocument(); expect(await screen.findAllByText('粤A12345')).toHaveLength(1); expect(realtime.mock.calls[realtime.mock.calls.length - 1]?.[0]?.get('sort')).toBe('identity'); expect(screen.getAllByText('42')).toHaveLength(1); expect(screen.getAllByText(/18\.6/)).toHaveLength(1); expect(screen.getAllByText(/1,234/)).toHaveLength(1); expect(screen.getByText('JT/T 808')).toBeInTheDocument(); expect(view.container.querySelector('.v2-monitor-protocol .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light'); expect(screen.getAllByText(/113\.260000/)).toHaveLength(1); expect(screen.queryByText('推荐来源')).not.toBeInTheDocument(); expect(screen.getByText('当日里程')).toBeInTheDocument(); expect(screen.getByText('协议来源')).toBeInTheDocument(); expect(screen.queryByText('最新上报')).not.toBeInTheDocument(); expect(reverseGeocode).not.toHaveBeenCalled(); expect(screen.getByRole('button', { name: '解析粤A12345位置' })).toHaveClass('semi-button', 'v2-monitor-address-action'); 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('tab', { name: /地图/ })); await waitFor(() => expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(0)); view.unmount(); }); test('shows one recovery state instead of stacking an empty state below a failed list query', async () => { vi.spyOn(api, 'vehicleRealtime').mockRejectedValue(new Error('当前账号无权查看该车辆')); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('当前账号无权查看该车辆')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '重试' })).toBeInTheDocument(); expect(screen.queryByText('暂无符合条件的车辆')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-monitor-table-scroll')).toHaveClass('is-error'); }); 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('tab', { name: /列表/ })); expect(await screen.findByText('粤A无定位')).toBeInTheDocument(); expect(screen.getByText('暂无实时位置')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /解析粤A无定位位置/ })).not.toBeInTheDocument(); expect(reverseGeocode).not.toHaveBeenCalled(); }); test('keeps the desktop realtime list available when a runtime response omits protocols', async () => { const runtimeRow = { ...vehicles[0], protocols: null, primaryProtocol: '' } as unknown as VehicleRealtimeRow; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [runtimeRow], total: 1, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); expect(await screen.findByText('粤A12345')).toBeInTheDocument(); expect(screen.getByText('待识别')).toBeInTheDocument(); expect(screen.queryByText('当前模块暂时无法显示')).not.toBeInTheDocument(); }); 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('tab', { name: /列表/ })); expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), '', false, false); fireEvent.click(screen.getByRole('tab', { 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('dialog', { name: '手机端全局监控' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '关闭手机端入口' })).toBeInTheDocument(); 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(document.body, { key: 'Escape', code: 'Escape', keyCode: 27 }); await waitFor(() => 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, removeEventListener } = mockMobileViewport(); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [vehicles[0]], total: 1, limit: 50, offset: 0 }); monitorSummaryFixture.frameToday = 2_861_323; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(
); const filterFields = view.container.querySelector('#monitor-filter-fields'); const scrollOwner = screen.getByTestId('mobile-monitor-scroll-owner'); expect(filterFields).toHaveClass('is-mobile-collapsed'); expect(screen.queryByRole('button', { name: '打开手机端入口' })).not.toBeInTheDocument(); expect(screen.getByRole('list', { name: '车辆整体统计' }).querySelectorAll(':scope > [role="listitem"]')).toHaveLength(6); const expandFilters = screen.getByRole('button', { name: '展开车辆筛选,当前 0 个条件' }); expect(expandFilters).toHaveAttribute('aria-expanded', 'false'); scrollOwner.scrollTop = 180; fireEvent.click(expandFilters); expect(scrollOwner.scrollTop).toBe(0); expect(filterFields).not.toHaveClass('is-mobile-collapsed'); const collapseFilters = screen.getByRole('button', { name: '收起车辆筛选,当前 0 个条件' }); expect(collapseFilters).toHaveAttribute('aria-expanded', 'true'); scrollOwner.scrollTop = 120; fireEvent.click(collapseFilters); expect(scrollOwner.scrollTop).toBe(0); expect(filterFields).toHaveClass('is-mobile-collapsed'); fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards .v2-monitor-mobile-card.semi-card')).toHaveLength(1)); const mobileCard = view.container.querySelector('.v2-monitor-mobile-card'); expect(mobileCard).toHaveAttribute('aria-label', '粤A12345 实时数据'); expect(mobileCard?.querySelector('.v2-monitor-mobile-primary')).toHaveTextContent('速度42 km/h当日里程18.6 km总里程1,234 km协议来源JT/T 808'); expect(mobileCard?.querySelectorAll('.v2-monitor-mobile-primary > div')).toHaveLength(4); expect(mobileCard?.querySelector('.v2-monitor-mobile-location')).toHaveTextContent('113.260000, 23.130000解析位置'); expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '在地图中定位粤A12345' })).toHaveTextContent('定位'); expect(screen.getByTitle('今日上报 2,861,323 条')).toHaveTextContent('今日上报286.1万条'); expect(view.container.querySelector('.v2-monitor-mobile-card footer')).not.toBeInTheDocument(); expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function)); view.unmount(); expect(removeEventListener).toHaveBeenCalledWith('change', expect.any(Function)); }); test('returns a mobile list-origin vehicle directly to its selected list card', async () => { mockMobileViewport(); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(await screen.findByRole('button', { name: '在地图中定位粤B67890' })).toBeInTheDocument(); const scrollOwner = screen.getByLabelText('车辆实时列表'); scrollOwner.scrollTop = 180; fireEvent.scroll(scrollOwner); fireEvent.click(screen.getByRole('button', { name: '在地图中定位粤B67890' })); expect(await screen.findByRole('region', { name: '粤B67890车辆详情' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '返回车辆列表' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '返回车辆列表' })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(view.container.querySelector('.v2-monitor-mobile-card.is-selected')).toBeInTheDocument(); expect(screen.getByLabelText('粤B67890 实时数据,当前地图选中')).toBeInTheDocument(); }); test('opens an exact mobile search match directly and does not reopen it after dismissal', async () => { mockMobileViewport(); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(
); fireEvent.click(screen.getByRole('button', { name: '展开车辆筛选,当前 0 个条件' })); fireEvent.change(screen.getByRole('textbox', { name: '搜索车辆' }), { target: { value: '粤A12345' } }); expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '展开车辆筛选,当前 1 个条件' })).toHaveAttribute('aria-expanded', 'false'); const scrollOwner = screen.getByTestId('mobile-exact-match-scroll-owner'); scrollOwner.scrollTop = 120; fireEvent.click(screen.getByRole('button', { name: '取消选择车辆' })); expect(screen.queryByRole('region', { name: '粤A12345车辆详情' })).not.toBeInTheDocument(); await waitFor(() => expect(scrollOwner.scrollTop).toBe(0)); expect(screen.queryByRole('region', { name: '粤A12345车辆详情' })).not.toBeInTheDocument(); }); test('renders missing and multiple runtime protocol sources on mobile without crashing', async () => { mockMobileViewport(); const runtimeRows = [{ ...vehicles[0], protocols: null, primaryProtocol: '' }, { ...vehicles[1], protocols: ['GB32960', 'JT808'], primaryProtocol: 'GB32960' }] as unknown as VehicleRealtimeRow[]; vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: runtimeRows, total: 2, limit: 50, offset: 0 }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-card')).toHaveLength(2)); expect(screen.getByText('待识别')).toBeInTheDocument(); expect(screen.getByText('2 路')).toBeInTheDocument(); expect(screen.queryByText('当前模块暂时无法显示')).not.toBeInTheDocument(); }); 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('tab', { 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 = await screen.findByRole('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('tab', { 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); });