Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx
T

766 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <div data-testid="fleet-map" data-selected-vin={selectedVin ?? ''} data-initial-zoom={initialViewport?.zoom ?? ''} data-initial-bounds={initialViewport?.bounds ?? ''}>
<button type="button" onClick={() => onSelectVin?.('LTEST000000000002')}>选择地图车辆</button>
<button type="button" onClick={onOpenList}>使用车辆列表</button>
</div>;
}
}));
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(
<main className="v2-content" data-testid="monitor-scroll-owner">
<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>
</main>
);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={[entry]}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor?selectedVin=LTEST000000000001&detail=collapsed']}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor?keyword=%E7%B2%A4A12345&protocol=JT808&status=online']}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor?mode=list']}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(
<main className="v2-content" data-testid="mobile-monitor-scroll-owner">
<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>
</main>
);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(
<main className="v2-content" data-testid="mobile-exact-match-scroll-owner">
<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>
</main>
);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor?mode=list']}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
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);
});