import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import App from '../App'; afterEach(() => { cleanup(); window.history.replaceState(null, '', '/'); delete window.__LINGNIU_APP_CONFIG__; vi.restoreAllMocks(); }); test('renders vehicle platform shell', () => { vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0, linkHealth: [], runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) }) as Response); render(); expect(screen.getByText('车辆服务中台')).toBeInTheDocument(); expect(screen.getByText('车辆地图 / 里程统计 / 数据交付')).toBeInTheDocument(); expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('以车辆为对象监控在线、位置和服务状态')).toBeInTheDocument(); expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('轨迹、里程、历史数据和客户导出')).toBeInTheDocument(); expect(screen.getByText('告警闭环')).toBeInTheDocument(); expect(screen.getByText('平台运维')).toBeInTheDocument(); expect(screen.getAllByText('客户').length).toBeGreaterThanOrEqual(3); expect(screen.getByText('内部')).toBeInTheDocument(); expect(screen.getByText('客户主流程外查看接入链路和运行质量')).toBeInTheDocument(); expect(screen.getAllByText('运营驾驶舱').length).toBeGreaterThanOrEqual(1); }); test('exposes AMap operations shortcuts when map key is configured', async () => { window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704153357' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 0, activeToday: 0, frameToday: 0, issueVehicles: 0, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 0, boundVehicles: 0, onlineVehicles: 0, singleSourceVehicles: 0, multiSourceVehicles: 0, noDataVehicles: 0, identityRequiredVehicles: 0, archiveIncompleteVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [], archiveMissingFields: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 0, recordCount: 0, sourceCount: 0, totalMileageKm: 0, averageMileagePerVin: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { rules: [], policies: [], priorityIssues: [], activeRuleCount: 0, p0RuleCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/source-readiness')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, onlineVehicles: 208, kafkaLag: 42, activeConnections: 1234, redisOnlineKeys: 368, platformRelease: 'platform-ops-test', sources: [ { protocol: 'GB32960', role: '整车与氢能实时数据主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 42', action: '核对现代和交投平台转发清单。', acceptance: '缺失来源车辆下降,单源车辆可解释。', vehiclesHash: '#/vehicles?protocol=GB32960&missingProtocol=GB32960', realtimeHash: '#/realtime?protocol=GB32960&online=online', historyHash: '#/history-query?protocol=GB32960&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=GB32960' }, { protocol: 'JT808', role: '位置、总里程和设备在线主来源', online: 174, total: 424, onlineRate: 41.04, missingVehicles: 609, severity: 'ok', status: '生产可用', evidence: '在线 174/424,缺失车辆 609,Kafka Lag 42', action: '保持实时、轨迹、RAW 和统计链路巡检。', acceptance: '车辆服务可回查实时、轨迹和 RAW 证据。', vehiclesHash: '#/vehicles?protocol=JT808&missingProtocol=JT808', realtimeHash: '#/realtime?protocol=JT808&online=online', historyHash: '#/history-query?protocol=JT808&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=JT808' } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('地图就绪')).toBeInTheDocument(); expect(await screen.findByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' })); expect(window.location.hash).toBe('#/map'); expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument(); expect(screen.getAllByText('关注车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('车辆列表')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' })); expect(window.location.hash).toBe('#/realtime'); fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' })); expect(window.location.hash.startsWith('#/history')).toBe(true); fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw'); expect(await screen.findByRole('heading', { name: '数据导出' })).toBeInTheDocument(); expect(screen.getByText('默认查询最近一天,不预置任何车辆;输入 VIN、车牌或手机号后进入单车证据交付。')).toBeInTheDocument(); expect(screen.queryByDisplayValue('LB9A32A24R0LS1426')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部里程统计' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' })); expect(window.location.hash).toBe('#/alert-events'); }); test('shows global alert pressure from notification plan in topbar', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704180000' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { summary: { issueVehicleCount: 5, issueRecordCount: 8, errorCount: 2, warningCount: 6, protocols: [], issueTypes: [] }, rules: [], policies: [], priorityIssues: [], activeRuleCount: 4, p0RuleCount: 2 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/source-readiness')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, onlineVehicles: 208, kafkaLag: 42, activeConnections: 1234, redisOnlineKeys: 368, platformRelease: 'platform-ops-test', sources: [ { protocol: 'GB32960', role: '整车与氢能实时数据主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 42', action: '核对现代和交投平台转发清单。', acceptance: '缺失来源车辆下降,单源车辆可解释。', vehiclesHash: '#/vehicles?protocol=GB32960&missingProtocol=GB32960', realtimeHash: '#/realtime?protocol=GB32960&online=online', historyHash: '#/history-query?protocol=GB32960&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=GB32960' }, { protocol: 'JT808', role: '位置、总里程和设备在线主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 42', action: '补齐手机号绑定和注册缺失车辆。', acceptance: '未知 VIN 注册下降,位置与里程持续可查。', vehiclesHash: '#/vehicles?protocol=JT808&missingProtocol=JT808', realtimeHash: '#/realtime?protocol=JT808&online=online', historyHash: '#/history-query?protocol=JT808&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=JT808' } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/source-readiness')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, onlineVehicles: 208, kafkaLag: 0, activeConnections: 1234, redisOnlineKeys: 368, platformRelease: 'platform-dashboard-test', sources: [ { protocol: 'GB32960', role: '整车与氢能实时数据主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 0', action: '核对现代和交投平台转发清单。', acceptance: '缺失来源车辆下降,单源车辆可解释。', vehiclesHash: '#/vehicles?protocol=GB32960&missingProtocol=GB32960', realtimeHash: '#/realtime?protocol=GB32960&online=online', historyHash: '#/history-query?protocol=GB32960&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=GB32960' }, { protocol: 'JT808', role: '位置、总里程和设备在线主来源', online: 174, total: 424, onlineRate: 41.04, missingVehicles: 609, severity: 'warning', status: '覆盖不足', evidence: '在线 174/424,缺失车辆 609,Kafka Lag 0', action: '补齐手机号绑定和注册缺失车辆。', acceptance: '未知 VIN 注册下降,位置与里程持续可查。', vehiclesHash: '#/vehicles?protocol=JT808&missingProtocol=JT808', realtimeHash: '#/realtime?protocol=JT808&online=online', historyHash: '#/history-query?protocol=JT808&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=JT808' }, { protocol: 'YUTONG_MQTT', role: '宇通派生工况与燃料电池补充来源', online: 11, total: 50, onlineRate: 22, missingVehicles: 983, severity: 'ok', status: '生产可用', evidence: '在线 11/50,缺失车辆 983,Kafka Lag 0', action: '保持实时、轨迹、RAW 和统计链路巡检。', acceptance: '车辆服务可回查实时、轨迹和 RAW 证据。', vehiclesHash: '#/vehicles?protocol=YUTONG_MQTT&missingProtocol=YUTONG_MQTT', realtimeHash: '#/realtime?protocol=YUTONG_MQTT&online=online', historyHash: '#/history-query?protocol=YUTONG_MQTT&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=YUTONG_MQTT' } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); const alertButton = await screen.findByRole('button', { name: '顶部告警事件 P0 2 / 活跃 4' }); expect(alertButton).toHaveTextContent('告警事件 P0 2 / 活跃 4'); fireEvent.click(alertButton); expect(window.location.hash).toBe('#/alert-events'); }); test('shows vehicle service status distribution on dashboard', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [ { status: 'healthy', title: '服务正常', count: 12 }, { status: 'degraded', title: '来源不完整', count: 39 } ], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); expect(screen.getAllByText('服务状态').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('告警事件预览')).toBeInTheDocument(); }); test('dashboard renders vehicle service summary metrics', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, archiveIncompleteVehicles: 366, serviceStatuses: [ { status: 'healthy', title: '服务正常', count: 161 }, { status: 'degraded', title: '来源不完整', count: 41 } ], protocols: [ { protocol: 'JT808', online: 157, total: 388 }, { protocol: 'YUTONG_MQTT', online: 0, total: 0 } ], missingSources: [{ protocol: 'YUTONG_MQTT', count: 1033 }], archiveMissingFields: [{ field: 'phone', title: '缺手机号', count: 353 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('总车辆')).toBeInTheDocument(); expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日活跃')).toBeInTheDocument(); expect(screen.getAllByText('有效定位').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('告警车辆')).toBeInTheDocument(); expect(screen.getByText('今日数据')).toBeInTheDocument(); expect(screen.getByText('档案不完整')).toBeInTheDocument(); expect(screen.getByText('366')).toBeInTheDocument(); expect(screen.queryByText('NaN%')).not.toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', undefined); }); test('dashboard presents one vehicle service operating posture', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapApiConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false, amapSecurityServiceHost: '/_AMapService' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务入口')).toBeInTheDocument(); expect(screen.getAllByText('208 / 1,033 在线').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日活跃 4')).toBeInTheDocument(); expect(screen.getByText('7 告警事件')).toBeInTheDocument(); }); test('dashboard exposes vehicle data center capability matrix', async () => { window.history.replaceState(null, '', '/#/dashboard'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/source-readiness')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, onlineVehicles: 208, kafkaLag: 0, activeConnections: 1234, redisOnlineKeys: 368, platformRelease: 'platform-dashboard-test', sources: [ { protocol: 'GB32960', role: '整车与氢能实时数据主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 0', action: '核对现代和交投平台转发清单。', acceptance: '缺失来源车辆下降,单源车辆可解释。', vehiclesHash: '#/vehicles?protocol=GB32960&missingProtocol=GB32960', realtimeHash: '#/realtime?protocol=GB32960&online=online', historyHash: '#/history-query?protocol=GB32960&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=GB32960' }, { protocol: 'JT808', role: '位置、总里程和设备在线主来源', online: 174, total: 424, onlineRate: 41.04, missingVehicles: 609, severity: 'warning', status: '覆盖不足', evidence: '在线 174/424,缺失车辆 609,Kafka Lag 0', action: '补齐手机号绑定和注册缺失车辆。', acceptance: '未知 VIN 注册下降,位置与里程持续可查。', vehiclesHash: '#/vehicles?protocol=JT808&missingProtocol=JT808', realtimeHash: '#/realtime?protocol=JT808&online=online', historyHash: '#/history-query?protocol=JT808&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=JT808' }, { protocol: 'YUTONG_MQTT', role: '宇通派生工况与燃料电池补充来源', online: 11, total: 50, onlineRate: 22, missingVehicles: 983, severity: 'ok', status: '生产可用', evidence: '在线 11/50,缺失车辆 983,Kafka Lag 0', action: '保持实时、轨迹、RAW 和统计链路巡检。', acceptance: '车辆服务可回查实时、轨迹和 RAW 证据。', vehiclesHash: '#/vehicles?protocol=YUTONG_MQTT&missingProtocol=YUTONG_MQTT', realtimeHash: '#/realtime?protocol=YUTONG_MQTT&online=online', historyHash: '#/history-query?protocol=YUTONG_MQTT&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=YUTONG_MQTT' } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapApiConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false, amapSecurityServiceHost: '/_AMapService' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 0, recordCount: 0, sourceCount: 0, totalMileageKm: 0, averageMileagePerVin: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { rules: [], policies: [], priorityIssues: [], activeRuleCount: 0, p0RuleCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); const renderDashboard = async () => { window.history.replaceState(null, '', '/#/dashboard'); render(); expect(await screen.findByText('车辆运营能力矩阵')).toBeInTheDocument(); }; await renderDashboard(); expect(screen.getByText('车辆运营监控台')).toBeInTheDocument(); expect(screen.getByText('先看车辆服务')).toBeInTheDocument(); expect(screen.getByText('车辆监控指挥台')).toBeInTheDocument(); expect(screen.getByText('全域车辆监控')).toBeInTheDocument(); expect(screen.getByText('异常车辆闭环')).toBeInTheDocument(); expect(screen.getByText('自定义时间复盘')).toBeInTheDocument(); expect(screen.getByText('数据交付与导出')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆监控指挥台 全域车辆监控 打开车辆地图' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆监控指挥台 异常车辆闭环 告警事件' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆监控指挥台 自定义时间复盘 轨迹复盘' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆监控指挥台 数据交付与导出 历史导出' })).toBeInTheDocument(); expect(screen.getByText('客户常用工作流')).toBeInTheDocument(); expect(screen.getByText('实时车辆地图')).toBeInTheDocument(); expect(screen.getByText('历史数据导出')).toBeInTheDocument(); expect(screen.getByText('今日车辆服务任务板')).toBeInTheDocument(); expect(screen.getByText('先看全域在线')).toBeInTheDocument(); expect(screen.getByText('再看重点车辆')).toBeInTheDocument(); expect(screen.getByText('按时间窗复盘')).toBeInTheDocument(); expect(screen.getByText('导出证据数据')).toBeInTheDocument(); expect(screen.getByText('闭环告警通知')).toBeInTheDocument(); expect(screen.getByText('车辆服务入口')).toBeInTheDocument(); expect(screen.getByText('客户服务路径')).toBeInTheDocument(); expect(screen.getByText('从车辆出发,完成监控、复盘、统计、导出和告警闭环')).toBeInTheDocument(); expect(screen.getByText('客户看到的是车辆服务结果;32960、808、MQTT 只在需要追溯时作为证据通道出现。')).toBeInTheDocument(); expect(screen.getByText('找车')).toBeInTheDocument(); expect(screen.getByText('看位置')).toBeInTheDocument(); expect(screen.getAllByText('复盘时间').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('交付数据')).toBeInTheDocument(); expect(screen.getByText('闭环异常')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务入口 找车 车辆中心' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务入口 看位置 实时地图' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制客户服务说明' })).toBeInTheDocument(); expect(screen.getByText('自定义时间监控')).toBeInTheDocument(); expect(screen.getByText('应用时间窗')).toBeInTheDocument(); expect(screen.getByText('重置最近一天')).toBeInTheDocument(); expect(screen.getByText('当前时间窗')).toBeInTheDocument(); expect(screen.getByText('客户时间窗复盘包')).toBeInTheDocument(); expect(screen.getByText('客户问某辆车、某段时间发生了什么时,直接按这个时间窗串起轨迹、里程、历史数据、告警通知和地图定位。')).toBeInTheDocument(); expect(screen.getByText('车辆范围')).toBeInTheDocument(); expect(screen.getAllByText('复盘时间').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('统计证据')).toBeInTheDocument(); expect(screen.getByText('时间窗服务核对')).toBeInTheDocument(); expect(screen.getByText('交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。')).toBeInTheDocument(); expect(screen.getByText('范围可解释')).toBeInTheDocument(); expect(screen.getByText('轨迹可复盘')).toBeInTheDocument(); expect(screen.getByText('数据可导出')).toBeInTheDocument(); expect(screen.getByText('异常可闭环')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制复盘包' })).toBeInTheDocument(); expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument(); expect(screen.getByText('查看轨迹')).toBeInTheDocument(); expect(screen.getByText('统计里程')).toBeInTheDocument(); expect(screen.getByText('导出原始记录')).toBeInTheDocument(); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('里程核对')).toBeInTheDocument(); expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警复盘').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '时间窗作业 轨迹回放 打开轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '时间窗作业 历史证据 导出证据' })).toBeInTheDocument(); expect(screen.getByText('今天车辆轨迹')).toBeInTheDocument(); expect(screen.getByText('历史时间窗')).toBeInTheDocument(); expect(screen.getByText('区间里程')).toBeInTheDocument(); expect(screen.getAllByText('告警复盘').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('车辆运营工作台')).toBeInTheDocument(); expect(screen.getByText('地图运营能力')).toBeInTheDocument(); expect(screen.getByText('车联网场景导航')).toBeInTheDocument(); expect(screen.getByText('高德 Web JS')).toBeInTheDocument(); expect(screen.getByText('服务端 API')).toBeInTheDocument(); expect(screen.getByText('安全代理')).toBeInTheDocument(); expect(screen.getByText('安全码暴露')).toBeInTheDocument(); expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('已配置').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('已启用')).toBeInTheDocument(); expect(screen.getByText('未暴露')).toBeInTheDocument(); expect(screen.getAllByText('实时地图').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('实时监控').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('历史数据查询').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警事件触发与通知').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('告警事件与通知')).toBeInTheDocument(); expect(screen.getByText('区间闭合复核')).toBeInTheDocument(); expect(screen.getByText('高德轨迹证据')).toBeInTheDocument(); expect(screen.getByText('1,033 车辆口径')).toBeInTheDocument(); expect(screen.getAllByText('在线 208').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线态势 208').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('有效定位 0').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('今日帧 1,286,320').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警 7').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计车辆 1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('确认车辆是否在线、位置是否可信、三类来源是否齐全。')).toBeInTheDocument(); expect(screen.getByText('按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。')).toBeInTheDocument(); expect(screen.getByText('优先返回必要字段,避免大 JSON 拖慢查询')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制功能蓝图' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台功能蓝图】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('32960 / 808 / 宇通 MQTT 都只是车辆服务证据源')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆规模:1,033;在线:208;告警:7')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('高德地图:Web JS 已配置;服务端 API 已配置;安全代理 已启用;安全码未暴露')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-1 秒内进入实时视图')); fireEvent.click(screen.getByRole('button', { name: '客户工作流 实时车辆地图' })); expect(window.location.hash).toBe('#/map?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '今日任务 先看全域在线 打开实时地图' })); expect(window.location.hash).toBe('#/map?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '今日任务 按时间窗复盘 里程统计' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '今日任务 导出证据数据 历史导出' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '客户工作流 历史数据导出' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw'); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '地图运营 实时监控' })); expect(window.location.hash).toBe('#/map?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '地图运营 历史证据' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '查看轨迹' })); expect(window.location.hash.startsWith('#/history')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '统计里程' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '复制复盘包' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台能力')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('时间窗判定:1 天窗口')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付核对:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 范围可解释')); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '导出原始记录' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw'); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '时间监控 历史时间窗' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '时间监控 区间里程' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '作业台 实时监控 打开实时地图' })); expect(window.location.hash).toBe('#/map?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '场景导航 实时监控 实时监控' })); expect(window.location.hash).toBe('#/realtime?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '场景导航 轨迹回放 原始记录' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 实时监控' })); expect(window.location.hash).toBe('#/realtime?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 实时地图' })); expect(window.location.hash).toBe('#/map?online=online'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 轨迹回放' })); expect(window.location.hash.startsWith('#/history')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBeNull(); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 历史数据查询' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 告警事件触发与通知' })); expect(window.location.hash).toBe('#/alert-events'); cleanup(); await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '能力入口 里程统计' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); }); test('dashboard exposes vehicle command center map actions', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-COMMAND-001', plate: '粤A指挥1', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], primaryProtocol: 'GB32960', sourceCount: 3, onlineSourceCount: 2, online: true, longitude: 113.2, latitude: 23.1, speedKmh: 31, socPercent: 78, totalMileageKm: 100.5, lastSeen: '2026-07-03 20:12:10' }, { vin: 'VIN-COMMAND-002', plate: '粤A指挥2', protocols: ['JT808'], primaryProtocol: 'JT808', sourceCount: 3, onlineSourceCount: 0, online: false, longitude: 0, latitude: 0, lastSeen: '2026-07-03 19:55:10' } ], total: 2, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { summary: { issueVehicleCount: 2, issueRecordCount: 2, errorCount: 1, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }, { name: 'JT808', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }, { name: 'VIN_MISSING', count: 1 }] }, rules: [], policies: [{ name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '站内告警 / 邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据' }], priorityIssues: [], activeRuleCount: 1, p0RuleCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-COMMAND-002', plate: '粤A指挥2', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', detail: '无来源', lastSeen: '2026-07-03 19:55:10' }], total: 1, limit: 5, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('实时态势地图')).toBeInTheDocument(); expect(screen.getByText('在线 1 / 2')).toBeInTheDocument(); expect(screen.getAllByText('有效定位 1').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('降级/离线 2')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '处理高优先级告警' })); expect(window.location.hash).toBe('#/alert-events?issueType=NO_SOURCE'); cleanup(); window.history.replaceState(null, '', '/#/dashboard'); render(); expect(await screen.findByText('实时态势地图')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '查看实时态势' })); expect(window.location.hash).toBe('#/map?online=online'); }); test('dashboard shows vehicle service action queue', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, archiveIncompleteVehicles: 366, serviceStatuses: [], protocols: [], missingSources: [{ protocol: 'YUTONG_MQTT', count: 983 }], archiveMissingFields: [{ field: 'phone', title: '缺手机号', count: 353 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务处置队列')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '确认平台转发 461' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '维护身份绑定 9' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '完善车辆档案 366' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '补齐缺手机号 353' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源 983' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '补齐缺手机号 353' })); await waitFor(() => { expect(window.location.hash).toBe('#/vehicles?archiveMissing=phone'); }); }); test.each([ { buttonName: '查看在线车辆', expectedHash: '#/vehicles?online=online' }, { buttonName: '实时地图', expectedHash: '#/map?online=online' }, { buttonName: '查看告警事件', expectedHash: '#/alert-events' } ])('dashboard posture opens %s', async ({ buttonName, expectedHash }) => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 7, issueRecordCount: 9, errorCount: 1, warningCount: 8, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: buttonName })); await waitFor(() => { expect(window.location.hash).toBe(expectedHash); }); }); test('dashboard exposes end-to-end operations workflow entries', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 88, activeToday: 96, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [{ name: 'Kafka', status: 'ok' }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 88, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 0, identityRequiredVehicles: 0, archiveIncompleteVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [], archiveMissingFields: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 7, issueRecordCount: 9, errorCount: 1, warningCount: 8, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('自定义时间监控')).toBeInTheDocument(); expect(screen.getByText('今天车辆轨迹')).toBeInTheDocument(); expect(screen.getByText('历史时间窗')).toBeInTheDocument(); expect(screen.getByText('区间里程')).toBeInTheDocument(); expect(screen.getByText('告警复盘')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '时间监控 今天车辆轨迹' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history')).toBe(true); }); }); test('dashboard surfaces highest priority quality issue with evidence shortcuts', async () => { window.history.replaceState(null, '', '/#/dashboard'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 88, activeToday: 96, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 88, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 0, identityRequiredVehicles: 0, archiveIncompleteVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [], archiveMissingFields: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤A告警1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 数据缺少 VIN 映射' }], total: 1, limit: 5, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 7, issueRecordCount: 9, errorCount: 1, warningCount: 8, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('重点车辆服务')).toBeInTheDocument(); expect(screen.getByText('把实时、轨迹、原始记录、里程和告警集中到同一辆车处理。')).toBeInTheDocument(); expect(screen.getByText('实时证据')).toBeInTheDocument(); expect(screen.getAllByText('轨迹证据').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('告警证据')).toBeInTheDocument(); expect(screen.getByText('统计证据')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制处置卡' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【重点车辆服务处置卡】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A告警1 / 13307795425')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&includeFields=true')); expect(await screen.findByText('最高优先级告警')).toBeInTheDocument(); expect(screen.getAllByText('粤A告警1 / 13307795425').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('VIN 缺失')).toBeInTheDocument(); expect(screen.getAllByText('JT808 数据缺少 VIN 映射').length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '首页告警轨迹证据' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('dashboard copies highest priority quality issue notification text', async () => { window.history.replaceState(null, '', '/#/dashboard'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:dashboard-snapshot'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 88, activeToday: 96, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 88, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 0, identityRequiredVehicles: 0, archiveIncompleteVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [], archiveMissingFields: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤A告警1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 数据缺少 VIN 映射' }], total: 1, limit: 5, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 7, issueRecordCount: 9, errorCount: 1, warningCount: 8, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('最高优先级告警')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '首页复制告警通知' })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【P0 告警通知】VIN 缺失')); }); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A告警1 / 13307795425')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:JT808 数据缺少 VIN 映射')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); fireEvent.click(screen.getByRole('button', { name: '复制运营交接摘要' })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台运营交接摘要】')); }); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:88 / 1,033')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日活跃:96')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日帧量:1,286,320')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最高告警:P0 VIN 缺失 / 粤A告警1 / 13307795425 / 2026-07-03 20:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?online=online')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时地图:http://localhost:3000/#/map?online=online')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件:http://localhost:3000/#/alert-events?issueType=VIN_MISSING')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: '导出驾驶舱 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:dashboard-snapshot'); }); test('dashboard keeps core vehicle service metrics when preview requests fail', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: false, status: 500, json: async () => ({ error: { message: '质量预览失败', detail: 'TDengine timeout' }, traceId: 'trace-quality', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('总车辆')).toBeInTheDocument(); expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日活跃')).toBeInTheDocument(); expect(screen.getByText('告警车辆')).toBeInTheDocument(); }); test('opens dashboard coverage from service action queue', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [{ protocol: 'YUTONG_MQTT', online: 1, total: 8 }], missingSources: [{ protocol: 'YUTONG_MQTT', count: 983 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '补齐 YUTONG_MQTT 来源 983' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); }); test('opens vehicle list filtered by service summary KPI', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, boundVehicles: 1024, onlineVehicles: 208, singleSourceVehicles: 391, multiSourceVehicles: 181, noDataVehicles: 461, identityRequiredVehicles: 9, serviceStatuses: [], protocols: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: /告警车辆/ })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&serviceStatus=degraded'), undefined); }); expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded'); }); test('shows vehicle service result summary on vehicle list filters', async () => { window.history.replaceState(null, '', '/#/vehicles?coverage=multi'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 181, onlineVehicles: 73, singleSourceVehicles: 0, multiSourceVehicles: 181, noDataVehicles: 4, unboundVehicles: 9, archiveIncompleteVehicles: 12, archiveMissingFields: [{ field: 'phone', title: '缺手机号', count: 7 }], missingSources: [{ protocol: 'YUTONG_MQTT', count: 181 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-MULTI-001', plate: '粤A服务1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'JT808', online: false, lastSeen: '2026-07-03 20:10:00', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false } ], sourceCount: 2, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '1/2 个来源在线', sourceCount: 2, onlineSourceCount: 1 } }, { vin: 'VIN-MISSING-ARCHIVE', plate: '粤A缺项', phone: '', oem: '', protocols: ['JT808'], missingProtocols: ['GB32960', 'YUTONG_MQTT'], sourceStatus: [], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:09', bindingStatus: 'bound' } ], total: 181, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前车辆结果')).toBeInTheDocument(); expect(screen.getByText('车辆服务台')).toBeInTheDocument(); expect(screen.getByText('客户视角')).toBeInTheDocument(); expect(screen.getByText('客户查车路径')).toBeInTheDocument(); expect(screen.getByText('车辆池服务')).toBeInTheDocument(); expect(screen.getByText('从车辆池筛出客户要看的车,再进入单车服务')).toBeInTheDocument(); expect(screen.getByText('客户侧只关心车辆服务结果,32960、808、MQTT 作为证据来源参与筛选和可信度判断。')).toBeInTheDocument(); expect(screen.getByText('车辆身份')).toBeInTheDocument(); expect(screen.getByText('VIN / 车牌 / 手机号')).toBeInTheDocument(); expect(screen.getByText('轨迹与位置')).toBeInTheDocument(); expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: /复制客户查车路径/ })).toBeInTheDocument(); expect(screen.getByText('车辆服务决策板')).toBeInTheDocument(); expect(screen.getAllByText('异常车辆').length).toBeGreaterThan(0); expect(screen.getByText('4 无来源')).toBeInTheDocument(); expect(screen.getByText('单车服务')).toBeInTheDocument(); expect(screen.getByText('输入车辆')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务决策 在线车辆 实时监控' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务决策 异常车辆 维护身份绑定' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务决策 数据交付 导出清单' })).toBeInTheDocument(); expect(screen.getAllByText('车辆地图').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('数据导出').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('当前页服务判读')).toBeInTheDocument(); expect(screen.getByText('建议下一步')).toBeInTheDocument(); expect(screen.getAllByText('降级可服务').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('2 辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('车辆仍可查询,但需要补齐缺失来源后再做跨来源校验。')).toBeInTheDocument(); expect(screen.getAllByText('181').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('过滤车辆')).toBeInTheDocument(); expect(screen.getByText('73')).toBeInTheDocument(); expect(screen.getAllByText('在线车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('单源车辆')).toBeInTheDocument(); expect(screen.getByText('暂无来源车辆')).toBeInTheDocument(); expect(screen.getByText('待绑定')).toBeInTheDocument(); expect(screen.getByText('档案不完整')).toBeInTheDocument(); expect(screen.getAllByText('缺手机号').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('处置队列')).toBeInTheDocument(); expect(screen.getAllByText('P0').length).toBeGreaterThanOrEqual(2); expect(screen.getAllByText('P1').length).toBeGreaterThanOrEqual(2); expect(screen.getAllByText('已有数据但无法归并到 VIN,先补绑定再看跨来源服务。').length).toBeGreaterThan(0); expect(screen.getByText('车辆没有任何来源证据,优先确认平台转发、端口和订阅配置。')).toBeInTheDocument(); expect(screen.getByText('缺手机号会影响车辆档案检索、绑定确认和运营侧筛选。')).toBeInTheDocument(); expect(screen.getByText('YUTONG_MQTT 来源缺失会降低跨来源定位、里程和实时判断可信度。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '维护身份绑定 9' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '确认平台转发 4' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '补齐缺手机号 7' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源 181' })).toBeInTheDocument(); expect(screen.getByText('档案完整度')).toBeInTheDocument(); expect(screen.getByText('4/4')).toBeInTheDocument(); expect(screen.getAllByText('2/4').length).toBeGreaterThan(0); expect(screen.getByText('缺OEM')).toBeInTheDocument(); expect(screen.getByText('VIN-MULTI-001')).toBeInTheDocument(); expect(screen.getByText('VIN-MISSING-ARCHIVE')).toBeInTheDocument(); expect(screen.getAllByText('来源证据').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('GB32960 在线')).toBeInTheDocument(); expect(screen.getAllByText('JT808 离线').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument(); expect(screen.getByText('1/2 来源在线')).toBeInTheDocument(); expect(screen.getByText('1/2 来源在线,缺 YUTONG_MQTT')).toBeInTheDocument(); expect(screen.getByText('建议动作')).toBeInTheDocument(); expect(screen.getByText('补齐 YUTONG_MQTT 来源')).toBeInTheDocument(); }); test('exports current vehicle coverage page as csv', async () => { window.history.replaceState(null, '', '/#/vehicles?missingProtocol=YUTONG_MQTT'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:vehicle-coverage'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, unboundVehicles: 0, archiveIncompleteVehicles: 0, archiveMissingFields: [], missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-EXPORT-VEHICLE', plate: '粤A车辆导出', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], missingProtocols: ['YUTONG_MQTT'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '缺少 YUTONG_MQTT', sourceCount: 1, onlineSourceCount: 1 }, sourceConsistency: { sourceCount: 1, onlineSourceCount: 1, locatedSourceCount: 1, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'overview', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '缺少 YUTONG_MQTT' } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-EXPORT-VEHICLE')).toBeInTheDocument(); expect(screen.getByText('当前页 1 辆')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '导出车辆当前页 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:vehicle-coverage'); }); test('copies vehicle governance summary from vehicle center', async () => { window.history.replaceState(null, '', '/#/vehicles?missingProtocol=YUTONG_MQTT'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 12, onlineVehicles: 5, singleSourceVehicles: 7, multiSourceVehicles: 2, noDataVehicles: 3, unboundVehicles: 4, archiveIncompleteVehicles: 6, archiveMissingFields: [{ field: 'phone', title: '缺手机号', count: 3 }], missingSources: [{ protocol: 'YUTONG_MQTT', count: 8 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-GOV-001', plate: '粤A治理1', phone: '', oem: 'G7s', protocols: ['JT808'], missingProtocols: ['YUTONG_MQTT'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '缺少 YUTONG_MQTT', sourceCount: 1, onlineSourceCount: 1 }, sourceConsistency: { sourceCount: 1, onlineSourceCount: 1, locatedSourceCount: 1, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'overview', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '缺少 YUTONG_MQTT' } }], total: 12, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-GOV-001')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制治理摘要' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆治理摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:缺失来源:YUTONG_MQTT')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆总数:12,在线:5')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('单源:7,多源:2,暂无来源:3')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('待绑定:4,档案不完整:6')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('档案缺项:缺手机号 3')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('缺失来源:YUTONG_MQTT 8')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('处置队列:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[P0] 维护身份绑定 4')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[P1] 补齐 YUTONG_MQTT 来源 8')); fireEvent.click(screen.getByRole('button', { name: '复制当前页处置清单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆处置清单】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:缺失来源:YUTONG_MQTT')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页:1 辆 / 总计 12 辆')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 车牌 粤A治理1 / VIN VIN-GOV-001 / 手机号 - / OEM G7s')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:来源不完整;在线:在线;来源:1/1 来源在线,缺 YUTONG_MQTT;最后时间:2026-07-03 20:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:补齐 YUTONG_MQTT 来源')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-GOV-001&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=VIN-GOV-001&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-GOV-001&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-GOV-001&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: '复制身份维护清单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆身份维护清单】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页待维护:1 辆 / 当前筛选 12 辆')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A治理1 / VIN-GOV-001 / - / G7s')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('档案缺项:缺手机号')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('关联键:vin=VIN-GOV-001;plate=粤A治理1;oem=G7s')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:补齐档案字段:缺手机号')); }); test('filters vehicle list from recommended action', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 181, onlineVehicles: 73, singleSourceVehicles: 0, multiSourceVehicles: 181, noDataVehicles: 0, unboundVehicles: 0, missingSources: [{ protocol: 'YUTONG_MQTT', count: 181 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MISSING-MQTT', plate: '粤A服务2', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceStatus: [], sourceCount: 2, onlineSourceCount: 2, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 181, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '补齐 YUTONG_MQTT 来源' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&missingProtocol=YUTONG_MQTT'), undefined); }); expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT'); }); test('filters vehicle list from action queue', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 181, onlineVehicles: 73, singleSourceVehicles: 0, multiSourceVehicles: 181, noDataVehicles: 4, unboundVehicles: 9, missingSources: [{ protocol: 'YUTONG_MQTT', count: 181 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '确认平台转发 4' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&serviceStatus=no_data'), undefined); }); expect(window.location.hash).toBe('#/vehicles?serviceStatus=no_data'); }); test('shows and clears current vehicle service filters', async () => { window.history.replaceState(null, '', '/#/vehicles?keyword=%E7%B2%A4A&protocol=JT808&coverage=multi&missingProtocol=YUTONG_MQTT&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 0, multiSourceVehicles: 1, noDataVehicles: 0, unboundVehicles: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前车辆筛选')).toBeInTheDocument(); expect(screen.getByText('关键词:粤A')).toBeInTheDocument(); expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); expect(screen.getByText('车辆覆盖:多源')).toBeInTheDocument(); expect(screen.getByText('缺失来源:YUTONG_MQTT')).toBeInTheDocument(); expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument(); expect(screen.getByText('在线:在线')).toBeInTheDocument(); expect(screen.getByText('绑定:已绑定')).toBeInTheDocument(); expect(screen.getByText('档案:不完整')).toBeInTheDocument(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('archiveStatus=incomplete'), undefined); }); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0'), undefined); }); expect(window.location.hash).toBe('#/vehicles'); }); test('filters vehicle list from result summary actions', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: path.includes('online=online') ? 73 : 181, onlineVehicles: 73, singleSourceVehicles: 108, multiSourceVehicles: 73, unboundVehicles: 9 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [], total: path.includes('online=online') ? 73 : 181, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: /在线车辆 73/ })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&online=online'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage/summary?online=online'), undefined); expect(window.location.hash).toBe('#/vehicles?online=online'); }); test('filters vehicle list by missing source evidence', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 0, singleSourceVehicles: 0, multiSourceVehicles: 1, noDataVehicles: 0, unboundVehicles: 0, missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceCount: 2, onlineSourceCount: 0, online: false, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await waitFor(() => { expect(screen.getAllByText('缺失来源').length).toBeGreaterThanOrEqual(2); }); expect(screen.getAllByText('缺 YUTONG_MQTT').length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '缺 YUTONG_MQTT 1' })); expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT'); fireEvent.click(screen.getByRole('button', { name: '缺 YUTONG_MQTT' })); expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT'); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); fireEvent.click(screen.getByTestId('missing-protocol-filter')); const mqttOptions = await screen.findAllByText('缺 YUTONG_MQTT'); fireEvent.click(mqttOptions[mqttOptions.length - 1]); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); }); test('copies shareable vehicle service filter link', async () => { window.history.replaceState(null, '', '/#/vehicles?keyword=%E7%B2%A4A&coverage=multi&serviceStatus=degraded'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 181, onlineVehicles: 73, singleSourceVehicles: 0, multiSourceVehicles: 181, noDataVehicles: 12, unboundVehicles: 9 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [], total: 181, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: /复制筛选链接/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/#/vehicles?keyword=%E7%B2%A4A&coverage=multi&serviceStatus=degraded')); }); test('shows resolved vehicle service status after topbar search', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'LB9A32A24R0LS1426', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], primaryProtocol: 'JT808', coverageStatus: 'partial', sourceCount: 2, onlineSourceCount: 1, lastSeen: '2026-07-03 20:12:10', realtimeCount: 2, historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0, sourceConsistency: { sourceCount: 2, onlineSourceCount: 1, locatedSourceCount: 2, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'all_sources', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '1/2 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'LB9A32A24R0LS1426', lookupKey: 'LB9A32A24R0LS1426', lookupResolved: true, sources: ['GB32960', 'JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤AG18312' } }); fireEvent.click(screen.getByRole('button', { name: '查询车辆' })); expect(await screen.findByText('当前车辆:来源不完整')).toBeInTheDocument(); expect(screen.getByText('粤AG18312 / LB9A32A24R0LS1426')).toBeInTheDocument(); expect(screen.getByText('一致性:来源不完整')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312'), undefined); expect(fetchMock).not.toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/resolve'), undefined); }); test('shows row service status in dashboard vehicle previews', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-COVERAGE-WARN', plate: '粤A预警1', phone: '', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'JT808', online: false, lastSeen: '2026-07-03 20:10:00', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false } ], sourceCount: 2, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '覆盖部分离线', detail: '覆盖预览由 API 判定', sourceCount: 2, onlineSourceCount: 1 } }], total: 1, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-REALTIME-OFFLINE', plate: '粤A离线1', phone: '', oem: 'G7s', protocols: ['JT808'], sourceCount: 1, onlineSourceCount: 0, online: false, bindingStatus: 'bound', serviceStatus: { status: 'offline', severity: 'error', title: '实时车辆离线', detail: '实时预览由 API 判定', sourceCount: 1, onlineSourceCount: 0 }, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 0, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }], total: 1, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-COVERAGE-WARN')).toBeInTheDocument(); expect(screen.getAllByText('来源证据').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('GB32960 在线')).toBeInTheDocument(); expect(screen.getByText('JT808 离线')).toBeInTheDocument(); expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument(); expect(screen.getAllByText('1/2 来源在线').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('覆盖部分离线')).toBeInTheDocument(); expect(screen.getByText('VIN-REALTIME-OFFLINE')).toBeInTheDocument(); expect(screen.getAllByText('实时车辆离线').length).toBeGreaterThanOrEqual(1); }); test('filters dashboard coverage from source consistency diagnosis', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, boundVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, identityRequiredVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-DASH-SINGLE', plate: '粤A总览', phone: '', oem: '', protocols: ['GB32960'], missingProtocols: ['JT808', 'YUTONG_MQTT'], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-04 07:15:28', bindingStatus: 'bound', sourceConsistency: { sourceCount: 1, onlineSourceCount: 1, locatedSourceCount: 0, missingProtocols: ['JT808', 'YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'coverage', status: 'single_source', severity: 'warning', title: '单一来源', detail: '当前车辆只有一个数据来源,无法做跨来源一致性校验。' } }], total: 1, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704155844' } } : { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-DASH-SINGLE')).toBeInTheDocument(); expect(screen.getByText('来源一致性')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '单一来源' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=JT808'), undefined); expect(screen.getByText('当前筛选:来源不完整 / 缺 JT808')).toBeInTheDocument(); }); test('opens full vehicle service list from dashboard coverage filters', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, boundVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, identityRequiredVehicles: 0, serviceStatuses: [], protocols: [], missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-DASH-FULL-LIST', plate: '粤A全量', phone: '', oem: '', protocols: ['GB32960'], missingProtocols: ['JT808', 'YUTONG_MQTT'], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-04 07:20:14', bindingStatus: 'bound', sourceConsistency: { sourceCount: 1, onlineSourceCount: 1, locatedSourceCount: 0, missingProtocols: ['JT808', 'YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'coverage', status: 'single_source', severity: 'warning', title: '单一来源', detail: '当前车辆只有一个数据来源,无法做跨来源一致性校验。' } }], total: 1, limit: path.includes('limit=20') ? 20 : 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704155844' } } : { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-DASH-FULL-LIST')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '单一来源' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=JT808'), undefined); }); fireEvent.click(screen.getByRole('button', { name: '查看全部车辆' })); await waitFor(() => { expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=JT808'); }); const vehicleCoverageCall = fetchMock.mock.calls.find(([input]) => String(input).startsWith('/api/vehicles/coverage?limit=20&offset=0')); expect(vehicleCoverageCall).toBeTruthy(); const vehicleCoverageParams = new URLSearchParams(String(vehicleCoverageCall?.[0]).split('?')[1]); expect(vehicleCoverageParams.get('serviceStatus')).toBe('degraded'); expect(vehicleCoverageParams.get('missingProtocol')).toBe('JT808'); }); test('opens vehicle service from dashboard realtime preview with source evidence', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 1, activeToday: 1, frameToday: 10, issueVehicles: 0, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, boundVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, identityRequiredVehicles: 0, serviceStatuses: [], protocols: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-DASH-RT', plate: '粤A实时1', phone: '', oem: 'G7s', protocols: ['JT808'], primaryProtocol: 'JT808', sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', longitude: 113.2, latitude: 23.1, speedKmh: 12, socPercent: 80, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }], total: 1, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-DASH-RT', plate: '粤A实时1', sourceCount: 1, onlineSourceCount: 1, coverageStatus: 'online', primaryProtocol: 'JT808', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-DASH-RT')).toBeInTheDocument(); const vehicleButtons = screen.getAllByRole('button', { name: '车辆服务' }); fireEvent.click(vehicleButtons[vehicleButtons.length - 1]); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-DASH-RT&protocol=JT808'); }); }); test('opens vehicle service from dashboard quality preview with issue source evidence', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 1, activeToday: 1, frameToday: 10, issueVehicles: 1, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤A质量1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 数据缺少 VIN 映射' }], total: 1, limit: 5, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-DASH-QUALITY', plate: '粤A质量1', sourceCount: 1, onlineSourceCount: 1, coverageStatus: 'online', primaryProtocol: 'JT808', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/vehicle-service/summary') ? { totalVehicles: 1, boundVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, identityRequiredVehicles: 0, serviceStatuses: [], protocols: [] } : { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('13307795425')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '按车牌查车' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-DASH-QUALITY&protocol=JT808'); }); }); test('filters dashboard coverage by service status', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('dashboard-service-status-filter')); fireEvent.click(await screen.findByText('来源不完整')); fireEvent.click(screen.getByRole('button', { name: '筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); }); expect(screen.getByText('当前筛选:来源不完整')).toBeInTheDocument(); }); test('filters dashboard coverage by missing source evidence', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('dashboard-missing-protocol-filter')); fireEvent.click(await screen.findByText('缺 YUTONG_MQTT')); fireEvent.click(screen.getByRole('button', { name: '筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); expect(screen.getByText('当前筛选:缺 YUTONG_MQTT')).toBeInTheDocument(); }); test('opens dashboard coverage from service status distribution', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [ { status: 'degraded', title: '来源不完整', count: 39 } ], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 8, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('dashboard-service-status-filter')); fireEvent.click(await screen.findByText('来源不完整')); fireEvent.click(screen.getByRole('button', { name: '筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); }); }); test('renders quality issues as vehicle-service governance labels', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-NO-SOURCE-001', plate: '粤A无源1', phone: '13300000002', sourceEndpoint: 'vehicle_identity_binding', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'warning', lastSeen: '', detail: '车辆已绑定,但暂无 32960、808 或 MQTT 数据来源' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-NO-SOURCE-001')).toBeInTheDocument(); expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('确认平台转发').length).toBeGreaterThan(0); expect(screen.getByText('车辆已绑定但没有任何来源证据,先确认平台转发、端口和订阅。')).toBeInTheDocument(); }); test('shows alert rule and notification policy workspace on quality page', async () => { window.history.replaceState(null, '', '/#/alert-events'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [{ name: 'tdengine', status: 'error', detail: 'write unavailable' }], kafkaLag: 42, activeConnections: 120000, capacityFindings: ['kafka lag 42'], redisOnlineKeys: 368, tdengineWritable: false, mysqlWritable: true, runtime: { platformRelease: 'platform-20260704155607', requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 3, issueRecordCount: 5, errorCount: 1, warningCount: 4, protocols: [{ name: 'VEHICLE_SERVICE', count: 3 }], issueTypes: [ { name: 'NO_SOURCE', count: 2 }, { name: 'VIN_MISSING', count: 1 }, { name: 'FIELD_MISSING', count: 2 } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('客户告警决策台')).toBeInTheDocument(); expect(screen.getByText('先判断影响车辆,再通知责任团队并回到车辆证据闭环')).toBeInTheDocument(); expect(screen.getByText('客户关心的是哪些车辆受影响、是否影响实时/轨迹/里程、是否需要通知、何时恢复;协议来源只作为证据辅助判断。')).toBeInTheDocument(); expect(screen.getAllByText('立即处置').length).toBeGreaterThan(0); expect(screen.getByText('待通知车辆')).toBeInTheDocument(); expect(screen.getByText('证据闭环')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制告警决策' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:立即处置')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆:3 辆;问题记录:5 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('客户处置路径:')); expect(await screen.findByText('告警事件与通知中心')).toBeInTheDocument(); expect(screen.getByText('客户处置:先判断影响车辆和优先级,再通知责任团队,并用实时、轨迹、RAW、里程证据闭环。', { exact: false })).toBeInTheDocument(); expect(screen.getByText('链路状态')).toBeInTheDocument(); expect(screen.getAllByText('优先队列').length).toBeGreaterThanOrEqual(1); expect(await screen.findByText('告警触发规则')).toBeInTheDocument(); expect(screen.getByText('业务影响评估')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制业务影响报告' })).toBeInTheDocument(); expect(screen.getAllByText('高风险').length).toBeGreaterThan(0); expect(screen.getByText('3 辆受影响')).toBeInTheDocument(); expect(screen.getAllByText('影响车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('3 辆').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('P0 0 / P1 0').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0); expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0); expect(screen.getAllByText('异常').length).toBeGreaterThan(0); expect(screen.getByText('4 类活跃')).toBeInTheDocument(); expect(screen.getByText('3 类 P0')).toBeInTheDocument(); expect(screen.getByText('容量与存储风险')).toBeInTheDocument(); expect(screen.getByText('15 分钟恢复')).toBeInTheDocument(); expect(screen.getByText('告警分派看板')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '分派团队 平台接入' })).toHaveTextContent('2 条'); expect(screen.getByRole('button', { name: '分派团队 车辆档案' })).toHaveTextContent('主问题:VIN 缺失'); expect(screen.getByRole('button', { name: '分派团队 基础设施' })).toHaveTextContent('SLA:15 分钟恢复'); expect(screen.getByText('通知策略')).toBeInTheDocument(); expect(screen.getAllByText('P0 实时中断').length).toBeGreaterThan(0); expect(screen.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThan(0); expect(screen.getByText('站内告警 / 邮件 / 企业微信')).toBeInTheDocument(); expect(screen.getByText('30 分钟升级')).toBeInTheDocument(); expect(screen.getByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制业务影响报告' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警业务影响报告】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704155607')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响等级:高风险')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆:3 辆')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要来源:车辆服务 3 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要问题:暂无数据来源 2 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('容量风险:存储异常;Kafka Lag 42;容量发现 1 项')); fireEvent.click(screen.getByRole('button', { name: '复制通知策略Runbook' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知策略Runbook】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704155607')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0 实时中断 / 接入运维 + 业务责任人')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级:30 分钟升级')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')); fireEvent.click(screen.getByRole('button', { name: '分派团队 车辆档案' })); expect(window.location.hash).toBe('#/alert-events?issueType=VIN_MISSING'); }); test('shows actionable priority queue on quality page', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 2, issueRecordCount: 2, errorCount: 1, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }, { name: 'JT808', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }, { name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-P0-001', plate: '粤A优先1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '车辆无任何来源' }, { vin: '', plate: '粤A优先2', phone: '13307795426', sourceEndpoint: '115.231.168.135:43626', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'warning', lastSeen: '2026-07-03 20:10:10', detail: '手机号未映射 VIN' } ], total: 2, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-P0-001', plate: '粤A优先1', sourceCount: 0, onlineSourceCount: 0, coverageStatus: 'no_data', primaryProtocol: 'VEHICLE_SERVICE', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); expect(screen.getByText('车辆风险处置任务板')).toBeInTheDocument(); expect(screen.getAllByText('粤A优先1 / VIN-P0-001').length).toBeGreaterThan(0); expect(screen.getByText('实时确认')).toBeInTheDocument(); expect(screen.getAllByText('轨迹证据').length).toBeGreaterThan(0); expect(screen.getByText('里程复核')).toBeInTheDocument(); expect(screen.getAllByText('原始证据').length).toBeGreaterThan(0); expect(screen.getByText('先确认车辆是否仍在线、最后上报来源和实时字段是否恢复。')).toBeInTheDocument(); expect(screen.getAllByText('P0').length).toBeGreaterThan(0); expect(screen.getAllByText('粤A优先1 / VIN-P0-001').length).toBeGreaterThan(0); expect(screen.getAllByText('确认平台转发').length).toBeGreaterThan(0); expect(screen.getAllByText('30 分钟确认').length).toBeGreaterThan(0); expect(screen.getAllByText('P1').length).toBeGreaterThan(0); expect(screen.getAllByText('粤A优先2 / 13307795426').length).toBeGreaterThan(0); fireEvent.click(screen.getAllByRole('button', { name: '进入车辆服务' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-P0-001&protocol=VEHICLE_SERVICE'); }); }); test('uses backend quality notification plan on quality page', async () => { window.history.replaceState(null, '', '/#/alert-events'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { summary: { issueVehicleCount: 9, issueRecordCount: 11, errorCount: 2, warningCount: 9, protocols: [], issueTypes: [{ name: 'NO_SOURCE', count: 4 }] }, rules: [{ issueType: 'NO_SOURCE', title: '后端无来源规则', level: 'P0', owner: '平台接入', trigger: '后端统一触发', notify: '后端统一通知', sla: '10 分钟确认', count: 4 }], policies: [{ name: '后端 P0 通知策略', target: '接入运维', channel: '邮件 / 企业微信', condition: '后端统一条件' }], priorityIssues: [{ vin: 'VIN-PLAN-001', plate: '粤A计划1', phone: '', sourceEndpoint: 'vehicle_identity_binding', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '后端计划生成的优先告警', priority: 'P0', actionLabel: '后端建议动作', actionDetail: '后端建议说明', sla: '10 分钟确认', vehicleLabel: '后端车辆标签 / VIN-PLAN-001', realtimeHash: '#/realtime?keyword=VIN-PLAN-001', historyHash: '#/history?keyword=VIN-PLAN-001', rawHash: '#/history-query?keyword=VIN-PLAN-001&tab=raw', vehicleHash: '#/detail?keyword=VIN-PLAN-001', notificationText: '【P0 告警通知】后端计划' }], activeRuleCount: 1, p0RuleCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('后端车辆标签 / VIN-PLAN-001')).length).toBeGreaterThan(0); expect(screen.getAllByText('后端建议动作').length).toBeGreaterThan(0); expect(screen.getAllByText('10 分钟确认').length).toBeGreaterThan(0); expect(screen.getByText('后端无来源规则')).toBeInTheDocument(); expect(screen.getByText('后端 P0 通知策略')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/notification-plan?limit=20'), undefined); }); test('renders notification rules as a standalone operations page', async () => { window.history.replaceState(null, '', '/#/notification-rules'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:notification-rules'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 1000, capacityFindings: [], redisOnlineKeys: 800, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-rules-test' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/notification-plan') || path.includes('/api/alert-events/notification-plan')) { return { ok: true, json: async () => ({ data: { summary: { issueVehicleCount: 5, issueRecordCount: 8, errorCount: 2, warningCount: 6, protocols: [], issueTypes: [{ name: 'NO_SOURCE', count: 3 }] }, rules: [ { issueType: 'NO_SOURCE', title: '无来源规则', level: 'P0', owner: '平台接入', trigger: '车辆无任何来源证据', notify: '立即通知接入运维', sla: '30 分钟确认', count: 3 }, { issueType: 'FIELD_MISSING', title: '字段缺失规则', level: 'P1', owner: '协议解析', trigger: '核心字段缺失', notify: '进入每日治理清单', sla: '当日闭环', count: 1 } ], policies: [ { name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }, { name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '每日汇总邮件', condition: '字段缺失、链路间断', escalationMinutes: 120, acceptanceCriteria: '核心字段恢复解析' } ], priorityIssues: [{ vin: 'VIN-RULES-001', plate: '粤A规则1', phone: '', sourceEndpoint: 'vehicle_identity_binding', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '通知规则页待通知告警', priority: 'P0', actionLabel: '确认平台转发', actionDetail: '核对平台转发配置', sla: '30 分钟确认', vehicleLabel: '粤A规则1 / VIN-RULES-001', realtimeHash: '#/realtime?keyword=VIN-RULES-001', historyHash: '#/history?keyword=VIN-RULES-001', rawHash: '#/history-query?keyword=VIN-RULES-001&tab=raw', vehicleHash: '#/detail?keyword=VIN-RULES-001', notificationText: '【P0 告警通知】通知规则页待通知' }], activeRuleCount: 2, p0RuleCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '通知规则' })).toBeInTheDocument(); expect(screen.getByText('无来源规则')).toBeInTheDocument(); expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument(); expect(screen.getAllByText('P0 实时中断').length).toBeGreaterThan(0); expect(screen.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThan(0); expect(screen.getByText('告警处置队列')).toBeInTheDocument(); expect(screen.getByText('下一步处置')).toBeInTheDocument(); expect(screen.getByText('主责任方')).toBeInTheDocument(); expect(screen.getAllByText('证据完整').length).toBeGreaterThan(0); expect(screen.getByText('核对平台转发配置')).toBeInTheDocument(); expect(screen.getByText('通知执行矩阵')).toBeInTheDocument(); expect(screen.getByText('无来源规则 / 平台接入')).toBeInTheDocument(); expect(screen.getAllByText('邮件 / 企业微信').length).toBeGreaterThan(0); expect(screen.getAllByText('30 分钟升级').length).toBeGreaterThan(0); expect(screen.getByText('SLA升级态势')).toBeInTheDocument(); expect(screen.getAllByText('已超时').length).toBeGreaterThan(0); expect(screen.getByText('即将升级')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制SLA升级报告' })).toBeInTheDocument(); expect(screen.getByText('当前待通知告警')).toBeInTheDocument(); expect(screen.getAllByText('粤A规则1 / VIN-RULES-001').length).toBeGreaterThan(0); expect(screen.getByText('确认平台转发')).toBeInTheDocument(); expect(screen.getByText('通知规则页待通知告警')).toBeInTheDocument(); expect(screen.getByText('platform-rules-test')).toBeInTheDocument(); expect(screen.getByText('通知覆盖与升级风险')).toBeInTheDocument(); expect(screen.getByText('责任覆盖')).toBeInTheDocument(); expect(screen.getByText('活跃命中')).toBeInTheDocument(); expect(screen.getByText('P0压力')).toBeInTheDocument(); expect(screen.getByText('升级风险')).toBeInTheDocument(); expect(screen.getByText('链路前置')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制通知覆盖报告' })).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/notification-plan?limit=50'), undefined); fireEvent.click(screen.getByRole('button', { name: '复制通知规则Runbook' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知规则Runbook】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('无来源规则 / P0 / 平台接入')); fireEvent.click(screen.getByRole('button', { name: '导出通知规则CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:notification-rules'); fireEvent.click(screen.getByRole('button', { name: '复制待通知汇总' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【当前待通知告警】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A规则1 / VIN-RULES-001')); fireEvent.click(screen.getByRole('button', { name: '复制升级值班清单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警升级值班清单】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('值班结论:P0 1 条,需要立即确认')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略:P0 实时中断 / 接入运维 + 业务责任人 / 邮件 / 企业微信')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级窗口:30 分钟升级')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. [P0] 无来源规则 / 平台接入 / 命中 3 / SLA 30 分钟确认')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:确认平台转发 - 核对平台转发配置')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-RULES-001')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=VIN-RULES-001&tab=raw')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('超过升级窗口未恢复时')); fireEvent.click(screen.getByRole('button', { name: '复制通知演练包' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知演练包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-rules-test')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('规则规模:P0 1 类,P1 1 类,待通知 1 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0策略:P0 实时中断 / 接入运维 + 业务责任人 / 邮件 / 企业微信 / 30 分钟升级')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 对第一条待通知告警打开车辆服务、实时、轨迹、RAW 四类证据')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知文本:【P0 告警通知】通知规则页待通知')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=VIN-RULES-001')); fireEvent.click(screen.getByRole('button', { name: '复制通知执行矩阵' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知执行矩阵】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('执行态势:P0 1 / 已超时 1 / 证据不完整 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知:接入运维 + 业务责任人 / 邮件 / 企业微信')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('规则:无来源规则 / 平台接入')); fireEvent.click(screen.getByRole('button', { name: '复制通知覆盖报告' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知覆盖与升级风险】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('责任覆盖:2 个责任方,2 类活跃规则')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警压力:待通知 1 / P0 1 / 证据不完整 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级风险:已超时 1 / 即将升级 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知渠道:邮件 / 企业微信;每日汇总邮件')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台接入')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主策略:P0 实时中断 / 接入运维 + 业务责任人 / 邮件 / 企业微信')); fireEvent.click(screen.getByRole('button', { name: '复制SLA升级报告' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警SLA升级报告】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级态势:已超时 1 / 即将升级 0 / 正常跟进 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[P0] 粤A规则1 / VIN-RULES-001')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('策略:P0 实时中断 / 接入运维 + 业务责任人 / 30 分钟升级')); fireEvent.click(screen.getByRole('button', { name: '复制单条告警通知' })); expect(writeText).toHaveBeenCalledWith('【P0 告警通知】通知规则页待通知'); fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001'); }); test('renders ops quality as a standalone runtime health page', async () => { window.history.replaceState(null, '', '/#/ops-quality'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [ { name: 'gb32960-gateway', status: 'ok', detail: 'listening :32960' }, { name: 'kafka', status: 'warning', detail: 'lag 42' } ], kafkaLag: 42, activeConnections: 1234, capacityMetrics: { activeConnections: 1234, kafkaLag: 42, bridgeConsumerPending: 1482047, bridgeAckPending: 4000, bridgeBatchPendingMessages: 12, fastWriterConsumerPending: 8, fastWriterAckPending: 0, fastWriterBatchPending: 0, historyBatchPending: 2, historyRowsPending: 320 }, capacityFindings: [ 'Kafka lag 42', 'bridge ack pending 4000 exceeds 100', 'bridge consumer pending 1482047 exceeds 10000', 'Redis online key below expected' ], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: false, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-ops-test', amapWebJsConfigured: true, amapApiConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false, amapSecurityServiceHost: '/_AMapService' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/source-readiness')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1033, onlineVehicles: 208, kafkaLag: 42, activeConnections: 1234, redisOnlineKeys: 368, platformRelease: 'platform-ops-test', sources: [ { protocol: 'GB32960', role: '整车与氢能实时数据主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 42', action: '核对现代和交投平台转发清单。', acceptance: '缺失来源车辆下降,单源车辆可解释。', vehiclesHash: '#/vehicles?protocol=GB32960&missingProtocol=GB32960', realtimeHash: '#/realtime?protocol=GB32960&online=online', historyHash: '#/history-query?protocol=GB32960&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=GB32960' }, { protocol: 'JT808', role: '位置、总里程和设备在线主来源', online: 73, total: 340, onlineRate: 21.47, missingVehicles: 693, severity: 'warning', status: '覆盖不足', evidence: '在线 73/340,缺失车辆 693,Kafka Lag 42', action: '补齐手机号绑定和注册缺失车辆。', acceptance: '未知 VIN 注册下降,位置与里程持续可查。', vehiclesHash: '#/vehicles?protocol=JT808&missingProtocol=JT808', realtimeHash: '#/realtime?protocol=JT808&online=online', historyHash: '#/history-query?protocol=JT808&tab=raw&includeFields=true', alertHash: '#/alert-events?protocol=JT808' } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument(); expect(screen.getByText('platform-ops-test')).toBeInTheDocument(); expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('368')).toBeInTheDocument(); expect(screen.getByText('TDengine 写入')).toBeInTheDocument(); expect(screen.getByText('MySQL 写入')).toBeInTheDocument(); expect(screen.getByText('服务端 API Key 已配置')).toBeInTheDocument(); expect(screen.getByText('/_AMapService')).toBeInTheDocument(); expect(screen.getByText('安全码未暴露')).toBeInTheDocument(); expect(screen.getByText('gb32960-gateway')).toBeInTheDocument(); expect(screen.getByText('消息链路积压')).toBeInTheDocument(); expect(screen.getByText('快速写入待消费')).toBeInTheDocument(); expect(screen.getByText('历史行待写')).toBeInTheDocument(); expect(screen.getByText('1,482,047')).toBeInTheDocument(); expect(screen.getByText('4,000')).toBeInTheDocument(); expect(screen.getByText('容量风险处置工作台')).toBeInTheDocument(); expect(screen.getByText('稳定 NATS 到 Kafka 桥接')).toBeInTheDocument(); expect(screen.getByText('恢复核心存储可写')).toBeInTheDocument(); expect(screen.getByText(/历史待写 320 行/)).toBeInTheDocument(); expect(screen.getByText('Kafka lag 42')).toBeInTheDocument(); expect(screen.getByText('NATS 桥接 ACK 未确认')).toBeInTheDocument(); expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('桥接处置顺序')).toBeInTheDocument(); expect(screen.getByText('数据源生产就绪度')).toBeInTheDocument(); expect(await screen.findByText('GB32960')).toBeInTheDocument(); expect(screen.getByText('JT808')).toBeInTheDocument(); expect(screen.getByText('整车与氢能实时数据主来源')).toBeInTheDocument(); expect(screen.getAllByText('覆盖不足').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线 73/340,缺失车辆 693,Kafka Lag 42').length).toBeGreaterThanOrEqual(1); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/source-readiness'), undefined); fireEvent.click(screen.getByRole('button', { name: '复制容量交接' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台运维容量交接】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-ops-test')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Kafka Lag:42')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('NATS桥接:consumer pending 1,482,047 / ack pending 4,000')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('存储状态:TDengine 正常 / MySQL 异常')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 先看 Kafka Lag 与 NATS ACK pending')); fireEvent.click(screen.getByRole('button', { name: '复制处置计划' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台容量处置计划】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0 稳定 NATS 到 Kafka 桥接')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('负责人:数据库 / 应用')); const sourceReadinessCopyButton = screen.getByText('复制就绪度报告').closest('button'); expect(sourceReadinessCopyButton).not.toBeNull(); fireEvent.click(sourceReadinessCopyButton as HTMLButtonElement); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【数据源生产就绪度】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('GB32960 - 覆盖不足')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?protocol=GB32960&online=online')); }); test('copies notification text from quality priority queue', async () => { window.history.replaceState(null, '', '/#/alert-events'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤A通知1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '手机号未映射 VIN,需要维护 binding' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '复制通知' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【P0 告警通知】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A通知1 / 13307795425')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题:VIN 缺失')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:2 小时修复')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最后时间:2026-07-03 20:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:手机号未映射 VIN,需要维护 binding')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时定位:http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/alert-events')); }); test('copies dispatch ticket from quality priority queue', async () => { window.history.replaceState(null, '', '/#/alert-events'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤A工单1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '手机号未映射 VIN,需要维护 binding' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); fireEvent.click(await screen.findByRole('button', { name: '复制工单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警处置工单】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('标题:[P0] VIN 缺失 - 粤A工单1 / 13307795425')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('责任团队:车辆档案')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('触发规则:来源有数据但无法归并到 VIN')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('来源地址:115.231.168.135:43625')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:维护身份绑定')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('处置步骤:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 打开车辆服务核对当前身份解析结果')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 核对实时定位、轨迹证据和 RAW 证据')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('3. 完成后确认车辆服务状态恢复,问题不再命中当前告警')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收标准:车辆能解析到 VIN,实时/历史/RAW 证据可通过同一车辆服务查询')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=%E7%B2%A4A%E5%B7%A5%E5%8D%951&protocol=JT808')); }); test('copies priority queue notification digest on quality page', async () => { window.history.replaceState(null, '', '/#/alert-events'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'platform-20260704153951', requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 2, issueRecordCount: 2, errorCount: 1, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }, { name: 'JT808', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }, { name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-DIGEST-001', plate: '粤A汇总1', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '车辆无任何来源' }, { vin: '', plate: '粤A汇总2', phone: '13307795426', sourceEndpoint: '115.231.168.135:43626', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'warning', lastSeen: '2026-07-03 20:10:10', detail: '手机号未映射 VIN' } ], total: 2, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); expect(await screen.findByText('platform-20260704153951')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制通知交接包' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知交接包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704153951')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:全部告警')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题车辆:2;问题记录:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('优先级:P0 1 条 / P1 1 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('活跃规则:2 类')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略:P0 实时中断 / 接入运维 + 业务责任人 / 站内告警 / 邮件 / 企业微信 / 30 分钟升级')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统一验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. [P0] 粤A汇总1 / VIN-DIGEST-001')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时:http://localhost:3000/#/realtime?keyword=VIN-DIGEST-001&protocol=VEHICLE_SERVICE')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW:http://localhost:3000/#/history-query?keyword=VIN-DIGEST-001&protocol=VEHICLE_SERVICE&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. [P1] 粤A汇总2 / 13307795426')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知规则:http://localhost:3000/#/notification-rules')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运维质量:http://localhost:3000/#/ops-quality')); fireEvent.click(screen.getByRole('button', { name: '复制优先队列通知汇总' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704153951')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知升级:P0 实时中断 / 30 分钟升级 / 接入运维 + 业务责任人')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收标准:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0:1 条,P1:1 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. [P0] 粤A汇总1 / VIN-DIGEST-001')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('确认平台转发')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. [P1] 粤A汇总2 / 13307795426')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('维护身份绑定')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/alert-events')); }); test('copies notification text from regular quality issue row', async () => { window.history.replaceState(null, '', '/#/alert-events'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'GB32960', count: 1 }], issueTypes: [{ name: 'FIELD_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-ROW-NOTIFY', plate: '粤A行通知', phone: '13307795425', sourceEndpoint: '115.29.187.205:32960', protocol: 'GB32960', issueType: 'FIELD_MISSING', severity: 'warning', lastSeen: '2026-07-03 20:12:10', detail: '位置字段缺失,影响轨迹回放' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('质量问题')).toBeInTheDocument(); const copyButtons = screen.getAllByRole('button', { name: '复制通知' }); expect(copyButtons.length).toBeGreaterThanOrEqual(2); fireEvent.click(copyButtons[copyButtons.length - 1]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【P1 告警通知】字段缺失')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A行通知 / VIN-ROW-NOTIFY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:核对解析字段')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:位置字段缺失,影响轨迹回放')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=VIN-ROW-NOTIFY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-ROW-NOTIFY&protocol=GB32960')); const evidenceButtons = screen.getAllByRole('button', { name: '复制证据包' }); expect(evidenceButtons.length).toBeGreaterThanOrEqual(2); fireEvent.click(evidenceButtons[evidenceButtons.length - 1]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警证据包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('优先级:P1')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收标准:缺失字段恢复解析,统计、定位或里程依赖的核心字段可查询')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时定位:http://localhost:3000/#/realtime?keyword=VIN-ROW-NOTIFY&protocol=GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-ROW-NOTIFY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=VIN-ROW-NOTIFY&protocol=GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程复核:http://localhost:3000/#/mileage?keyword=VIN-ROW-NOTIFY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-ROW-NOTIFY&protocol=GB32960')); }); test('opens realtime map context from quality priority queue', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-ALERT-MAP', plate: '粤A地图告警', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 来源间断,需要定位当前车辆' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/locations')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-ALERT-MAP', plate: '粤A地图告警', primaryProtocol: 'JT808', longitude: 113.24567, latitude: 23.12345, online: true, sourceCount: 1, onlineSourceCount: 1, lastSeen: '2026-07-03 20:12:11' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '实时定位' })); await waitFor(() => { expect(window.location.hash).toBe('#/realtime?keyword=VIN-ALERT-MAP&protocol=JT808'); }); expect(await screen.findByRole('heading', { name: '实时监控' })).toBeInTheDocument(); }); test('opens same-day trajectory evidence from quality priority queue', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'GB32960', count: 1 }], issueTypes: [{ name: 'FIELD_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-ALERT-TRACK', plate: '粤A轨迹告警', phone: '13307795425', sourceEndpoint: '115.29.187.205:32960', protocol: 'GB32960', issueType: 'FIELD_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'GB32960 位置字段缺失,需要核对轨迹' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations') || path.includes('/api/history/raw-frames')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '轨迹证据' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history?')).toBe(true); }); const params = new URLSearchParams(window.location.hash.slice('#/history?'.length)); expect(params.get('keyword')).toBe('VIN-ALERT-TRACK'); expect(params.get('protocol')).toBe('GB32960'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('tab')).toBeNull(); expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); }); test('opens same-day raw evidence from quality priority queue', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-ALERT-RAW', plate: '粤ARAW告警', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 来源间断,需要核对 RAW' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations') || path.includes('/api/history/raw-frames')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('处置优先队列')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'RAW证据' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history-query?')).toBe(true); }); const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length)); expect(params.get('keyword')).toBe('VIN-ALERT-RAW'); expect(params.get('protocol')).toBe('JT808'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('includeFields')).toBe('true'); expect(params.get('tab')).toBe('raw'); expect(await screen.findByRole('heading', { name: '数据导出' })).toBeInTheDocument(); }); test('opens vehicle service from quality issue with issue source evidence', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤AG18312', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 数据缺少 VIN 映射' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-JT808-QUALITY', plate: '粤AG18312', sourceCount: 1, onlineSourceCount: 1, coverageStatus: 'online', primaryProtocol: 'JT808', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 1 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('13307795425')).toBeInTheDocument(); expect(screen.getAllByText('维护身份绑定').length).toBeGreaterThan(0); expect(screen.getByText('数据已有来源但无法归并到 VIN,优先用车牌/手机号补齐绑定。')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '按车牌查车' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-JT808-QUALITY&protocol=JT808'); }); }); test('opens same-day history evidence from quality issue row', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-QUALITY-HISTORY', plate: '粤A告警证', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'warning', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 来源存在上报间断' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-QUALITY-HISTORY')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对历史' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN-QUALITY-HISTORY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('quality page surfaces escalation clock for priority issues', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 2, issueRecordCount: 2, errorCount: 1, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }, { name: 'JT808', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }, { name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-QUALITY-OVERDUE', plate: '粤A超时', phone: '13307790001', sourceEndpoint: 'vehicle_identity_binding', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '2026-07-03 08:00:00', detail: '绑定车辆无来源' }, { vin: 'VIN-QUALITY-RECENT', plate: '粤A正常', phone: '13307790002', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'warning', lastSeen: new Date().toISOString(), detail: '最近链路间断' } ], total: 2, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('告警升级时钟')).toBeInTheDocument(); expect(await screen.findByText('超 SLA')).toBeInTheDocument(); expect(screen.getByText('SLA 正常')).toBeInTheDocument(); expect(screen.getByText('VIN-QUALITY-OVERDUE')).toBeInTheDocument(); }); test('opens same-day raw evidence from quality issue row', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'GB32960', count: 1 }], issueTypes: [{ name: 'FIELD_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-QUALITY-RAW', plate: '粤A告警RAW', phone: '', sourceEndpoint: '115.29.187.205:32960', protocol: 'GB32960', issueType: 'FIELD_MISSING', severity: 'warning', lastSeen: '2026-07-03 20:12:10', detail: 'GB32960 实时帧缺少氢耗字段' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations') || path.includes('/api/history/raw-frames')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-QUALITY-RAW')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对 RAW' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history-query?')).toBe(true); }); const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length)); expect(params.get('keyword')).toBe('VIN-QUALITY-RAW'); expect(params.get('protocol')).toBe('GB32960'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('includeFields')).toBe('true'); expect(params.get('tab')).toBe('raw'); expect(await screen.findByRole('heading', { name: '数据导出' })).toBeInTheDocument(); }); test('copies trajectory playback summary from history page', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-HISTORY-SUMMARY', plate: '粤A轨迹1', protocol: 'JT808', longitude: 113.201, latitude: 23.122, speedKmh: 12.5, totalMileageKm: 1000, deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:02' }, { vin: 'VIN-HISTORY-SUMMARY', plate: '粤A轨迹1', protocol: 'JT808', longitude: 113.301, latitude: 23.222, speedKmh: 58.8, totalMileageKm: 1022.6, deviceTime: '2026-07-03 11:00:00', serverTime: '2026-07-03 11:00:02' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-history-evidence-001', vin: 'VIN-HISTORY-SUMMARY', plate: '粤A轨迹1', protocol: 'JT808', frameType: '0x0200', rawSizeBytes: 96, deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:02', parsedFields: { 'jt808.location.longitude': 113.201, 'jt808.location.total_mileage_km': 1000 } }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-HISTORY-SUMMARY')).length).toBeGreaterThan(0); expect(screen.getByText('客户轨迹决策台')).toBeInTheDocument(); expect(screen.getByText('先判断轨迹能否复盘,再进入 RAW、字段和里程证据')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户轨迹决策 复盘结论 复制决策' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户轨迹决策 定位覆盖 播放轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户轨迹决策 时间断点 复盘包' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户轨迹决策 里程速度 里程复核' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户轨迹决策 证据交付 RAW证据' })).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: /复制决策说明/ })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户轨迹决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:可复盘,需附异常说明')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('客户复盘路径:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('4. 最后回到证据:异常点必须打开 RAW、字段明细和里程统计复核。')); fireEvent.click(screen.getByRole('button', { name: /复制轨迹摘要/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹回放摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('查询范围:2026-07-03 至 2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹点:2,有效定位:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程:22.6 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最高速度:58.8 km/h')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('起点:2026-07-03 10:00:00')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('终点:2026-07-03 11:00:00')); fireEvent.click(screen.getAllByRole('button', { name: /复制数据复核包/ })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【数据复核包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('位置记录:2,有效定位:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:1,字段明细:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常:断点 1 / 里程回退 0 / 超速 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史位置:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=location&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('字段明细:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=fields&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程复核:http://localhost:3000/#/mileage?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-HISTORY-SUMMARY&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: /复制轨迹影响/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹运营影响】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务状态:需要复核')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹范围:位置 2 / 有效定位 2 / 原始记录 1 / 字段明细 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位覆盖率:100%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('回放跨度:60 分钟,采样间隔:60 分钟/点')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常影响:断点 1 / 里程回退 0 / 超速 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图能力:高德 JS API 待配置')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据导出:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=location&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); fireEvent.click(screen.getByRole('button', { name: /复制轨迹复盘包/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹复盘交接包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹规模:位置 2 / 有效定位 2 / 原始记录 1 / 字段明细 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹质量:定位覆盖率 100% / 回放跨度 60 分钟 / 采样间隔 60 分钟/点')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程速度:区间里程 22.6 km / 最高速度 58.8 km/h')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前回放点:点 1/2,2026-07-03 10:00:00,12.5 km/h,1,000 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常判读:断点 1 / 里程回退 0 / 超速 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('下一步:建议核对原始记录、字段明细、当日里程统计,并确认平台是否存在断链或补发。')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); }); test('opens same-day mileage statistics from quality issue row', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'FIELD_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-QUALITY-MILEAGE', plate: '粤A告警里', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'FIELD_MISSING', severity: 'warning', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 位置帧缺少总里程字段' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-QUALITY-MILEAGE', plate: '粤A告警里', source: 'JT808', date: '2026-07-03', startMileageKm: 200, endMileageKm: 218.8, dailyMileageKm: 18.8, anomalySeverity: 'warning' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/statistics/online-summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 4, onlineVehicleCount: 3, offlineVehicleCount: 1, onlineRatePercent: 75, redisOnlineKeys: 92, protocolStats: [ { protocol: 'JT808', online: 2, total: 3 }, { protocol: 'GB32960', online: 1, total: 2 } ], evidence: '由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-QUALITY-MILEAGE')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对里程' })); await waitFor(() => { expect(window.location.hash).toBe('#/mileage?keyword=VIN-QUALITY-MILEAGE&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('drills into quality issues by issue type', async () => { window.history.replaceState(null, '', '/#/alert-events'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); const issueButtons = await screen.findAllByRole('button', { name: /主要问题 暂无数据来源 1/ }); const issueSummaryButton = issueButtons[issueButtons.length - 1]; expect(issueButtons.length).toBeGreaterThanOrEqual(1); fireEvent.click(issueSummaryButton as HTMLElement); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?issueType=NO_SOURCE&limit=20&offset=0'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/summary?issueType=NO_SOURCE'), undefined); }); test('drills into quality issues by protocol bucket', async () => { window.history.replaceState(null, '', '/#/alert-events'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 2, issueRecordCount: 3, errorCount: 1, warningCount: 2, protocols: [ { name: 'JT808', count: 2 }, { name: 'VEHICLE_SERVICE', count: 1 } ], issueTypes: [{ name: 'VIN_MISSING', count: 2 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 3, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 质量问题' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?protocol=JT808&limit=20&offset=0'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/summary?protocol=JT808'), undefined); expect(window.location.hash).toBe('#/alert-events?protocol=JT808'); }); test('drills into quality issues by issue type bucket', async () => { window.history.replaceState(null, '', '/#/alert-events'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 2, issueRecordCount: 4, errorCount: 2, warningCount: 2, protocols: [{ name: 'JT808', count: 4 }], issueTypes: [ { name: 'VIN_MISSING', count: 2 }, { name: 'LINK_GAP', count: 2 } ] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 4, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '查看 VIN 缺失质量问题' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?issueType=VIN_MISSING&limit=20&offset=0'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/summary?issueType=VIN_MISSING'), undefined); expect(window.location.hash).toBe('#/alert-events?issueType=VIN_MISSING'); }); test('applies shareable quality filters from hash', async () => { window.history.replaceState(null, '', '/#/alert-events?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE&limit=20&offset=0'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/summary?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'), undefined); }); test('shows and clears current quality filters', async () => { window.history.replaceState(null, '', '/#/alert-events?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前筛选')).toBeInTheDocument(); expect(screen.getByText('关键词:粤A')).toBeInTheDocument(); expect(screen.getByText('数据来源:车辆服务')).toBeInTheDocument(); expect(screen.getByText('问题类型:暂无数据来源')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?limit=20&offset=0'), undefined); }); expect(window.location.hash).toBe('#/alert-events'); }); test('copies shareable quality filter link', async () => { window.history.replaceState(null, '', '/#/alert-events?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'); const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 0, warningCount: 1, protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], issueTypes: [{ name: 'NO_SOURCE', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: /复制筛选链接/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/#/alert-events?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE')); }); }); test('opens vehicle detail from shareable hash', async () => { window.history.replaceState(null, '', '/#/detail?keyword=%E7%B2%A4AG18312'); vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { vin: 'LB9A32A24R0LS1426', lookupKey: '粤AG18312', lookupResolved: true, resolution: { lookupKey: '粤AG18312', resolved: true, vin: 'LB9A32A24R0LS1426', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response); render(); expect(await screen.findByDisplayValue('粤AG18312')).toBeInTheDocument(); expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1); }); test('shows vehicle context when opening detail from shareable hash', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], primaryProtocol: 'JT808', coverageStatus: 'partial', sourceCount: 2, onlineSourceCount: 1, lastSeen: '2026-07-03 20:12:10', realtimeCount: 2, historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, sources: ['GB32960', 'JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704155844' } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('粤AG18312 / VIN001')).toBeInTheDocument(); expect(screen.getByText('当前车辆:来源不完整')).toBeInTheDocument(); }); test('refreshes vehicle context when hash changes to another detail vehicle', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN002', plate: '粤B20002', phone: '', oem: 'G7s', protocols: ['JT808'], primaryProtocol: 'JT808', coverageStatus: 'offline', sourceCount: 1, onlineSourceCount: 0, lastSeen: '2026-07-03 20:10:10', realtimeCount: 1, historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN002', lookupKey: 'VIN002', lookupResolved: true, sources: ['JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); window.history.pushState(null, '', '/#/detail?keyword=VIN002'); window.dispatchEvent(new HashChangeEvent('hashchange')); expect(await screen.findByText('粤B20002 / VIN002')).toBeInTheDocument(); expect(screen.getByText('当前车辆:车辆离线')).toBeInTheDocument(); }); test('shows vehicle context when opening detail from sidebar navigation', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'LB9A32A24R0LS1426', plate: '粤AGQ8398', phone: '', oem: 'G7s', protocols: ['JT808'], primaryProtocol: 'JT808', coverageStatus: 'offline', sourceCount: 1, onlineSourceCount: 0, lastSeen: '2026-07-03 20:10:10', realtimeCount: 1, historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/dashboard/summary')) { return { ok: true, json: async () => ({ data: { onlineVehicles: 3, activeToday: 4, frameToday: 1286320, issueVehicles: 7, kafkaLag: 0, protocols: [], serviceStatuses: [], linkHealth: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'LB9A32A24R0LS1426', lookupKey: 'LB9A32A24R0LS1426', lookupResolved: true, sources: ['JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(screen.getAllByText('车辆档案')[0]); expect(await screen.findByText('粤AGQ8398 / LB9A32A24R0LS1426')).toBeInTheDocument(); expect(screen.getByText('当前车辆:车辆离线')).toBeInTheDocument(); }); test('applies protocol from shareable history hash to API requests', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=JT808'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('protocol=JT808'), undefined); expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ method: 'POST', body: expect.stringContaining('"protocol":"JT808"') })); expect(screen.getByText('当前车辆:粤AG18312')).toBeInTheDocument(); expect(screen.getByText('当前来源:JT808')).toBeInTheDocument(); }); test('shows and clears current history filters while keeping vehicle scope', async () => { window.history.replaceState(null, '', '/#/history-query?keyword=VIN-HISTORY-001&protocol=JT808&dateFrom=2026-07-01+00%3A00%3A00&dateTo=2026-07-01+23%3A59%3A59&includeFields=true&fields=jt808.location.longitude%2Cjt808.location.latitude&tab=raw'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前历史筛选')).toBeInTheDocument(); expect(screen.getByText('车辆:VIN-HISTORY-001')).toBeInTheDocument(); expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); expect(screen.getByText('开始时间:2026-07-01 00:00:00')).toBeInTheDocument(); expect(screen.getByText('结束时间:2026-07-01 23:59:59')).toBeInTheDocument(); expect(screen.getAllByText('返回字段明细').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('字段裁剪:2 个')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?limit=10&offset=0&keyword=VIN-HISTORY-001'), undefined); }); expect(window.location.hash).toBe('#/history-query?keyword=VIN-HISTORY-001&tab=raw'); }); test('shows parsed field history as a flattened evidence table', async () => { window.history.replaceState(null, '', '/#/history-query?keyword=VIN-FIELDS-001&protocol=GB32960&tab=fields&fields=gb32960.vehicle.speed_kmh'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-field-001', vin: 'VIN-FIELDS-001', plate: '粤A字段1', protocol: 'GB32960', frameType: 'realtime', rawSizeBytes: 256, deviceTime: '2026-07-03 11:56:06', serverTime: '2026-07-03 11:56:07', parsedFields: { 'gb32960.vehicle.speed_kmh': 42, 'gb32960.vehicle.soc_percent': 78.4 } }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '数据导出' })).toBeInTheDocument(); expect(screen.getByText('客户轨迹决策台')).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: /复制决策说明/ }).length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '客户轨迹决策 证据交付 RAW证据' })).toBeInTheDocument(); expect(screen.getByText('客户数据交付包')).toBeInTheDocument(); expect(screen.getByText('面向客户交付时,先确认车辆、时间、交付物和质量提示,再导出位置、RAW 或字段明细,避免把内部排查过程直接暴露给客户。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户数据交付 原始证据 导出RAW' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户数据交付 字段明细 字段明细' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户数据交付 质量提示 复制说明' })).toBeInTheDocument(); expect(screen.getByText('客户交付物清单')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 位置历史 导出位置' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 轨迹回放 查看轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 里程复核 里程复核' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 RAW证据 导出RAW' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 字段明细 字段明细' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户交付清单 质量提示 复制清单' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /复制交付清单/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:位置历史 / 轨迹回放 / 里程复核 / RAW证据 / 字段明细 / 质量提示')); }); expect(await screen.findByRole('tab', { name: '字段明细' })).toHaveAttribute('aria-selected', 'true'); expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ method: 'POST', body: expect.stringContaining('"includeFields":true') })); expect(screen.getAllByText('gb32960.vehicle.speed_kmh').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('gb32960.vehicle.soc_percent')).toBeInTheDocument(); expect(screen.getByText('42')).toBeInTheDocument(); expect(screen.getByText('78.4')).toBeInTheDocument(); expect(screen.getByText('当前页 2 个字段')).toBeInTheDocument(); }); test('updates history hash when vehicle history filters are submitted', async () => { window.history.replaceState(null, '', '/#/history'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await screen.findByRole('heading', { name: '轨迹回放' }); fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号'), { target: { value: '粤AG18312' } }); fireEvent.click(screen.getAllByText('全部来源')[0]); fireEvent.click(await screen.findByText('JT808')); fireEvent.change(screen.getByPlaceholderText('2026-07-03 00:00:00'), { target: { value: '2026-07-01 00:00:00' } }); fireEvent.change(screen.getByPlaceholderText('2026-07-03 23:59:59'), { target: { value: '2026-07-01 23:59:59' } }); fireEvent.click(screen.getByRole('button', { name: 'search 查询' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=%E7%B2%A4AG18312&protocol=JT808&dateFrom=2026-07-01+00%3A00%3A00&dateTo=2026-07-01+23%3A59%3A59'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateFrom=2026-07-01+00%3A00%3A00'), undefined); expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ method: 'POST', body: expect.stringContaining('"dateFrom":"2026-07-01 00:00:00"') })); }); test('prevents unscoped raw parsed-field query from history form', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-SAFE-RAW'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await screen.findByRole('heading', { name: '轨迹回放' }); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ body: expect.stringContaining('VIN-SAFE-RAW') })); }); fetchMock.mockClear(); const keywordInput = screen.getByPlaceholderText('VIN / 车牌 / 手机号'); fireEvent.change(keywordInput, { target: { value: '' } }); fireEvent.input(keywordInput, { target: { value: '' } }); await waitFor(() => { expect(keywordInput).toHaveValue(''); }); fireEvent.click(screen.getByRole('checkbox', { name: '返回字段明细' })); fireEvent.click(screen.getByRole('button', { name: 'search 查询' })); expect((await screen.findAllByText('字段明细查询需要车辆、时间范围或字段裁剪')).length).toBeGreaterThanOrEqual(1); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); expect(fetchMock).not.toHaveBeenCalledWith('/api/history/raw-frames/query', expect.anything()); }); test('shows trajectory playback workspace from history locations', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-001&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-001', plate: '粤A轨迹1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-001', plate: '粤A轨迹1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); expect(screen.getByText('历史数据交付任务板')).toBeInTheDocument(); expect(screen.getByText('2 个有效点')).toBeInTheDocument(); expect(screen.getByText('围绕当前车辆生成可播放轨迹,用于定位复盘和客户问询。')).toBeInTheDocument(); expect(screen.getByText('位置导出')).toBeInTheDocument(); expect(screen.getByText('原始证据')).toBeInTheDocument(); expect(screen.getAllByText('字段裁剪').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('轨迹运营影响')).toBeInTheDocument(); expect(screen.getByText('轨迹范围')).toBeInTheDocument(); expect(screen.getByText('定位覆盖')).toBeInTheDocument(); expect(screen.getByText('异常影响')).toBeInTheDocument(); expect(screen.getAllByText('原始记录').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('地图能力')).toBeInTheDocument(); expect(screen.getByText('轨迹回放作业台')).toBeInTheDocument(); expect(screen.getByText('2 个有效轨迹点')).toBeInTheDocument(); expect(screen.getByText('区间里程')).toBeInTheDocument(); expect(screen.getAllByText('12.4 km').length).toBeGreaterThan(0); expect(screen.getByText('最高速度')).toBeInTheDocument(); expect(screen.getByText('42 km/h')).toBeInTheDocument(); expect(screen.getByText('起点')).toBeInTheDocument(); expect(screen.getByText('终点')).toBeInTheDocument(); }); test('shows trajectory evidence quality on history playback workspace', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-QUALITY&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-QUALITY', plate: '粤A轨迹质量', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-QUALITY', plate: '粤A轨迹质量', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('轨迹证据质量')).toBeInTheDocument(); expect(screen.getByText('定位覆盖率')).toBeInTheDocument(); expect(screen.getAllByText('100%').length).toBeGreaterThan(0); expect(screen.getByText('回放跨度')).toBeInTheDocument(); expect(screen.getByText('10 分钟')).toBeInTheDocument(); expect(screen.getByText('采样间隔')).toBeInTheDocument(); expect(screen.getByText('10 分钟/点')).toBeInTheDocument(); }); test('flags trajectory playback anomalies for gap mileage and speed review', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-ANOMALY&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-ANOMALY', plate: '粤A轨迹异', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 18, socPercent: 0, totalMileageKm: 300, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-ANOMALY', plate: '粤A轨迹异', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 132, socPercent: 0, totalMileageKm: 318, lastSeen: '2026-07-03 10:40:00', deviceTime: '2026-07-03 10:40:00', serverTime: '2026-07-03 10:40:01' }, { vin: 'VIN-TRACK-ANOMALY', plate: '粤A轨迹异', protocol: 'JT808', longitude: 113.4, latitude: 23.3, speedKmh: 22, socPercent: 0, totalMileageKm: 316, lastSeen: '2026-07-03 10:45:00', deviceTime: '2026-07-03 10:45:00', serverTime: '2026-07-03 10:45:01' } ], total: 3, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('轨迹异常判读')).toBeInTheDocument(); expect(screen.getByText('数据断点')).toBeInTheDocument(); expect(screen.getByText('1 处')).toBeInTheDocument(); expect(screen.getByText('里程回退')).toBeInTheDocument(); expect(screen.getByText('2 km')).toBeInTheDocument(); expect(screen.getByText('速度异常')).toBeInTheDocument(); expect(screen.getAllByText('132 km/h').length).toBeGreaterThan(0); expect(screen.getByText('建议核对原始记录和当日里程统计')).toBeInTheDocument(); }); test('opens amap route from trajectory playback workspace', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AMAP&protocol=JT808'); const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-AMAP', plate: '粤A高德1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-AMAP', plate: '粤A高德1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '高德线路' })); expect(openSpy).toHaveBeenCalledWith( expect.stringContaining('https://uri.amap.com/navigation?from=113.2,23.1'), '_blank', 'noopener,noreferrer' ); expect(openSpy).toHaveBeenCalledWith( expect.stringContaining('to=113.3,23.2'), '_blank', 'noopener,noreferrer' ); }); test('opens amap marker when trajectory has one valid point', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AMAP-ONE&protocol=JT808'); const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-AMAP-ONE', plate: '粤A单点1', protocol: 'JT808', longitude: 113.28, latitude: 23.18, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' } ], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '高德线路' })); expect(openSpy).toHaveBeenCalledWith( expect.stringContaining('https://uri.amap.com/marker?position=113.28,23.18'), '_blank', 'noopener,noreferrer' ); }); test('resolves current trajectory point address through server-side amap api', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-ADDRESS&protocol=JT808'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-ADDRESS', plate: '粤A地址1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-ADDRESS', plate: '粤A地址1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/map/reverse-geocode')) { expect(path).toContain('longitude=113.2'); expect(path).toContain('latitude=23.1'); return { ok: true, json: async () => ({ data: { provider: 'AMap', longitude: 113.2, latitude: 23.1, formattedAddress: '广东省广州市天河区测试路', province: '广东省', city: '广州市', district: '天河区', adcode: '440106' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前回放点')).toBeInTheDocument(); expect(screen.getByText('地址未解析')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '解析地址' })); expect(await screen.findByText('广东省广州市天河区测试路')).toBeInTheDocument(); expect(screen.getByText('AMap')).toBeInTheDocument(); fireEvent.click(screen.getByText('复制轨迹复盘包')); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前点地址:广东省广州市天河区测试路')); }); }); test('controls trajectory playback current point from history locations', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-CONTROL&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' }, { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', protocol: 'JT808', longitude: 113.4, latitude: 23.3, speedKmh: 28, socPercent: 0, totalMileageKm: 119.9, lastSeen: '2026-07-03 10:20:00', deviceTime: '2026-07-03 10:20:00', serverTime: '2026-07-03 10:20:01' } ], total: 3, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-TRACK-CONTROL', plate: '粤A回放1', sourceCount: 1, onlineSourceCount: 1, coverageStatus: 'healthy', primaryProtocol: 'JT808', lastSeen: '2026-07-03 10:20:00', historyCount: 3, rawCount: 0, mileageCount: 1, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前回放点')).toBeInTheDocument(); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); expect(screen.getAllByText('2026-07-03 10:00:00').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '下一点' })); expect(screen.getByText('点 2 / 3')).toBeInTheDocument(); expect(screen.getAllByText('2026-07-03 10:10:00').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '选择轨迹点 3' })); expect(screen.getByText('点 3 / 3')).toBeInTheDocument(); fireEvent.change(screen.getByRole('slider', { name: '轨迹回放进度' }), { target: { value: '0' } }); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); const mapPointButtons = screen.getAllByRole('button', { name: '选择地图车辆 粤A回放1' }); fireEvent.click(mapPointButtons[0]); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); fireEvent.click(mapPointButtons[1]); expect(screen.getByText('点 2 / 3')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '上一点' })); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '回放点车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-TRACK-CONTROL&protocol=JT808'); }); }); test('auto plays trajectory playback points with selectable speed', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AUTO&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-TRACK-AUTO', plate: '粤A自动1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' }, { vin: 'VIN-TRACK-AUTO', plate: '粤A自动1', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' }, { vin: 'VIN-TRACK-AUTO', plate: '粤A自动1', protocol: 'JT808', longitude: 113.4, latitude: 23.3, speedKmh: 28, socPercent: 0, totalMileageKm: 119.9, lastSeen: '2026-07-03 10:20:00', deviceTime: '2026-07-03 10:20:00', serverTime: '2026-07-03 10:20:01' } ], total: 3, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('播放速度')).toBeInTheDocument(); vi.useFakeTimers(); expect(screen.getByText('点 1 / 3')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '播放轨迹' })); expect(screen.getByRole('button', { name: '暂停轨迹' })).toBeInTheDocument(); await vi.advanceTimersByTimeAsync(1200); expect(screen.getByText('点 2 / 3')).toBeInTheDocument(); await vi.advanceTimersByTimeAsync(2400); expect(screen.getByText('点 3 / 3')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '播放轨迹' })).toBeInTheDocument(); vi.useRealTimers(); }); test('exports current history location page as csv', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-EXPORT-LOC&protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:history-location'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-EXPORT-LOC', plate: '粤A导出1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11' }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { expect(init?.method).toBe('POST'); } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-EXPORT-LOC')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '导出位置当前页 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:history-location'); }); test('exports current raw frame page as csv with parsed fields', async () => { window.history.replaceState(null, '', '/#/history-query?keyword=VIN-EXPORT-RAW&protocol=GB32960&includeFields=true&tab=raw'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:history-raw'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-export-001', vin: 'VIN-EXPORT-RAW', plate: '粤A导出2', protocol: 'GB32960', frameType: 'REALTIME', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11', rawSizeBytes: 256, parsedFields: { 'gb32960.vehicle.speed_kmh': 42 } }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('raw-export-001')).length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '导出原始记录当前页 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:history-raw'); }); test('shows vehicle and source scope on mileage hash', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-001&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 2, sourceCount: 1, totalMileageKm: 12.3, averageMileagePerVin: 12.3 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前车辆:VIN-MILEAGE-001')).toBeInTheDocument(); expect(screen.getByText('当前来源:GB32960')).toBeInTheDocument(); }); test('applies mileage date range from shareable hash to API requests', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-002&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 2, sourceCount: 1, totalMileageKm: 12.3, averageMileagePerVin: 12.3 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/mileage/summary?'), undefined); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateFrom=2026-07-01'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateTo=2026-07-03'), undefined); expect(screen.getByText('当前车辆:VIN-MILEAGE-002')).toBeInTheDocument(); expect(screen.getByText('当前来源:JT808')).toBeInTheDocument(); }); test('shows and clears current mileage filters while keeping vehicle scope', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-002&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 2, sourceCount: 1, totalMileageKm: 12.3, averageMileagePerVin: 12.3 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前里程筛选')).toBeInTheDocument(); expect(screen.getByText('车辆:VIN-MILEAGE-002')).toBeInTheDocument(); expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); expect(screen.getByText('开始日期:2026-07-01')).toBeInTheDocument(); expect(screen.getByText('结束日期:2026-07-03')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/mileage/summary?keyword=VIN-MILEAGE-002'), undefined); }); expect(window.location.hash).toBe('#/mileage?keyword=VIN-MILEAGE-002'); }); test('updates mileage hash when mileage filters are submitted', async () => { window.history.replaceState(null, '', '/#/mileage'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 2, sourceCount: 1, totalMileageKm: 12.3, averageMileagePerVin: 12.3 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await screen.findByRole('heading', { name: '里程统计' }); fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号 / OEM'), { target: { value: '粤AG18312' } }); fireEvent.click(screen.getByText('全部来源')); fireEvent.click(await screen.findByText('JT808')); fireEvent.change(screen.getByPlaceholderText('2026-07-01'), { target: { value: '2026-07-01' } }); fireEvent.change(screen.getByPlaceholderText('2026-07-03'), { target: { value: '2026-07-03' } }); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => { expect(window.location.hash).toBe('#/mileage?keyword=%E7%B2%A4AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/mileage/summary?'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateFrom=2026-07-01'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateTo=2026-07-03'), undefined); }); test('opens current vehicle service from mileage header with current source evidence', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-HEADER&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 0, sourceCount: 1, totalMileageKm: 0, averageMileagePerVin: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN-MILEAGE-HEADER', resolved: true, vin: 'VIN-MILEAGE-HEADER', plate: '', phone: '', oem: '', protocols: ['GB32960'], online: true, lastSeen: '2026-07-03 20:12:10' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前来源:GB32960')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '当前车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-MILEAGE-HEADER&protocol=GB32960'); }); }); test('opens vehicle service from mileage row with row source evidence', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-ROW&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 12.3, averageMileagePerVin: 12.3 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MILEAGE-ROW', plate: '粤AG18312', source: 'JT808', date: '2026-07-03', startMileageKm: 100, endMileageKm: 112.3, dailyMileageKm: 12.3, anomalySeverity: '' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN-MILEAGE-ROW', resolved: true, vin: 'VIN-MILEAGE-ROW', plate: '粤AG18312', phone: '', oem: 'G7s', protocols: ['JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-MILEAGE-ROW')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-MILEAGE-ROW&protocol=JT808'); }); }); test('opens same-day trajectory playback from mileage row for evidence review', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-TRACK&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MILEAGE-TRACK', plate: '粤A轨迹核', source: 'JT808', date: '2026-07-03', startMileageKm: 200, endMileageKm: 218.8, dailyMileageKm: 18.8, anomalySeverity: 'warning' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-MILEAGE-TRACK')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对轨迹' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN-MILEAGE-TRACK&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('opens same-day raw frame evidence from mileage row', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-RAW&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MILEAGE-RAW', plate: '粤ARAW核', source: 'JT808', date: '2026-07-03', startMileageKm: 200, endMileageKm: 218.8, dailyMileageKm: 18.8, anomalySeverity: 'warning' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations') || path.includes('/api/history/raw-frames')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-MILEAGE-RAW')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对 RAW' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history-query?')).toBe(true); }); const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length)); expect(params.get('keyword')).toBe('VIN-MILEAGE-RAW'); expect(params.get('protocol')).toBe('JT808'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('tab')).toBe('raw'); expect(await screen.findByRole('heading', { name: '数据导出' })).toBeInTheDocument(); }); test('shows mileage anomaly action guidance', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: -3.2, averageMileagePerVin: -3.2 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MILEAGE-ANOMALY', plate: '粤A异常1', source: 'GB32960', date: '2026-07-03', startMileageKm: 100, endMileageKm: 96.8, dailyMileageKm: -3.2, anomalySeverity: 'warning' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-MILEAGE-ANOMALY')).toBeInTheDocument(); expect(screen.getByText('核对里程来源')).toBeInTheDocument(); expect(screen.getByText('日里程异常,优先核对当天首末总里程、来源断链和补传数据。')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '复制证据' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程异常证据包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A异常1 / VIN-MILEAGE-ANOMALY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计日期:2026-07-03')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('日里程:-3.2 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常等级:warning')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计查询:http://localhost:3000/#/mileage?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); }); test('shows mileage statistics workspace with trend source and definition', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-STATS'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 2, recordCount: 4, sourceCount: 2, totalMileageKm: 140, averageMileagePerVin: 70 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-MILEAGE-STATS', plate: '粤A统计1', source: 'JT808', date: '2026-07-01', startMileageKm: 100, endMileageKm: 140, dailyMileageKm: 40, anomalySeverity: '' }, { vin: 'VIN-MILEAGE-STATS', plate: '粤A统计1', source: 'GB32960', date: '2026-07-01', startMileageKm: 200, endMileageKm: 230, dailyMileageKm: 30, anomalySeverity: '' }, { vin: 'VIN-MILEAGE-STATS-2', plate: '粤A统计2', source: 'JT808', date: '2026-07-02', startMileageKm: 300, endMileageKm: 360, dailyMileageKm: 60, anomalySeverity: 'warning' }, { vin: 'VIN-MILEAGE-STATS-2', plate: '粤A统计2', source: 'GB32960', date: '2026-07-02', startMileageKm: 400, endMileageKm: 410, dailyMileageKm: 10, anomalySeverity: '' } ], total: 4, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('区间趋势')).toBeInTheDocument(); expect(screen.getAllByText('来源贡献').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('2026-07-01').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('2026-07-02').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('JT808').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('GB32960').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('当前页里程')).toBeInTheDocument(); expect(screen.getAllByText('140 km').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('统计闭合工作台')).toBeInTheDocument(); expect(screen.getByText('汇总总里程')).toBeInTheDocument(); expect(screen.getByText('明细合计')).toBeInTheDocument(); expect(screen.getByText('闭合差值')).toBeInTheDocument(); expect(screen.getByText('记录覆盖率')).toBeInTheDocument(); expect(screen.getAllByText('闭合通过').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('当前筛选范围汇总总里程与明细合计一致,可作为统计证据。').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '复制闭合摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '闭合复核轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '闭合复核 RAW' })).toBeInTheDocument(); expect(screen.getByText('统计发布审计')).toBeInTheDocument(); expect(screen.getAllByText('复核后发布').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('区间闭合').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('记录覆盖').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('在线证据')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制统计发布审计' })).toBeInTheDocument(); expect(screen.getAllByText('异常记录').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('1').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('最大单日')).toBeInTheDocument(); expect(screen.getAllByText('60 km').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('统计可信度')).toBeInTheDocument(); expect(screen.getByText('异常率')).toBeInTheDocument(); expect(screen.getByText('25%')).toBeInTheDocument(); expect(screen.getByText('最大单日占比')).toBeInTheDocument(); expect(screen.getByText('42.9%')).toBeInTheDocument(); expect(screen.getByText('分页覆盖率')).toBeInTheDocument(); expect(screen.getAllByText('100%').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('闭合校验').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('0 km').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计口径').length).toBeGreaterThanOrEqual(1); expect(screen.getByText((content) => content.includes('区间总值应等于各日里程之和'))).toBeInTheDocument(); }); test('shows vehicle-first statistics domains for one vehicle service', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-STATS-SERVICE&protocol=JT808'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 3, recordCount: 6, sourceCount: 2, totalMileageKm: 210, averageMileagePerVin: 70 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-STATS-SERVICE', plate: '粤A统计服务1', source: 'JT808', date: '2026-07-01', startMileageKm: 100, endMileageKm: 150, dailyMileageKm: 50, anomalySeverity: '' }, { vin: 'VIN-STATS-SERVICE', plate: '粤A统计服务1', source: 'GB32960', date: '2026-07-01', startMileageKm: 200, endMileageKm: 240, dailyMileageKm: 40, anomalySeverity: '' }, { vin: 'VIN-STATS-SERVICE-2', plate: '粤A统计服务2', source: 'JT808', date: '2026-07-02', startMileageKm: 300, endMileageKm: 380, dailyMileageKm: 80, anomalySeverity: 'warning' } ], total: 3, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/statistics/online-summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 4, onlineVehicleCount: 3, offlineVehicleCount: 1, onlineRatePercent: 75, redisOnlineKeys: 92, protocolStats: [ { protocol: 'JT808', online: 2, total: 3 }, { protocol: 'GB32960', online: 1, total: 2 } ], evidence: '由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/statistics/online-vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-STATS-SERVICE', plate: '粤A统计服务1', phone: '13307795425', oem: 'G7s', protocols: ['JT808', 'GB32960'], online: true, lastSeen: '2026-07-03 20:12:10', offlineDurationMinutes: 0, sourceCount: 2, onlineSourceCount: 2, bindingStatus: 'bound' }, { vin: 'VIN-STATS-SERVICE-OFFLINE', plate: '粤A离线1', phone: '13307795426', oem: '广安车联', protocols: ['JT808'], online: false, lastSeen: '2026-07-03 18:12:10', offlineDurationMinutes: 120, sourceCount: 1, onlineSourceCount: 0, bindingStatus: 'bound' } ], total: 2, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆统计服务总览')).toBeInTheDocument(); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('在线率与离线时长')).toBeInTheDocument(); expect(screen.getByText('数据完整性')).toBeInTheDocument(); expect(screen.getAllByText('来源一致性').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('三类数据源最终汇总为一个车辆服务统计视图')).toBeInTheDocument(); expect(screen.getByText('当前统计证据')).toBeInTheDocument(); expect(screen.getByText('3 车 / 2 来源 / 3 条明细')).toBeInTheDocument(); expect(screen.getByText('客户里程决策台')).toBeInTheDocument(); expect(screen.getByText('先判断能不能交付,再进入轨迹、RAW 和导出复核')).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: '复制决策说明' }).length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '客户里程决策 交付结论 复制决策' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 区间闭合 统计摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 覆盖率 导出明细' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 异常复核 异常证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 在线影响 在线态势' })).toBeInTheDocument(); expect(screen.getByText('客户里程复核交付包')).toBeInTheDocument(); expect(screen.getByText('面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与 RAW 证据复核。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 区间里程 复制交付' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 闭合校验 闭合摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 异常记录 异常证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 在线影响 在线态势' })).toBeInTheDocument(); expect(screen.getByText('统计影响范围')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制影响范围' })).toBeInTheDocument(); expect(screen.getByText('影响待复核')).toBeInTheDocument(); expect(screen.getByText('3 辆车')).toBeInTheDocument(); expect(screen.getByText('当前筛选会影响 6 条里程记录、210 km 统计口径和 1 辆离线车辆判断。')).toBeInTheDocument(); expect(screen.getByText('里程服务任务板')).toBeInTheDocument(); expect(screen.getAllByText('发布判断').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('区间复核')).toBeInTheDocument(); expect(screen.getByText('异常处理')).toBeInTheDocument(); expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '里程服务任务 发布判断 BI口径' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程服务任务 区间复核 轨迹证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程服务任务 异常处理 复制证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程服务任务 数据交付 导出明细' })).toBeInTheDocument(); expect(screen.getByText('影响车辆')).toBeInTheDocument(); expect(screen.getByText('统计规模')).toBeInTheDocument(); expect(screen.getByText('闭合影响')).toBeInTheDocument(); expect(screen.getByText('异常影响')).toBeInTheDocument(); expect(screen.getAllByText('发布判断').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('在线统计运营态势')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制在线态势' })).toBeInTheDocument(); expect(screen.getByText('离线车辆')).toBeInTheDocument(); expect(screen.getByText('协议覆盖')).toBeInTheDocument(); expect(screen.getByText('当前页离线')).toBeInTheDocument(); expect(screen.getByText('最长离线')).toBeInTheDocument(); expect(screen.getByText('统计证据')).toBeInTheDocument(); expect((await screen.findAllByText('75%')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('离线 1 车')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('Redis 在线 92 key')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算')).length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '复制影响范围' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【统计影响范围】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆:3 辆')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计记录:6 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:210 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线状态:3 在线 / 1 离线 / 在线率 75%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布判断:复核后发布')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务影响:当前统计范围需要先复核轨迹、RAW和离线车辆影响')); fireEvent.click(screen.getAllByRole('button', { name: '复制决策说明' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:复核后交付')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('客户判断路径:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 先看区间闭合:区间总值必须等于每日里程之和。')); fireEvent.click(screen.getByRole('button', { name: '复制在线态势' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【在线统计运营态势】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线概览:3 在线 / 1 离线 / 在线率 75%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Redis 在线 Key:92')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页车辆:2 / 当前筛选 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最长离线:粤A离线1 / 2 小时 / 2026-07-03 18:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. JT808:2/3,在线率 66.7%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 离线车辆优先打开车辆服务,核对实时、轨迹、RAW 和告警证据')); fireEvent.click(screen.getByRole('button', { name: '里程交付 区间里程 复制交付' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程复核交付包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:210 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布判断:复核后发布')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计口径:日里程按首末总里程差值计算,区间总值应等于每日里程之和')); expect(await screen.findByText('在线状态明细')).toBeInTheDocument(); expect(await screen.findByText('VIN-STATS-SERVICE-OFFLINE')).toBeInTheDocument(); expect(screen.getByText('粤A离线1')).toBeInTheDocument(); expect(screen.getAllByText('2 小时').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '复制在线状态清单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆在线状态交接】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线概览:3 在线 / 1 离线 / 在线率 75%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('协议在线:JT808 2/3;GB32960 1/2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 粤A离线1 / VIN-STATS-SERVICE-OFFLINE / 广安车联')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('离线时长:2 小时;最后上报:2026-07-03 18:12:10')); }); test('copies mileage statistics summary for operations reporting', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-COPY&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 2, recordCount: 4, sourceCount: 2, totalMileageKm: 140, averageMileagePerVin: 70 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-MILEAGE-COPY', plate: '粤A统计1', source: 'JT808', date: '2026-07-01', startMileageKm: 100, endMileageKm: 140, dailyMileageKm: 40, anomalySeverity: '' }, { vin: 'VIN-MILEAGE-COPY-2', plate: '粤A统计2', source: 'JT808', date: '2026-07-02', startMileageKm: 300, endMileageKm: 400, dailyMileageKm: 100, anomalySeverity: 'warning' } ], total: 2, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('区间趋势')).toBeInTheDocument(); expect(screen.getByText('BI发布口径')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制BI发布口径' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制统计发布审计' })).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '复制统计摘要' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程统计摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-MILEAGE-COPY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('日期范围:2026-07-01 至 2026-07-03')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:140 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页里程:140 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常记录:1')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最大单日:粤A统计2 / 2026-07-02 / 100 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计口径:区间总值等于各日里程之和')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计查询:http://localhost:3000/#/mileage?keyword=VIN-MILEAGE-COPY&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-MILEAGE-COPY&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: '复制BI发布口径' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【BI里程发布口径】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布结论:需复核后发布')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计口径:日里程按首末总里程差值计算,区间总值等于每日里程之和')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('闭合状态:分页抽样')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('记录覆盖率:50%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常记录:1')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('来源一致性:可做跨来源核对')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布风险:存在分页未全量、异常记录或闭合差值,需要先复核轨迹和 RAW 证据')); fireEvent.click(screen.getByRole('button', { name: '复制统计发布审计' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【统计发布审计】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布结论:复核后发布')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('审计结果:通过 1 / 复核 4 / 阻断 0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[复核] 记录覆盖:50%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[复核] 异常记录:1')); }); test('exports current mileage page as CSV', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-EXPORT&protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mileage-export'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 42.5, averageMileagePerVin: 42.5 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-MILEAGE-EXPORT', plate: '粤A导出1', source: 'JT808', date: '2026-07-03', startMileageKm: 100, endMileageKm: 142.5, dailyMileageKm: 42.5, anomalySeverity: '' } ], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-MILEAGE-EXPORT')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '导出里程当前页 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:mileage-export'); }); test('opens vehicle service from history results with row source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-JT808-001', plate: '粤AG18312', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11' }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN-JT808-001', resolved: true, vin: 'VIN-JT808-001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-JT808-001')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-JT808-001&protocol=JT808'); }); }); test('opens same-day mileage statistics from history location row', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-MILEAGE&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-HISTORY-MILEAGE', plate: '粤A历史账', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 0, totalMileageKm: 218.8, lastSeen: '2026-07-03 20:12:10', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11' }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-HISTORY-MILEAGE', plate: '粤A历史账', source: 'JT808', date: '2026-07-03', startMileageKm: 200, endMileageKm: 218.8, dailyMileageKm: 18.8, anomalySeverity: 'normal' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-HISTORY-MILEAGE')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '核对里程' })); await waitFor(() => { expect(window.location.hash).toBe('#/mileage?keyword=VIN-HISTORY-MILEAGE&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('opens same-day raw evidence from history location row', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-RAW&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-HISTORY-RAW', plate: '粤A原帧核', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 0, totalMileageKm: 218.8, lastSeen: '2026-07-03 20:12:10', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11' }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-from-location-001', vin: 'VIN-HISTORY-RAW', plate: '粤A原帧核', protocol: 'JT808', frameType: '0x0200', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11', rawSizeBytes: 128, parsedFields: { 'jt808.location.longitude': 113.2 } }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-HISTORY-RAW')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '核对原始记录' })); await waitFor(() => { expect(window.location.hash.startsWith('#/history-query?')).toBe(true); }); const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length)); expect(params.get('keyword')).toBe('VIN-HISTORY-RAW'); expect(params.get('protocol')).toBe('JT808'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('includeFields')).toBe('true'); expect(params.get('tab')).toBe('raw'); expect(await screen.findByRole('tab', { name: '原始记录' })).toHaveAttribute('aria-selected', 'true'); expect((await screen.findAllByText('raw-from-location-001')).length).toBeGreaterThanOrEqual(1); }); test('opens vehicle service from raw history rows with row source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-gb32960-001', vin: 'VIN-GB32960-001', plate: '粤AG18312', protocol: 'GB32960', frameType: 'REALTIME', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11', rawSizeBytes: 256, parsedFields: {} }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN-GB32960-001', resolved: true, vin: 'VIN-GB32960-001', plate: '粤AG18312', phone: '', oem: 'G7s', protocols: ['GB32960'], online: true, lastSeen: '2026-07-03 20:12:10' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('tab', { name: '原始记录' })); expect(await screen.findByText('raw-gb32960-001')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-GB32960-001&protocol=GB32960'); }); }); test('opens same-day mileage statistics from raw history row', async () => { window.history.replaceState(null, '', '/#/history-query?keyword=VIN-RAW-MILEAGE&protocol=JT808&tab=raw'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/locations')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/history/raw-frames/query')) { return { ok: true, json: async () => ({ data: { items: [{ id: 'raw-jt808-mileage-001', vin: 'VIN-RAW-MILEAGE', plate: '粤ARAW里', protocol: 'JT808', frameType: '0x0200', deviceTime: '2026-07-03 20:12:10', serverTime: '2026-07-03 20:12:11', rawSizeBytes: 128, parsedFields: { 'jt808.location.total_mileage_km': 218.8 } }], total: 1, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/summary')) { return { ok: true, json: async () => ({ data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/mileage/daily')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RAW-MILEAGE', plate: '粤ARAW里', source: 'JT808', date: '2026-07-03', startMileageKm: 200, endMileageKm: 218.8, dailyMileageKm: 18.8, anomalySeverity: '' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('raw-jt808-mileage-001')).length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '核对里程' })); await waitFor(() => { expect(window.location.hash).toBe('#/mileage?keyword=VIN-RAW-MILEAGE&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); expect(await screen.findByRole('heading', { name: '里程统计' })).toBeInTheDocument(); }); test('opens current vehicle service from history header with current source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN-JT808-HEADER&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN-JT808-HEADER', resolved: true, vin: 'VIN-JT808-HEADER', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前来源:JT808')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '当前车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-JT808-HEADER&protocol=JT808'); }); }); test('opens vehicle service from realtime vehicles with primary source evidence', async () => { window.history.replaceState(null, '', '/#/realtime'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MQTT-001', plate: '川AHTWO1', phone: '', oem: '宇通', protocols: ['GB32960', 'YUTONG_MQTT'], sourceCount: 2, onlineSourceCount: 1, online: true, primaryProtocol: 'YUTONG_MQTT', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10' }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-MQTT-001', plate: '川AHTWO1', sourceCount: 2, onlineSourceCount: 1, coverageStatus: 'online', primaryProtocol: 'YUTONG_MQTT', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-MQTT-001')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-MQTT-001&protocol=YUTONG_MQTT'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=VIN-MQTT-001&protocol=YUTONG_MQTT'), undefined); }); test('opens vehicle service from realtime vehicles with current source filter', async () => { window.history.replaceState(null, '', '/#/realtime?keyword=VIN-RT-FILTER&protocol=JT808'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-FILTER', plate: '粤ART808', phone: '', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false } ], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10' }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-RT-FILTER', plate: '粤ART808', sourceCount: 2, onlineSourceCount: 2, coverageStatus: 'online', primaryProtocol: 'GB32960', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-RT-FILTER')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-RT-FILTER&protocol=JT808'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=VIN-RT-FILTER&protocol=JT808'), undefined); }); test('shows canonical service status in realtime vehicles', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-DEGRADED', plate: '粤ART001', phone: '', oem: 'G7s', protocols: ['GB32960'], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '由实时车辆 API 统一判定', sourceCount: 2, onlineSourceCount: 1 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-RT-DEGRADED')).length).toBeGreaterThan(0); expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); }); test('frames realtime page as one vehicle service with source evidence', async () => { window.history.replaceState(null, '', '/#/realtime'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-SERVICE', plate: '粤ART002', phone: '', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false } ], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '2/2 来源在线', sourceCount: 2, onlineSourceCount: 2 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '实时监控' })).toBeInTheDocument(); expect(screen.getByText('高德地图待配置')).toBeInTheDocument(); expect(screen.getAllByText('定位有效').length).toBeGreaterThan(0); expect(screen.getByText('车辆核心数据')).toBeInTheDocument(); expect(screen.getByText('实时通道')).toBeInTheDocument(); expect(screen.getByText('GB32960 在线')).toBeInTheDocument(); expect(screen.getByText('JT808 在线')).toBeInTheDocument(); expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument(); expect(screen.getAllByText('2/2 通道在线').length).toBeGreaterThan(0); expect(screen.getByText('实时运营影响')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制影响报告/ })).toBeInTheDocument(); expect(screen.getByText('需要处置')).toBeInTheDocument(); expect(screen.getByText('车辆范围')).toBeInTheDocument(); expect(screen.getByText('在线影响')).toBeInTheDocument(); expect(screen.getByText('定位影响')).toBeInTheDocument(); expect(screen.getByText('服务影响')).toBeInTheDocument(); expect(screen.getByText('地图能力')).toBeInTheDocument(); expect(screen.getByText('当前页 1 辆,在线 1 辆,定位有效 1 辆,降级 0 辆,超时 1 辆。')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /复制影响报告/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时运营影响】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响等级:需要处置')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:当前页 1 / 总计 1,覆盖率 100%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线状态:1 在线 / 0 离线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位影响:1 辆有有效坐标')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务影响:0 辆降级 / 1 辆超时')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图能力:高德地图未配置,使用坐标预览')); }); test('refreshes realtime vehicle data from the monitoring workspace', async () => { window.history.replaceState(null, '', '/#/realtime'); let realtimeCalls = 0; vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { realtimeCalls += 1; return { ok: true, json: async () => ({ data: { items: [{ vin: `VIN-REFRESH-${realtimeCalls}`, plate: `粤A刷新${realtimeCalls}`, protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('粤A刷新1')).length).toBeGreaterThan(0); expect(screen.getByText('自动刷新 15秒')).toBeInTheDocument(); expect(screen.getByText((content) => content.includes('最后刷新'))).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '刷新实时数据' })); expect((await screen.findAllByText('粤A刷新2')).length).toBeGreaterThan(0); expect(realtimeCalls).toBeGreaterThanOrEqual(2); }); test('shows realtime freshness status for recently updated and stale vehicles', async () => { window.history.replaceState(null, '', '/#/map'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-FRESH-001', plate: '粤A新鲜1', protocols: ['GB32960'], sourceStatus: [{ protocol: 'GB32960', online: true, lastSeen: '2999-07-04 17:00:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2999-07-04 17:00:00', bindingStatus: 'bound' }, { vin: 'VIN-STALE-001', plate: '粤A超时1', protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: false, lastSeen: '2020-01-01 00:00:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 0, online: false, primaryProtocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 0, socPercent: 50, totalMileageKm: 1000, lastSeen: '2020-01-01 00:00:00', bindingStatus: 'bound' } ], total: 2, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument(); expect(screen.getAllByText('数据新鲜').length).toBeGreaterThan(0); expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0); expect(screen.getByText('地图客户决策')).toBeInTheDocument(); expect(screen.getByText('先判断车辆是否在线、是否有位置、是否有异常,再进入单车轨迹和数据交付。')).toBeInTheDocument(); expect(screen.getByText('地图页面向客户展示车辆服务状态,协议来源只作为定位、在线和异常追溯证据。')).toBeInTheDocument(); expect(screen.getByText('先看在线')).toBeInTheDocument(); expect(screen.getByText('再看定位')).toBeInTheDocument(); expect(screen.getByText('优先异常')).toBeInTheDocument(); expect(screen.getByText('选车复盘')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图客户决策 先看在线 只看在线' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图客户决策 再看定位 地图态势' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图客户决策 优先异常 告警事件' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制地图决策说明/ })).toBeInTheDocument(); expect(screen.getByText('实时盯车')).toBeInTheDocument(); expect(screen.getByText('异常优先')).toBeInTheDocument(); expect(screen.getAllByText('轨迹复盘').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '地图监控任务 实时盯车 只看在线' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图监控任务 异常优先 关注车辆' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图监控任务 轨迹复盘 轨迹回放' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图监控任务 数据交付 导出当前页' })).toBeInTheDocument(); expect(screen.getByText('客户地图监控包')).toBeInTheDocument(); expect(screen.getByText('面向客户看图时,先确认车辆范围、在线态势、定位态势和选中车辆,再进入轨迹、里程、历史数据或告警通知。')).toBeInTheDocument(); expect(screen.getByText('车辆范围')).toBeInTheDocument(); expect(screen.getByText('在线态势')).toBeInTheDocument(); expect(screen.getByText('定位态势')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制地图监控包/ })).toBeInTheDocument(); expect(screen.getByText('地图车辆作业台')).toBeInTheDocument(); expect(screen.getAllByText('粤A新鲜1').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '地图车辆作业 车辆服务 车辆档案' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图车辆作业 轨迹复盘 打开轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图车辆作业 里程核对 查看里程' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图车辆作业 告警处置 查看告警' })).toBeInTheDocument(); expect(screen.getAllByText('关注车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('选中车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('车辆列表')).toBeInTheDocument(); expect(screen.getAllByText('1 辆更新超时').length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: /复制地图监控包/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户地图监控包】')); }); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线态势:1 在线 / 1 离线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-STALE-001&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: /复制地图决策说明/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【地图客户决策说明】')); }); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位判断:2 辆有有效坐标 / 0 辆无坐标')); }); test('exports current realtime vehicles as CSV', async () => { window.history.replaceState(null, '', '/#/realtime?protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:realtime-export'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-EXPORT', plate: '粤A实时导', phone: '13300000001', oem: 'G7s', protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 76, totalMileageKm: 1200.5, lastSeen: '2026-07-03 20:12:10', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('button', { name: '导出实时当前页 CSV' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '导出实时当前页 CSV' })); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:realtime-export'); }); test('copies realtime operations summary from realtime page', async () => { window.history.replaceState(null, '', '/#/realtime?protocol=JT808&online=online'); Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', { configurable: true, value: { amapWebJsKey: 'amap-key' } }); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-RT-SUMMARY-001', plate: '粤A摘要1', phone: '13300000001', oem: 'G7s', protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 76, totalMileageKm: 1200.5, lastSeen: '2026-07-03 20:12:10', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } }, { vin: 'VIN-RT-SUMMARY-002', plate: '粤A摘要2', phone: '13300000002', oem: '现代', protocols: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: false, lastSeen: '2026-07-03 19:58:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], sourceCount: 2, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'GB32960', longitude: 113.4, latitude: 23.2, speedKmh: 18, socPercent: 69, totalMileageKm: 2200.5, lastSeen: '2026-07-03 20:12:08', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'JT808 离线,GB32960 仍可支撑车辆服务', sourceCount: 2, onlineSourceCount: 1 } }, { vin: 'VIN-RT-SUMMARY-003', plate: '粤A摘要3', phone: '13300000003', oem: '宇通', protocols: ['YUTONG_MQTT'], sourceStatus: [{ protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 18:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 0, online: false, bindingStatus: 'bound', primaryProtocol: 'YUTONG_MQTT', longitude: 0, latitude: 0, speedKmh: 0, socPercent: 60, totalMileageKm: 3200.5, lastSeen: '2026-07-03 18:12:10', serviceStatus: { status: 'offline', severity: 'error', title: '车辆离线', detail: 'YUTONG_MQTT 超过在线窗口', sourceCount: 1, onlineSourceCount: 0 } } ], total: 3, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-RT-SUMMARY-001')).length).toBeGreaterThan(0); expect(screen.getByText('实时作业入口')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '实时筛选 在线车辆 2' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '实时筛选 离线车辆 1' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '实时筛选 需要关注 2' })).toBeInTheDocument(); expect(screen.getByText('建议处置')).toBeInTheDocument(); expect(screen.getByText('查看需关注车辆 2')).toBeInTheDocument(); expect(screen.getByText('车辆覆盖概览')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '筛选数据通道 GB32960' })).toHaveTextContent('GB32960'); expect(screen.getByRole('button', { name: '筛选数据通道 JT808' })).toHaveTextContent('2 辆'); expect(screen.getByRole('button', { name: '筛选数据通道 YUTONG_MQTT' })).toHaveTextContent('超时 1'); expect(screen.getByText('实时覆盖判读')).toBeInTheDocument(); expect(screen.getByText('在线率')).toBeInTheDocument(); expect(screen.getAllByText('66.7%').length).toBeGreaterThanOrEqual(3); expect(screen.getByText('定位有效率')).toBeInTheDocument(); expect(screen.getByText('降级率')).toBeInTheDocument(); expect(screen.getByText('分页覆盖率')).toBeInTheDocument(); expect(screen.getByText('100%')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制实时摘要' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时监控摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据通道:JT808;在线:在线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆总数:3,当前页:3')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:2,定位有效:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('需要关注:2,数据通道:3')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图配置:已配置')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('重点车辆:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / YUTONG_MQTT / 车辆离线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时页面:http://localhost:3000/#/realtime?protocol=JT808&online=online')); fireEvent.click(screen.getByRole('button', { name: '复制实时异常处置清单' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时异常处置清单】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据通道:JT808;在线:在线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常车辆:3 / 当前页 3 / 总计 3')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / VIN-RT-SUMMARY-003 / JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:车辆离线;在线:离线;问题:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('坐标无效')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:核对YUTONG_MQTT定位字段解析和平台转发')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: '复制实时运营交接包' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时运营交接包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('页面车辆:3 / 总计 3;版本:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线:2;离线:1;定位有效:2;需要关注:2;超时:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆覆盖:GB32960:在线 1/1,实时 1/1;JT808:在线 1/2,实时 2/2;YUTONG_MQTT:在线 0/1,实时 1/1')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时入口:http://localhost:3000/#/realtime?protocol=JT808&online=online')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程统计:http://localhost:3000/#/mileage?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/api/history/raw-frames?protocol=JT808&vin=VIN-RT-SUMMARY-003&limit=20&includeFields=true')); fireEvent.click(screen.getByRole('button', { name: '实时筛选 需要关注 2' })); expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online'); fireEvent.click(screen.getByRole('button', { name: '筛选数据通道 GB32960' })); expect(window.location.hash).toBe('#/realtime?protocol=GB32960&serviceStatus=degraded&online=online'); }); test('shows production AMap integration status on realtime page', async () => { window.history.replaceState(null, '', '/#/realtime'); Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', { configurable: true, value: {} }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapApiConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false, amapSecurityServiceHost: '/_AMapService', platformRelease: 'platform-20260704153005' } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-AMAP-READY-001', plate: '粤A地图就绪', phone: '13300000001', oem: 'G7s', protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 76, totalMileageKm: 1200.5, lastSeen: '2026-07-03 20:12:10', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } }, { vin: 'VIN-AMAP-READY-002', plate: '粤A地图无坐标', phone: '13300000002', oem: 'G7s', protocols: ['GB32960'], sourceStatus: [{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'GB32960', longitude: 0, latitude: 0, speedKmh: 12, socPercent: 68, totalMileageKm: 2200.5, lastSeen: '2026-07-03 20:12:08', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } } ], total: 2, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('地图展示状态')).toBeInTheDocument(); expect(screen.getByText('Web JS Key')).toBeInTheDocument(); expect(screen.getByText('服务端 API Key')).toBeInTheDocument(); expect(screen.getAllByText('已配置').length).toBeGreaterThan(0); expect(screen.getByText('安全代理')).toBeInTheDocument(); expect(screen.getByText('/_AMapService')).toBeInTheDocument(); expect(screen.getByText('当前版本')).toBeInTheDocument(); expect(screen.getByText('platform-20260704153005')).toBeInTheDocument(); expect(screen.getByText('定位覆盖')).toBeInTheDocument(); expect(screen.getByText('1 / 2')).toBeInTheDocument(); }); test('selects realtime map vehicle from fallback map point', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [ { vin: 'VIN-MAP-SELECT-1', plate: '粤A选中1', phone: '', oem: 'G7s', protocols: ['GB32960'], sourceStatus: [{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } }, { vin: 'VIN-MAP-SELECT-2', plate: '粤A选中2', phone: '', oem: 'G7s', protocols: ['JT808'], sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', longitude: 113.4, latitude: 23.2, speedKmh: 42, socPercent: 66, totalMileageKm: 2200, lastSeen: '2026-07-03 20:11:10', bindingStatus: 'bound', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '1/1 来源在线', sourceCount: 1, onlineSourceCount: 1 } } ], total: 2, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); const selectedPanelTitle = await screen.findByText('当前选中车辆'); const selectedPanel = selectedPanelTitle.closest('.vp-selected-vehicle-panel'); expect(selectedPanel).not.toBeNull(); expect(within(selectedPanel as HTMLElement).getByText('粤A选中1')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '选择地图车辆 粤A选中2' })); await waitFor(() => { expect(within(selectedPanel as HTMLElement).getByText('粤A选中2')).toBeInTheDocument(); }); expect(within(selectedPanel as HTMLElement).getByText('42 km/h')).toBeInTheDocument(); const locateButtons = screen.getAllByRole('button', { name: '地图定位' }); fireEvent.click(locateButtons[0]); expect(within(selectedPanel as HTMLElement).getByText('粤A选中1')).toBeInTheDocument(); fireEvent.click(locateButtons[1]); expect(within(selectedPanel as HTMLElement).getByText('粤A选中2')).toBeInTheDocument(); }); test('opens vehicle service from realtime map service queue', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MAP-001', plate: '粤AMAP01', phone: '', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, { protocol: 'JT808', online: false, lastSeen: '2026-07-03 19:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false } ], sourceCount: 2, onlineSourceCount: 1, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'JT808 离线,GB32960 仍可支撑车辆服务', sourceCount: 2, onlineSourceCount: 1 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-MAP-001', plate: '粤AMAP01', sourceCount: 2, onlineSourceCount: 1, coverageStatus: 'degraded', primaryProtocol: 'GB32960', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('地图车辆队列')).toBeInTheDocument(); expect(screen.getAllByText('粤AMAP01').length).toBeGreaterThan(0); expect(screen.getByText('JT808 离线,GB32960 仍可支撑车辆服务')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '车辆详情' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-MAP-001&protocol=GB32960'); }); }); test('opens amap coordinate and trajectory playback from realtime map service queue', async () => { window.history.replaceState(null, '', '/#/realtime'); const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-MAP-LINK', plate: '粤AMAP02', phone: '', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.24567, latitude: 23.12345, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'healthy', severity: 'ok', title: '服务正常', detail: '2/2 来源在线', sourceCount: 2, onlineSourceCount: 2 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('地图车辆队列')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '高德坐标' })); expect(openSpy).toHaveBeenCalledWith( expect.stringContaining('https://uri.amap.com/marker?position=113.24567,23.12345'), '_blank', 'noopener,noreferrer' ); fireEvent.click(screen.getAllByRole('button', { name: '轨迹回放' })[0]); expect(window.location.hash).toBe('#/history?keyword=VIN-MAP-LINK&protocol=GB32960'); }); test('surfaces multi-source realtime consistency issues with quality drilldown', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-CONSISTENCY', plate: '粤A一致监控', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:05', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:01:40', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } ], sourceCount: 3, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'YUTONG_MQTT 离线,GB32960/JT808 仍可支撑车辆服务', sourceCount: 3, onlineSourceCount: 2 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('需关注车辆')).toBeInTheDocument(); expect(screen.getAllByText('粤A一致监控').length).toBeGreaterThan(0); expect(screen.getByText('处置说明:YUTONG_MQTT 离线,GB32960/JT808 仍可支撑车辆服务')).toBeInTheDocument(); expect(screen.getByText('问题:YUTONG_MQTT 离线')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '查看告警' })); await waitFor(() => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN-RT-CONSISTENCY&protocol=GB32960'); }); }); test('opens quality issues from realtime vehicle row with source evidence', async () => { window.history.replaceState(null, '', '/#/realtime?protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-QUALITY', plate: '粤A质量1', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], sourceStatus: [ { protocol: 'JT808', online: false, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: false } ], sourceCount: 1, onlineSourceCount: 0, online: false, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 0, socPercent: 70, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'offline', severity: 'error', title: '车辆离线', detail: 'JT808 离线', sourceCount: 1, onlineSourceCount: 0 } }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-QUALITY', plate: '粤A质量1', phone: '13307795425', sourceEndpoint: '115.29.187.205:808', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'error', title: 'JT808 离线', detail: '实时来源不可用', firstSeen: '2026-07-03 20:12:10', lastSeen: '2026-07-03 20:12:10' }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, activeConnections: 1, capacityFindings: [], redisOnlineKeys: 1, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-RT-QUALITY')).length).toBeGreaterThan(0); fireEvent.click(screen.getAllByRole('button', { name: '告警事件' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN-RT-QUALITY&protocol=JT808'); }); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); }); test('loads realtime vehicles from shareable source filter hash', async () => { window.history.replaceState(null, '', '/#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-RT-FILTERED', plate: '粤ART002', phone: '', oem: 'G7s', protocols: ['JT808'], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 119925, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN-RT-FILTERED')).length).toBeGreaterThan(0); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'), undefined); }); test('opens realtime status from quality issue row with source evidence', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return { ok: true, json: async () => ({ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'VIN_MISSING', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: '', plate: '粤AG18312', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 数据缺少 VIN 映射' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('13307795425')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '实时状态' })); await waitFor(() => { expect(window.location.hash).toBe('#/realtime?keyword=%E7%B2%A4AG18312&protocol=JT808'); }); expect(await screen.findByRole('heading', { name: '实时监控' })).toBeInTheDocument(); }); test('shows and clears current realtime service filters', async () => { window.history.replaceState(null, '', '/#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [], total: 1, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前实时筛选')).toBeInTheDocument(); expect(screen.getByText('关键词:粤ART002')).toBeInTheDocument(); expect(screen.getByText('数据通道:JT808')).toBeInTheDocument(); expect(screen.getByText('在线:在线')).toBeInTheDocument(); expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0'), undefined); }); expect(window.location.hash).toBe('#/realtime'); }); test('updates realtime hash when source filters are submitted', async () => { window.history.replaceState(null, '', '/#/realtime'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/realtime/vehicles')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await screen.findByRole('heading', { name: '实时监控' }); fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号 / OEM'), { target: { value: '粤ART002' } }); fireEvent.click(screen.getByText('全部数据通道')); fireEvent.click(await screen.findByText('JT808')); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => { expect(window.location.hash).toBe('#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808'), undefined); }); test('shows vehicle service status in vehicle list', async () => { window.history.replaceState(null, '', '/#/vehicles'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-DEGRADED-001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '由车辆服务 API 统一判定', sourceCount: 2, onlineSourceCount: 1 } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-DEGRADED-001')).toBeInTheDocument(); expect(screen.getByText('车辆服务状态')).toBeInTheDocument(); expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); }); test('shows source consistency diagnosis in vehicle service list', async () => { window.history.replaceState(null, '', '/#/vehicles'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 0, multiSourceVehicles: 1, noDataVehicles: 0, unboundVehicles: 0, missingSources: [] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-CONSISTENCY-LIST', plate: '粤A一致', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceCount: 2, onlineSourceCount: 2, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', sourceConsistency: { sourceCount: 2, onlineSourceCount: 2, locatedSourceCount: 0, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'coverage', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/2 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-CONSISTENCY-LIST')).toBeInTheDocument(); expect(screen.getByText('来源一致性')).toBeInTheDocument(); expect(screen.getByText('来源不完整')).toBeInTheDocument(); }); test('filters vehicle list from source consistency diagnosis', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 0, multiSourceVehicles: 1, noDataVehicles: 0, unboundVehicles: 0, missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-CONSISTENCY-ACTION', plate: '粤A动作', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: ['YUTONG_MQTT'], sourceCount: 2, onlineSourceCount: 2, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', sourceConsistency: { sourceCount: 2, onlineSourceCount: 2, locatedSourceCount: 0, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'coverage', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/2 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-CONSISTENCY-ACTION')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '来源不完整' })); await waitFor(() => { expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=YUTONG_MQTT'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); test('filters vehicle list from single-source consistency diagnosis', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage/summary')) { return { ok: true, json: async () => ({ data: { totalVehicles: 1, onlineVehicles: 1, singleSourceVehicles: 1, multiSourceVehicles: 0, noDataVehicles: 0, unboundVehicles: 0, missingSources: [{ protocol: 'JT808', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-SINGLE-SOURCE', plate: '粤A单源', phone: '', oem: '', protocols: ['GB32960'], missingProtocols: ['JT808', 'YUTONG_MQTT'], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-04 07:15:28', bindingStatus: 'bound', sourceConsistency: { sourceCount: 1, onlineSourceCount: 1, locatedSourceCount: 0, missingProtocols: ['JT808', 'YUTONG_MQTT'], mileageDeltaKm: 0, sourceTimeDeltaSeconds: 0, scope: 'coverage', status: 'single_source', severity: 'warning', title: '单一来源', detail: '当前车辆只有一个数据来源,无法做跨来源一致性校验。' } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-SINGLE-SOURCE')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '单一来源' })); await waitFor(() => { expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=JT808'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=JT808'), undefined); }); test('opens vehicle service from source-filtered vehicle list with source evidence', async () => { window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-LIST-JT808', plate: '粤A列表1', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/vehicle-service/overview')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-LIST-JT808', plate: '粤A列表1', sourceCount: 1, onlineSourceCount: 1, coverageStatus: 'online', primaryProtocol: 'JT808', lastSeen: '2026-07-03 20:12:10', historyCount: 0, rawCount: 0, mileageCount: 0, qualityIssueCount: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-LIST-JT808')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-LIST-JT808&protocol=JT808'); }); }); test('opens realtime and trajectory from vehicle center row', async () => { vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-LIST-WORKFLOW', plate: '粤A列表流', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], missingProtocols: [], sourceStatus: [ { protocol: 'GB32960', online: false, lastSeen: '2026-07-03 18:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], sourceCount: 2, onlineSourceCount: 1, online: true, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); window.history.replaceState(null, '', '/#/vehicles'); render(); expect(await screen.findByText('VIN-LIST-WORKFLOW')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '实时' })); await waitFor(() => { expect(window.location.hash).toBe('#/realtime?keyword=VIN-LIST-WORKFLOW&protocol=JT808'); }); cleanup(); window.history.replaceState(null, '', '/#/vehicles'); render(); expect(await screen.findByText('VIN-LIST-WORKFLOW')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '轨迹' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN-LIST-WORKFLOW&protocol=JT808'); }); }); test('opens quality issues from source-filtered vehicle list with source evidence', async () => { window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-LIST-QUALITY', plate: '粤A列表质', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], sourceCount: 1, onlineSourceCount: 0, online: false, lastSeen: '2026-07-03 20:12:10', bindingStatus: 'bound', serviceStatus: { status: 'offline', severity: 'error', title: '车辆离线', detail: 'JT808 来源离线', sourceCount: 1, onlineSourceCount: 0 } }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary')) { return { ok: true, json: async () => ({ data: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [{ name: 'JT808', count: 1 }], issueTypes: [{ name: 'LINK_GAP', count: 1 }] }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } if (path.includes('/api/quality/issues') || path.includes('/api/alert-events?')) { return { ok: true, json: async () => ({ data: { items: [{ vin: 'VIN-LIST-QUALITY', plate: '粤A列表质', phone: '13307795425', sourceEndpoint: '115.231.168.135:43625', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 来源离线' }], total: 1, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('VIN-LIST-QUALITY')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '告警' })); await waitFor(() => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN-LIST-QUALITY&protocol=JT808'); }); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); }); test('filters vehicle list by service status', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/coverage')) { return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); await screen.findByRole('heading', { name: '车辆服务' }); fireEvent.click(screen.getByTestId('service-status-filter')); fireEvent.click(await screen.findByText('来源不完整')); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); }); }); test('switches vehicle detail to a source from the source matrix', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceCount: 3, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false } ], realtime: [ { vin: 'VIN001', plate: '粤AG18312', protocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, { vin: 'VIN001', plate: '粤AG18312', protocol: 'JT808', longitude: 113.21, latitude: 23.11, speedKmh: 28, socPercent: 0, totalMileageKm: 99.5, lastSeen: '2026-07-03 20:12:08' } ], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('VIN001')).length).toBeGreaterThan(0); expect(screen.getAllByText('GB32960 在线').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('JT808 在线').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('YUTONG_MQTT 未接入').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('多源归并矩阵')).toBeInTheDocument(); expect(screen.getByText('主实时来源')).toBeInTheDocument(); expect(screen.getAllByText('定位证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计口径').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('无实时字段')).toBeInTheDocument(); expect(screen.getByText('无原始记录')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '复制归并矩阵' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【单车多源归并矩阵】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. GB32960 / 主来源')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('贡献:主实时来源、在线心跳、实时字段、定位证据、轨迹回放、原始记录、统计口径')); expect(screen.getByText('车辆服务角色')).toBeInTheDocument(); expect(screen.getAllByText('主来源').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('暂无来源').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('最新位置')).toBeInTheDocument(); expect(screen.getAllByText('113.2, 23.1').length).toBeGreaterThan(0); expect(screen.getAllByText(/100\s*km/).length).toBeGreaterThan(0); expect(screen.getAllByText(/30\s*km\/h/).length).toBeGreaterThan(0); expect(screen.getAllByText('YUTONG_MQTT').length).toBeGreaterThan(0); expect(screen.getAllByText('无上报').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '仅看 JT808' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN001&protocol=JT808'); }); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined); }); test('shows unified service overview on vehicle detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, serviceOverview: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], primaryProtocol: 'GB32960', coverageStatus: 'partial', sourceCount: 3, onlineSourceCount: 2, lastSeen: '2026-07-03 20:12:10', realtimeCount: 3, historyCount: 12, rawCount: 34, mileageCount: 7, qualityIssueCount: 1 }, serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线', sourceCount: 3, onlineSourceCount: 2 }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [], realtime: [], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 7, limit: 20, offset: 0 }, quality: { items: [], total: 1, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务概览')).toBeInTheDocument(); expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制档案/ })).toBeInTheDocument(); expect(screen.getByText('今日单车服务任务板')).toBeInTheDocument(); expect(screen.getByText('看实时位置')).toBeInTheDocument(); expect(screen.getByText('回放轨迹证据')).toBeInTheDocument(); expect(screen.getByText('导出原始证据')).toBeInTheDocument(); expect(screen.getByText('复核里程统计')).toBeInTheDocument(); expect(screen.getByText('闭环告警事件')).toBeInTheDocument(); expect(screen.getByText('单车运营工作台')).toBeInTheDocument(); expect(screen.getByText('单车服务处理清单')).toBeInTheDocument(); expect(screen.getAllByText('档案完整度').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('实时')).toBeInTheDocument(); expect(screen.getAllByText('轨迹').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('原始记录').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('里程').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('实时定位')).toBeInTheDocument(); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('原始记录').length).toBeGreaterThan(0); expect(screen.getByText('告警处置')).toBeInTheDocument(); expect(screen.getByText('里程复核')).toBeInTheDocument(); expect(screen.getByText('确认实时状态')).toBeInTheDocument(); expect(screen.getByText('回放轨迹断点')).toBeInTheDocument(); expect(screen.getByText('核对原始记录')).toBeInTheDocument(); expect(screen.getByText('处理告警闭环')).toBeInTheDocument(); expect(screen.getByText('复核统计口径')).toBeInTheDocument(); expect(screen.getByText('车辆在线状态、最新时间、有效坐标均可解释。')).toBeInTheDocument(); expect(screen.getByText('2026-07-03 20:12:10')).toBeInTheDocument(); expect(screen.getAllByText('12 条历史').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('34 帧').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('1 项').length).toBeGreaterThan(0); expect(screen.getAllByText('7 条统计').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('2/3')).toBeInTheDocument(); expect(screen.getByText('轨迹 12 / 原始记录 34 / 里程 7')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 看实时位置 实时监控' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 回放轨迹证据 轨迹回放' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 导出原始证据 数据导出' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 复核里程统计 里程统计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 闭环告警事件 告警事件' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '单车运营工作台 原始记录 查看原始记录' })); expect(window.location.hash).toBe('#/history-query?keyword=VIN001&tab=raw'); }); test('opens quality governance from vehicle detail overview', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, serviceOverview: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], primaryProtocol: 'GB32960', coverageStatus: 'partial', sourceCount: 2, onlineSourceCount: 1, lastSeen: '2026-07-03 20:12:10', realtimeCount: 2, historyCount: 12, rawCount: 34, mileageCount: 7, qualityIssueCount: 3 }, sources: ['GB32960', 'JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 7, limit: 20, offset: 0 }, quality: { items: [], total: 3, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary') ? { issueVehicleCount: 1, issueRecordCount: 3, errorCount: 0, warningCount: 3, protocols: [], issueTypes: [] } : { items: [], total: 3, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务概览')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '查看质量问题 3 项' })); await waitFor(() => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN001'); }); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); expect(screen.getByText('告警事件闭环')).toBeInTheDocument(); expect(screen.getByText('事件触发')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?keyword=VIN001&limit=20&offset=0'), undefined); }); test('quality health storage card stays pending before ops health loads', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { return new Promise(() => undefined); } return { ok: true, json: async () => ({ data: path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary') ? { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); expect(screen.getByText('存储读取')).toBeInTheDocument(); expect(screen.getByText('检测中')).toBeInTheDocument(); expect(screen.queryByText('异常')).not.toBeInTheDocument(); }); test('quality health shows active connection capacity metric', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, activeConnections: 120000, redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704153710' } } : path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary') ? { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); expect(await screen.findByText('活跃连接')).toBeInTheDocument(); expect(screen.getByText('120,000')).toBeInTheDocument(); expect(screen.getByText('运行版本')).toBeInTheDocument(); expect(screen.getByText('platform-20260704153710')).toBeInTheDocument(); }); test('quality health shows structured capacity findings', async () => { window.history.replaceState(null, '', '/#/alert-events'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 42, activeConnections: 120000, capacityFindings: ['kafka lag 42'], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : path.includes('/api/quality/summary') || path.includes('/api/alert-events/summary') ? { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] } : { items: [], total: 0, limit: 20, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByRole('heading', { name: '告警事件' })).toBeInTheDocument(); expect(await screen.findByText('容量检查发现')).toBeInTheDocument(); expect(screen.getByText('kafka lag 42')).toBeInTheDocument(); }); test('shows vehicle service evidence chain before source details', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, serviceOverview: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], primaryProtocol: 'GB32960', coverageStatus: 'partial', sourceCount: 3, onlineSourceCount: 2, lastSeen: '2026-07-03 20:12:10', realtimeCount: 3, historyCount: 12, rawCount: 34, mileageCount: 7, qualityIssueCount: 1 }, serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线', sourceCount: 3, onlineSourceCount: 2 }, sourceConsistency: { sourceCount: 3, onlineSourceCount: 2, locatedSourceCount: 2, mileageDeltaKm: 1.4, sourceTimeDeltaSeconds: 125, scope: 'vehicle', status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'MQTT 离线,32960 与 808 仍可支撑车辆服务' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:10:05', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } ], realtime: [], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 7, limit: 20, offset: 0 }, quality: { items: [], total: 1, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务证据链')).toBeInTheDocument(); expect(screen.getAllByText('2/3 来源在线').length).toBeGreaterThan(0); expect(screen.getAllByText('2 个来源有位置').length).toBeGreaterThan(0); expect(screen.getAllByText('1.4 km').length).toBeGreaterThan(0); expect(screen.getAllByText('125 秒').length).toBeGreaterThan(0); expect(screen.getAllByText('MQTT 离线,32960 与 808 仍可支撑车辆服务').length).toBeGreaterThan(0); }); test('shows unified latest location posture on vehicle detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-MAP-001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-MAP-001', lookupKey: 'VIN-MAP-001', lookupResolved: true, serviceOverview: { vin: 'VIN-MAP-001', plate: '粤A地图1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], primaryProtocol: 'GB32960', coverageStatus: 'partial', sourceCount: 3, onlineSourceCount: 2, lastSeen: '2026-07-03 20:12:10', realtimeCount: 3, historyCount: 12, rawCount: 34, mileageCount: 7, qualityIssueCount: 0 }, serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线', sourceCount: 3, onlineSourceCount: 2 }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:10:05', hasRealtime: false, hasHistory: false, hasRaw: true, hasMileage: false } ], realtime: [ { vin: 'VIN-MAP-001', plate: '粤A地图1', protocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 36, socPercent: 76, totalMileageKm: 1200.5, lastSeen: '2026-07-03 20:12:10' }, { vin: 'VIN-MAP-001', plate: '粤A地图1', protocol: 'JT808', longitude: 113.21, latitude: 23.11, speedKmh: 35, socPercent: 0, totalMileageKm: 1200.2, lastSeen: '2026-07-03 20:12:08' } ], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 7, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆位置态势')).toBeInTheDocument(); expect(screen.getByText('2 个来源有最新位置')).toBeInTheDocument(); expect(screen.getByText('GB32960 / 36 km/h')).toBeInTheDocument(); expect(screen.getByText('JT808 / 35 km/h')).toBeInTheDocument(); expect(screen.getByText('高德地图未配置,显示车辆服务坐标预览')).toBeInTheDocument(); }); test('copies shareable vehicle service detail link', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001&protocol=JT808'); const writeText = vi.fn(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, sources: ['GB32960', 'JT808'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: /复制服务链接/ })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/#/detail?keyword=VIN001&protocol=JT808')); }); test('copies vehicle service diagnostic summary', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); const writeText = vi.fn<(value: string) => Promise>(() => Promise.resolve()); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { const serviceStatus = { status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'JT808 在线,GB32960 离线', sourceCount: 2, onlineSourceCount: 1 }; const sourceConsistency = { sourceCount: 2, onlineSourceCount: 1, locatedSourceCount: 2, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 12.6, sourceTimeDeltaSeconds: 90, scope: 'all_sources', status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'JT808 在线,GB32960 离线,需要补齐 MQTT' }; return { ok: true, json: async () => ({ data: { vin: 'VIN-SUMMARY-001', lookupKey: 'VIN-SUMMARY-001', lookupResolved: true, identity: { vin: 'VIN-SUMMARY-001', plate: '粤A诊断1', phone: '13307795425', oem: 'G7s', online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN-SUMMARY-001', plate: '粤A诊断1', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], sourceCount: 2, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', lastSeen: '2026-07-03 20:12:10' }, serviceOverview: { vin: 'VIN-SUMMARY-001', plate: '粤A诊断1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], primaryProtocol: 'JT808', coverageStatus: 'partial', sourceCount: 2, onlineSourceCount: 1, lastSeen: '2026-07-03 20:12:10', realtimeCount: 2, historyCount: 12, rawCount: 34, mileageCount: 3, qualityIssueCount: 1 }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'GB32960', online: false, lastSeen: '2026-07-03 19:55:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [ { vin: 'VIN-SUMMARY-001', plate: '粤A诊断1', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 32, totalMileageKm: 1200.5, lastSeen: '2026-07-03 20:12:10' } ], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 3, limit: 20, offset: 0 }, quality: { items: [ { vin: 'VIN-SUMMARY-001', plate: '粤A诊断1', protocol: 'JT808', issueType: 'LINK_GAP', severity: 'warning', lastSeen: '2026-07-03 20:12:10', detail: 'JT808 间断' } ], total: 1, limit: 20, offset: 0 }, serviceStatus, sourceConsistency }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000, platformRelease: 'platform-20260704155844' } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆运营总览')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制归一摘要/ })).toBeInTheDocument(); expect(screen.getByText('车辆身份')).toBeInTheDocument(); expect(screen.getByText('实时主来源')).toBeInTheDocument(); expect(screen.getByText('业务可用性')).toBeInTheDocument(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument(); expect(screen.getByText('单车客户服务交付包')).toBeInTheDocument(); expect(screen.getByText('面向客户交付时,以车辆为主对象提供实时监控、轨迹回放、历史数据查询、告警通知和统计查询,协议来源只作为可追溯证据。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户服务交付 实时监控 打开监控' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户服务交付 轨迹回放 回放轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户服务交付 历史数据查询 查询历史' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户服务交付 告警通知 告警闭环' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户服务交付 统计查询 统计复核' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /复制归一摘要/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【统一车辆服务摘要】'))).toBe(true); }); const unifiedCopied = String(writeText.mock.lastCall?.[0] ?? ''); expect(unifiedCopied).toContain('【统一车辆服务摘要】'); expect(unifiedCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(unifiedCopied).toContain('当前范围:单一来源:JT808'); expect(unifiedCopied).toContain('车辆身份:VIN-SUMMARY-001'); expect(unifiedCopied).toContain('服务状态:来源不完整'); expect(unifiedCopied).toContain('实时主来源:JT808'); expect(unifiedCopied).toContain('来源在线:1/2'); expect(unifiedCopied).toContain('业务可用性:在线可用'); expect(unifiedCopied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(unifiedCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制档案/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【车辆服务档案】'))).toBe(true); }); const dossierCopied = String(writeText.mock.lastCall?.[0] ?? ''); expect(dossierCopied).toContain('【车辆服务档案】'); expect(dossierCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(dossierCopied).toContain('当前范围:单一来源:JT808'); expect(dossierCopied).toContain('主来源:JT808'); expect(dossierCopied).toContain('来源在线:1/2'); expect(dossierCopied).toContain('资产概览:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(dossierCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制交付包/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【单车客户服务交付包】'))).toBe(true); }); const customerPackageCopied = String(writeText.mock.lastCall?.[0] ?? ''); expect(customerPackageCopied).toContain('【单车客户服务交付包】'); expect(customerPackageCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(customerPackageCopied).toContain('平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询'); expect(customerPackageCopied).toContain('实时监控:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(customerPackageCopied).toContain('轨迹回放:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(customerPackageCopied).toContain('历史数据查询:http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); expect(customerPackageCopied).toContain('告警通知:http://localhost:3000/#/alert-events?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(customerPackageCopied).toContain('统计查询:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制诊断摘要/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.any(String)); }); const copied = String(writeText.mock.lastCall?.[0] ?? ''); expect(copied).toContain('【车辆服务诊断摘要】'); expect(copied).toContain('运行版本:platform-20260704155844'); expect(copied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(copied).toContain('当前范围:单一来源:JT808'); expect(copied).toContain('服务状态:来源不完整'); expect(copied).toContain('来源证据:1/2 来源在线'); expect(copied).toContain('定位证据:2 个来源有位置'); expect(copied).toContain('里程差异:12.6 km'); expect(copied).toContain('时间差异:90 秒'); expect(copied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(copied).toContain('一致性:来源不完整'); expect(copied).toContain('下一步:'); expect(copied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('实时:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('轨迹:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('原始记录:http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); expect(copied).toContain('里程:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制运营摘要/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【车辆服务运营摘要】'))).toBe(true); }); const operationsCopied = String(writeText.mock.lastCall?.[0] ?? ''); expect(operationsCopied).toContain('【车辆服务运营摘要】'); expect(operationsCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(operationsCopied).toContain('当前范围:单一来源:JT808'); expect(operationsCopied).toContain('服务状态:来源不完整'); expect(operationsCopied).toContain('在线来源:1/2'); expect(operationsCopied).toContain('定位来源:2'); expect(operationsCopied).toContain('质量问题:1 项'); expect(operationsCopied).toContain('下一步清单:'); expect(operationsCopied).toContain('补齐 YUTONG_MQTT 来源'); expect(operationsCopied).toContain('排查 GB32960 离线'); expect(operationsCopied).toContain('处理质量问题 1 项'); expect(operationsCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('实时监控:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('轨迹回放:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('原始记录:http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); expect(operationsCopied).toContain('里程统计:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: '复制处理清单' })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【单车服务处理清单】'))).toBe(true); }); const runbookCopied = String(writeText.mock.lastCall?.[0] ?? ''); expect(runbookCopied).toContain('【单车服务处理清单】'); expect(runbookCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); expect(runbookCopied).toContain('当前范围:单一来源:JT808'); expect(runbookCopied).toContain('1. 确认实时状态'); expect(runbookCopied).toContain('2. 回放轨迹断点'); expect(runbookCopied).toContain('3. 核对原始记录'); expect(runbookCopied).toContain('4. 处理告警闭环'); expect(runbookCopied).toContain('5. 复核统计口径'); expect(runbookCopied).toContain('验收:车辆在线状态、最新时间、有效坐标均可解释。'); expect(runbookCopied).toContain('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(runbookCopied).toContain('里程统计:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); }); test('exports vehicle detail service dossier as csv', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-REPORT-001&protocol=GB32960'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:vehicle-report'); const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); const appendChild = vi.spyOn(document.body, 'appendChild'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-REPORT-001', lookupKey: 'VIN-REPORT-001', lookupResolved: true, serviceOverview: { vin: 'VIN-REPORT-001', plate: '粤A档案导出', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], primaryProtocol: 'GB32960', coverageStatus: 'partial', sourceCount: 3, onlineSourceCount: 2, lastSeen: '2026-07-03 20:12:10', realtimeCount: 3, historyCount: 12, rawCount: 34, mileageCount: 7, qualityIssueCount: 1 }, serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线', sourceCount: 3, onlineSourceCount: 2 }, sourceConsistency: { sourceCount: 3, onlineSourceCount: 2, locatedSourceCount: 2, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 1.4, sourceTimeDeltaSeconds: 125, scope: 'vehicle', status: 'degraded', severity: 'warning', title: '来源不完整', detail: 'MQTT 离线,32960 与 808 仍可支撑车辆服务' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:10:05', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } ], realtime: [], history: { items: [], total: 12, limit: 20, offset: 0 }, raw: { items: [], total: 34, limit: 10, offset: 0 }, mileage: { items: [], total: 7, limit: 20, offset: 0 }, quality: { items: [], total: 1, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /导出档案 CSV/ })); expect(createObjectURL).toHaveBeenCalled(); expect(click).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:vehicle-report'); const link = appendChild.mock.calls.find(([node]) => node instanceof HTMLAnchorElement)?.[0] as HTMLAnchorElement | undefined; expect(link?.download).toBe('vehicle-service-VIN-REPORT-001-GB32960.csv'); }); test('opens history with the currently selected vehicle detail source', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '仅看 JT808' })); fireEvent.click(screen.getAllByRole('button', { name: '轨迹回放' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN001&protocol=JT808'); }); }); test('opens raw history with the currently selected vehicle detail source', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '仅看 JT808' })); fireEvent.click(screen.getAllByRole('button', { name: '数据导出' })[0]); await waitFor(() => { expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw'); }); }); test('opens realtime with the currently selected vehicle detail source', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '仅看 JT808' })); fireEvent.click(screen.getByRole('button', { name: '查看实时' })); await waitFor(() => { expect(window.location.hash).toBe('#/realtime?keyword=VIN001&protocol=JT808'); }); }); test('opens source-specific history directly from vehicle detail source card', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 轨迹' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN001&protocol=JT808'); }); }); test('opens source-specific raw history directly from vehicle detail source evidence', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 2, online: true, primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 原始记录' })); await waitFor(() => { expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw'); }); }); test('opens vehicle service from an empty history query with current source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=VIN404&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicles/resolve')) { return { ok: true, json: async () => ({ data: { lookupKey: 'VIN404', resolved: true, vin: 'VIN404', plate: '', phone: '', oem: '', protocols: ['JT808'], online: false, lastSeen: '' }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); fireEvent.click(await screen.findByRole('button', { name: '当前车辆服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN404&protocol=JT808'); }); }); test('shows selected source scope on vehicle detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001&protocol=JT808'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN001', lookupKey: 'VIN001', lookupResolved: true, resolution: { lookupKey: 'VIN001', resolved: true, vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN001', plate: '粤AG18312', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], sourceCount: 2, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 30, socPercent: 78, totalMileageKm: 100, lastSeen: '2026-07-03 20:12:10' }, sources: ['GB32960', 'JT808'], sourceStatus: [ { protocol: 'GB32960', online: false, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '1/2 个来源在线,车辆服务可用但需要关注离线来源。', sourceCount: 2, onlineSourceCount: 1 }, realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('当前查看范围')).toBeInTheDocument(); expect(screen.getAllByText('单一来源:JT808').length).toBeGreaterThan(0); expect(screen.getByText('车辆服务状态')).toBeInTheDocument(); expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined); }); test('shows cross-source consistency for one vehicle service', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-CONSISTENCY-001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-CONSISTENCY-001', lookupKey: 'VIN-CONSISTENCY-001', lookupResolved: true, resolution: { lookupKey: 'VIN-CONSISTENCY-001', resolved: true, vin: 'VIN-CONSISTENCY-001', plate: '粤A一致1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], online: true, lastSeen: '2026-07-03 20:12:10' }, realtimeSummary: { vin: 'VIN-CONSISTENCY-001', plate: '粤A一致1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceCount: 3, onlineSourceCount: 2, online: true, bindingStatus: 'bound', primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 78, totalMileageKm: 1000.5, lastSeen: '2026-07-03T20:12:10+08:00' }, sourceConsistency: { sourceCount: 3, onlineSourceCount: 2, locatedSourceCount: 3, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0.4, sourceTimeDeltaSeconds: 35, scope: 'all_sources', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03T20:12:10+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03T20:12:05+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03T20:11:40+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [ { vin: 'VIN-CONSISTENCY-001', plate: '粤A一致1', protocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 78, totalMileageKm: 1000.5, lastSeen: '2026-07-03T20:12:10+08:00' }, { vin: 'VIN-CONSISTENCY-001', plate: '粤A一致1', protocol: 'JT808', longitude: 113.201, latitude: 23.101, speedKmh: 30, socPercent: 0, totalMileageKm: 1000.8, lastSeen: '2026-07-03T20:12:05+08:00' }, { vin: 'VIN-CONSISTENCY-001', plate: '粤A一致1', protocol: 'YUTONG_MQTT', longitude: 113.2, latitude: 23.1, speedKmh: 31, socPercent: 77, totalMileageKm: 1000.7, lastSeen: '2026-07-03T20:11:40+08:00' } ], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('跨来源一致性')).toBeInTheDocument(); expect(screen.getAllByText('来源不完整').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。').length).toBeGreaterThan(0); expect(screen.getByText('3 个来源')).toBeInTheDocument(); expect(screen.getByText('2/3 在线')).toBeInTheDocument(); expect(screen.getByText('缺失来源')).toBeInTheDocument(); expect(screen.getAllByText('3 个来源有位置').length).toBeGreaterThan(0); expect(screen.getAllByText('0.4 km').length).toBeGreaterThan(0); expect(screen.getAllByText('35 秒').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '查看缺 YUTONG_MQTT 车辆' })); expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=YUTONG_MQTT'); }); test('shows actionable vehicle service recommendations on detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-ACTION-001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-ACTION-001', lookupKey: 'VIN-ACTION-001', lookupResolved: true, resolution: { lookupKey: 'VIN-ACTION-001', resolved: true, vin: 'VIN-ACTION-001', plate: '粤A处置1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], online: true, lastSeen: '2026-07-03T20:12:10+08:00' }, realtimeSummary: { vin: 'VIN-ACTION-001', plate: '粤A处置1', phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceCount: 3, onlineSourceCount: 2, online: true, bindingStatus: 'bound', primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 78, totalMileageKm: 1000.5, lastSeen: '2026-07-03T20:12:10+08:00' }, serviceStatus: { status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。', sourceCount: 3, onlineSourceCount: 2 }, sourceConsistency: { sourceCount: 3, onlineSourceCount: 2, locatedSourceCount: 2, missingProtocols: ['YUTONG_MQTT'], mileageDeltaKm: 0.4, sourceTimeDeltaSeconds: 35, scope: 'all_sources', status: 'degraded', severity: 'warning', title: '来源不完整', detail: '2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [ { protocol: 'GB32960', online: true, lastSeen: '2026-07-03T20:12:10+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'JT808', online: true, lastSeen: '2026-07-03T20:12:05+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03T20:11:40+08:00', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } ], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 2, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 3, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); expect(screen.getByText('降级可服务')).toBeInTheDocument(); expect(screen.getByText('3 项质量问题影响可信度')).toBeInTheDocument(); expect(screen.getByText('先处理质量问题,再将该车用于 BI、告警或对外查询。')).toBeInTheDocument(); expect(await screen.findByText('服务处置建议')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '处理质量问题 3 项' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '排查 YUTONG_MQTT 离线' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源' })); expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=YUTONG_MQTT'); }); test('opens single vehicle quality evidence from vehicle detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-DETAIL-QUALITY&protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-DETAIL-QUALITY', lookupKey: 'VIN-DETAIL-QUALITY', lookupResolved: true, resolution: { lookupKey: 'VIN-DETAIL-QUALITY', resolved: true, vin: 'VIN-DETAIL-QUALITY', plate: '粤A详情告警', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], online: true, lastSeen: '2026-07-03T20:12:10+08:00' }, realtimeSummary: { vin: 'VIN-DETAIL-QUALITY', plate: '粤A详情告警', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], sourceCount: 1, onlineSourceCount: 1, online: true, primaryProtocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 0, totalMileageKm: 1000.5, lastSeen: '2026-07-03T20:12:10+08:00' }, serviceOverview: { vin: 'VIN-DETAIL-QUALITY', plate: '粤A详情告警', phone: '13307795425', oem: 'G7s', protocols: ['JT808'], primaryProtocol: 'JT808', coverageStatus: 'online', sourceCount: 1, onlineSourceCount: 1, lastSeen: '2026-07-03T20:12:10+08:00', realtimeCount: 1, historyCount: 8, rawCount: 16, mileageCount: 2, qualityIssueCount: 1 }, sources: ['JT808'], sourceStatus: [ { protocol: 'JT808', online: true, lastSeen: '2026-07-03T20:12:10+08:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } ], realtime: [], history: { items: [], total: 8, limit: 20, offset: 0 }, raw: { items: [], total: 16, limit: 10, offset: 0 }, mileage: { items: [], total: 2, limit: 20, offset: 0 }, quality: { items: [ { vin: 'VIN-DETAIL-QUALITY', plate: '粤A详情告警', phone: '13307795425', protocol: 'JT808', issueType: 'VIN_MISSING', severity: 'error', lastSeen: '2026-07-03 20:12:10', detail: '详情页质量问题需要处理', sourceEndpoint: '115.231.168.135:43625' } ], total: 1, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect(await screen.findByText('单车处置建议')).toBeInTheDocument(); expect(screen.getAllByText('VIN 缺失').length).toBeGreaterThan(0); expect(screen.getAllByText('详情页质量问题需要处理').length).toBeGreaterThan(0); expect(screen.getAllByText('粤A详情告警 / VIN-DETAIL-QUALITY').length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '单车告警轨迹证据' })); await waitFor(() => { expect(window.location.hash).toBe('#/history?keyword=VIN-DETAIL-QUALITY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); }); }); test('shows canonical vehicle archive on detail', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN-ARCHIVE-001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/vehicle-service')) { return { ok: true, json: async () => ({ data: { vin: 'VIN-ARCHIVE-001', lookupKey: 'VIN-ARCHIVE-001', lookupResolved: true, resolution: { lookupKey: 'VIN-ARCHIVE-001', resolved: true, vin: 'VIN-ARCHIVE-001', plate: '粤A档案1', phone: '', oem: '', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], online: true, lastSeen: '2026-07-03T20:12:10+08:00' }, realtimeSummary: { vin: 'VIN-ARCHIVE-001', plate: '粤A档案1', phone: '', oem: '', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceCount: 3, onlineSourceCount: 2, online: true, bindingStatus: 'bound', primaryProtocol: 'GB32960', longitude: 113.2, latitude: 23.1, speedKmh: 32, socPercent: 78, totalMileageKm: 1000.5, lastSeen: '2026-07-03T20:12:10+08:00' }, sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceStatus: [], realtime: [], history: { items: [], total: 0, limit: 20, offset: 0 }, raw: { items: [], total: 0, limit: 10, offset: 0 }, mileage: { items: [], total: 0, limit: 20, offset: 0 }, quality: { items: [], total: 0, limit: 20, offset: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; } return { ok: true, json: async () => ({ data: path.includes('/api/ops/health') ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } : { items: [], total: 0, limit: 10, offset: 0 }, traceId: 'trace-test', timestamp: 1783094400000 }) } as Response; }); render(); expect((await screen.findAllByText('车辆档案')).length).toBeGreaterThan(0); expect(screen.getByText('车辆主键')).toBeInTheDocument(); expect(screen.getAllByText('VIN-ARCHIVE-001').length).toBeGreaterThan(0); expect(screen.getAllByText('档案完整度').length).toBeGreaterThan(0); expect(screen.getAllByText('2/4').length).toBeGreaterThan(0); expect(screen.getAllByText('缺手机号').length).toBeGreaterThan(0); expect(screen.getAllByText('缺OEM').length).toBeGreaterThan(0); expect(screen.getByText('归并来源数')).toBeInTheDocument(); expect(screen.getAllByText('3').length).toBeGreaterThan(0); });