import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import App from '../App';
afterEach(() => {
cleanup();
vi.useRealTimers();
window.localStorage.clear();
window.history.replaceState(null, '', '/');
delete window.__LINGNIU_APP_CONFIG__;
vi.restoreAllMocks();
});
test('mobile shell prioritizes customer vehicle service content before sidebar navigation', () => {
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.getByTestId('vehicle-platform-shell')).toHaveAttribute('data-mobile-layout', 'content-first');
});
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.getByText('客户主线')).toBeInTheDocument();
expect(screen.getByText('先看车,再查时间窗,最后导出和通知。')).toBeInTheDocument();
expect(screen.getByText('客户任务分发台')).toBeInTheDocument();
expect(screen.getByText('输入 VIN / 车牌 / 手机号后,地图、轨迹、里程、导出和告警都围绕同一辆车服务。')).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();
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('地图看车、实时状态、异常优先')).toBeInTheDocument();
expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('按 VIN、车牌、手机号管理车辆')).toBeInTheDocument();
expect(screen.getAllByText('轨迹里程').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('回放轨迹、里程统计、导出数据')).toBeInTheDocument();
expect(screen.getAllByText('告警通知').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('接入运维')).toBeInTheDocument();
expect(screen.getAllByText('客户').length).toBeGreaterThanOrEqual(3);
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.getAllByText('车辆总览').length).toBeGreaterThanOrEqual(1);
expect(getComputedStyle(document.body).minWidth).not.toBe('1280px');
});
test('sidebar frames the first navigation group as vehicle monitoring for customers', () => {
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.queryByText('实时运营')).not.toBeInTheDocument();
expect(screen.queryByText('运营总览')).not.toBeInTheDocument();
});
test('dashboard opens with a customer vehicle service path instead of protocol entry points', 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: 15,
redisOnlineKeys: 92,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 92,
activeToday: 120,
frameToday: 21000,
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: 988,
onlineVehicles: 92,
singleSourceVehicles: 170,
multiSourceVehicles: 220,
noDataVehicles: 15,
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.findByRole('heading', { name: '客户车辆服务路径' })).toBeInTheDocument();
expect(screen.getByText('客户入口只回答车辆问题:在线在哪里、这段时间怎么跑、里程是否可信、异常是否通知、数据能否交付。')).toBeInTheDocument();
expect(screen.getByText('32960 / 808 / MQTT 只是证据通道')).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();
});
test('dashboard keeps advanced customer workbench collapsed behind the service path', 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: 15,
redisOnlineKeys: 92,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 92,
activeToday: 120,
frameToday: 21000,
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: 988,
onlineVehicles: 92,
singleSourceVehicles: 170,
multiSourceVehicles: 220,
noDataVehicles: 15,
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.findByRole('heading', { name: '客户车辆服务路径' })).toBeInTheDocument();
const summary = screen.getByText('客户进阶工作台');
const details = summary.closest('details');
expect(details).toBeInTheDocument();
expect(details).not.toHaveAttribute('open');
expect(within(details as HTMLElement).getByText('车辆服务指挥台')).toBeInTheDocument();
expect(within(details as HTMLElement).getByText('车辆地图态势')).toBeInTheDocument();
});
test('dashboard gives a customer next best action directly on the first screen', 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: 15,
redisOnlineKeys: 92,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 92,
activeToday: 120,
frameToday: 21000,
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: 988,
onlineVehicles: 92,
singleSourceVehicles: 170,
multiSourceVehicles: 220,
noDataVehicles: 15,
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.findByRole('heading', { name: '客户车辆服务路径' })).toBeInTheDocument();
const nextActionRegion = screen.getByLabelText('建议下一步');
expect(within(nextActionRegion).getByText('建议下一步')).toBeInTheDocument();
expect(within(nextActionRegion).getByText('先处理告警车辆')).toBeInTheDocument();
expect(within(nextActionRegion).getByRole('button', { name: '建议下一步 优先 先处理告警车辆 进入告警' })).toBeInTheDocument();
expect(within(nextActionRegion).getByRole('button', { name: '建议下一步 范围 选择一辆车复盘 车辆中心' })).toBeInTheDocument();
expect(within(nextActionRegion).getByRole('button', { name: '建议下一步 时间窗 按同一范围交付数据 复制复盘包' })).toBeInTheDocument();
});
test('dashboard exposes customer service scope delivery actions on the first screen', async () => {
window.history.replaceState(null, '', '/#/dashboard?keyword=VIN-SCOPE-001&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
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: 15,
redisOnlineKeys: 92,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 92,
activeToday: 120,
frameToday: 21000,
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: 988,
onlineVehicles: 92,
singleSourceVehicles: 170,
multiSourceVehicles: 220,
noDataVehicles: 15,
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.findByRole('heading', { name: '客户车辆服务路径' })).toBeInTheDocument();
const scopeRegion = screen.getByLabelText('客户服务范围');
expect(within(scopeRegion).getByText('当前服务范围')).toBeInTheDocument();
expect(within(scopeRegion).getByText('VIN-SCOPE-001 / JT808 / 2026-07-01 至 2026-07-03')).toBeInTheDocument();
expect(within(scopeRegion).getByRole('button', { name: '客户服务范围 复制范围' })).toBeInTheDocument();
expect(within(scopeRegion).getByRole('button', { name: '客户服务范围 保存视图' })).toBeInTheDocument();
});
test('dashboard presents customer vehicle service modules before operations details', async () => {
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: 15,
redisOnlineKeys: 92,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 88,
activeToday: 119,
frameToday: 32000,
issueVehicles: 3,
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: 1000,
boundVehicles: 960,
onlineVehicles: 88,
singleSourceVehicles: 180,
multiSourceVehicles: 230,
noDataVehicles: 20,
identityRequiredVehicles: 10,
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();
const servicePath = await screen.findByLabelText('客户车辆服务路径');
const serviceOverview = within(servicePath).getByLabelText('客户服务总览');
expect(within(serviceOverview).getByText('客户服务总览')).toBeInTheDocument();
expect(within(serviceOverview).getByText('地图、轨迹、里程、导出是客户能直接使用的车辆服务;协议来源只进入证据和追溯。')).toBeInTheDocument();
expect(within(serviceOverview).getByRole('button', { name: '客户服务总览 车辆地图 进入地图' })).toBeInTheDocument();
expect(within(serviceOverview).getByRole('button', { name: '客户服务总览 轨迹回放 回放轨迹' })).toBeInTheDocument();
expect(within(serviceOverview).getByRole('button', { name: '客户服务总览 里程统计 查看统计' })).toBeInTheDocument();
expect(within(serviceOverview).getByRole('button', { name: '客户服务总览 历史数据 查询导出' })).toBeInTheDocument();
});
test('topbar frames customer work as a vehicle service delivery strip', () => {
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.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.getByRole('button', { name: '车辆服务交付条 告警' })).toBeInTheDocument();
});
test('topbar exposes a mobile vehicle service shortcut dock', () => {
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.getByRole('navigation', { 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.getByRole('button', { name: '移动端车辆服务快捷入口 告警' })).toBeInTheDocument();
});
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: '保持实时、轨迹、原始记录和统计链路巡检。',
acceptance: '车辆服务可回查实时、轨迹和原始记录。',
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: '保持实时、轨迹、原始记录和统计链路巡检。',
acceptance: '车辆服务可回查实时、轨迹和原始记录。',
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.getAllByText('告警车辆').length).toBeGreaterThanOrEqual(1);
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('今日活跃')).toBeInTheDocument();
expect(screen.getAllByText('4').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('7 告警事件')).toBeInTheDocument();
});
test('dashboard prioritizes customer vehicle service command over data sources', 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,
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.findAllByText('车辆服务指挥台')).length).toBeGreaterThan(0);
expect(screen.getByText('10 秒内回答客户最关心的事:车辆在哪里、哪辆车异常、这段时间怎么跑、数据能不能导出。')).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.getAllByText('客户今日运营台').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('今天先看在线和地图,再处理告警、复盘轨迹、核对里程,最后把证据导出给客户。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户今日运营台 车辆在线 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户今日运营台 地图可见 0 有坐标 实时地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户今日运营台 轨迹复盘 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户今日运营台 里程报表 1 天窗口 核对里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户今日运营台 告警通知 7 告警 闭环处理' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制今日服务简报/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制今日服务简报/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户今日车辆服务简报】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:208 / 1,033'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图可见:0 辆有坐标'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日活跃:4'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警车辆:7'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆地图:http://localhost:3000/#/map?online=online'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程报表:http://localhost:3000/#/mileage'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史导出:http://localhost:3000/#/history-query?tab=raw&includeFields=true'));
expect(screen.getByText('客户车队运营总控')).toBeInTheDocument();
expect(screen.getByText('客户首屏只保留车辆地图、在线状态、轨迹回放、里程统计、数据导出和告警通知,协议来源退到证据层。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队运营总控 实时地图 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队运营总控 轨迹回放 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队运营总控 里程统计 1 天窗口 核对里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队运营总控 数据导出 1,286,320 今日数据 导出报表' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队运营总控 告警通知 7 告警 闭环处理' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制运营总控包/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制运营总控包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户车队运营总控包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('首屏能力:实时地图 / 轨迹回放 / 里程统计 / 数据导出 / 告警通知'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('协议来源:仅作为证据层,不作为客户主入口'));
expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 告警待处理 7 辆 告警事件' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 无有效定位 208 辆 打开地图' })).toBeInTheDocument();
expect(screen.getAllByText('车辆服务指挥台')[0].compareDocumentPosition(screen.getByText('车辆地图态势')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(await screen.findByText('车辆服务驾驶舱')).toBeInTheDocument();
expect(screen.getByText('先看车辆,不先看协议;客户进来先知道哪些车在线、在哪里、今天跑了多少、哪些异常要通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 车辆总览 1,033 辆 车辆中心' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 实时可见 208 在线 地图看车' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 轨迹复盘 4 今日活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 里程统计 1 天窗口 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 数据交付 1,286,320 今日数据 导出证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务驾驶舱 告警通知 7 告警 闭环处理' })).toBeInTheDocument();
expect(await screen.findByText('车辆服务闭环')).toBeInTheDocument();
expect(screen.getByText('客户从首页开始,只需要按这五步完成找车、看车、复盘、统计、导出和通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务闭环 1 选车 1,033 车辆中心' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务闭环 2 实时看车 208 在线 实时地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务闭环 3 锁定时间窗 1 天窗口 自定义时间' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务闭环 4 复盘里程 4 活跃 轨迹里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务闭环 5 交付闭环 7 告警 导出通知' })).toBeInTheDocument();
expect(screen.getByText('车辆地图态势')).toBeInTheDocument();
expect(screen.getByText('先看在线车辆在哪里,再决定是否回放轨迹、核对里程、导出数据或通知处理。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆地图态势 打开实时地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆地图态势 回放轨迹' })).toBeInTheDocument();
expect(screen.getByText('车辆地图态势').compareDocumentPosition(screen.getByText('客户车辆服务驾驶舱')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.getByText('客户服务配置和说明')).toBeInTheDocument();
expect(screen.getByText('把蓝图、常用视图、处置说明和服务闭环收起,默认首页只保留地图、车辆、时间窗、导出和告警。')).toBeInTheDocument();
expect(screen.getByText('客户服务配置和说明').compareDocumentPosition(screen.getByText('客户调度运营首页')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(await screen.findByText('客户车辆服务总控台')).toBeInTheDocument();
expect(screen.getByText('来源证据收敛层')).toBeInTheDocument();
expect(screen.getByText('协议来源只用于证明车辆服务可信度,客户主流程仍然从车辆、地图、时间窗、统计、导出和告警进入。')).toBeInTheDocument();
expect(screen.getByText('三个接入来源统一沉到证据层,首页只回答客户怎么监控车辆、回放轨迹、核对里程、导出数据和处理告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 实时地图 208 在线 进入地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 轨迹回放 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 里程统计 1,033 车辆 查里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 时间窗监控 1 天窗口 自定义时间' })).toBeInTheDocument();
expect(screen.getByText('同一时间窗贯穿轨迹、统计、历史导出和告警复盘。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 数据导出 1,286,320 今日数据 查询导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务总控台 告警通知 7 告警 闭环告警' })).toBeInTheDocument();
expect(screen.getByText('来源证据:GB32960 / JT808 / YUTONG_MQTT')).toBeInTheDocument();
expect(screen.getByText('客户车队分组工作台')).toBeInTheDocument();
expect(screen.getByText('按客户责任范围组织车辆池:在线运营、告警处置、身份维护和报表交付都能保存成复用视图。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队分组工作台 在线运营组 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队分组工作台 告警处置组 7 告警 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队分组工作台 身份维护组 9 待绑定 维护车辆' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队分组工作台 报表交付组 1,286,320 今日数据 历史导出' })).toBeInTheDocument();
expect(screen.getByText('客户服务 SLA 承诺')).toBeInTheDocument();
expect(screen.getByText('把断链发现、异常通知、恢复验收和数据交付变成客户能看懂的服务承诺。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 断链发现 1 分钟内 在线状态' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 异常通知 7 告警 通知闭环' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 恢复验收 轨迹/统计可查 验收证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 数据交付 1,286,320 今日数据 导出证据' })).toBeInTheDocument();
expect(screen.getByText('车辆健康与能耗工作台')).toBeInTheDocument();
expect(screen.getByText('把实时工况、SOC/氢能字段、异常维护和里程能耗放到同一辆车的服务视角里。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆健康与能耗工作台 实时工况 208 在线 实时监控' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆健康与能耗工作台 SOC/氢能字段 1,286,320 今日数据 字段证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆健康与能耗工作台 异常维护 7 告警 告警事件' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆健康与能耗工作台 里程能耗 1 天窗口 里程统计' })).toBeInTheDocument();
expect(screen.getByText('地图服务能力清单')).toBeInTheDocument();
expect(screen.getByText('把高德 Web JS 和服务端 API 翻译成客户能使用的车辆服务能力。')).toBeInTheDocument();
expect(screen.getByText('实时底图')).toBeInTheDocument();
expect(screen.getByText('轨迹路径')).toBeInTheDocument();
expect(screen.getByText('区域围栏')).toBeInTheDocument();
expect(screen.getByText('地址解析')).toBeInTheDocument();
expect(await screen.findByText('客户车辆服务中枢')).toBeInTheDocument();
expect(screen.getByText('客户只关心车怎么服务:在哪里、是否在线、这段时间怎么跑、里程是否可信、异常是否已通知、数据能否交付。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中枢 实时地图 208 在线 看车在哪' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中枢 轨迹回放 4 活跃 回放路线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中枢 里程统计 1 天窗口 核对里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中枢 数据导出 1,286,320 今日数据 交付证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中枢 告警通知 7 告警 闭环通知' })).toBeInTheDocument();
expect(screen.getByText('运维接入、队列、存储、协议解析降级为证据层,默认不打断客户主流程。')).toBeInTheDocument();
});
test('dashboard presents a customer dispatch operations home', 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
}
},
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.getByText('值班人员进来先看地图态势和异常队列,再进入轨迹回放、时间窗统计、历史导出和告警通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户调度运营首页 实时地图 0 有定位 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户调度运营首页 今日重点 7 告警 处理告警' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '调度运营任务 看全域位置 208 在线 实时地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '调度运营任务 回放重点轨迹 4 活跃 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '调度运营任务 核对时间窗里程 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '调度运营任务 导出客户证据 1,286,320 帧 历史导出' })).toBeInTheDocument();
});
test('dashboard frames history query and export as a customer delivery workbench', async () => {
window.history.replaceState(null, '', '/#/dashboard');
vi.setSystemTime(new Date('2026-07-05T12:00:00+08:00'));
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
}
},
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.getByText('先锁定车辆和时间窗,再选择轨迹、里程、历史明细或告警说明,最终交付一份客户能看懂的数据包。')).toBeInTheDocument();
expect(screen.getByText('当前交付范围')).toBeInTheDocument();
expect(screen.getAllByText('全部车辆 / 全部数据通道 / 2026-07-04 至 2026-07-05').length).toBeGreaterThan(0);
expect(screen.getByText('常用时间范围')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用时间范围 最近一天 2026-07-04 至 2026-07-05 应用' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用时间范围 最近三天 2026-07-02 至 2026-07-05 应用' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用时间范围 本周 2026-06-29 至 2026-07-05 应用' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用时间范围 自定义复盘 当前范围 应用' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户查询导出交付台 轨迹回放 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户查询导出交付台 里程统计 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户查询导出交付台 历史明细 1,286,320 帧 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户查询导出交付台 告警说明 7 告警 告警通知' })).toBeInTheDocument();
});
test('dashboard separates customer vehicle service from operations evidence', 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: 281,
tdengineWritable: true,
mysqlWritable: true,
runtime: {
requestTimeoutMs: 5000,
amapWebJsConfigured: true,
amapApiConfigured: true,
amapSecurityProxyEnabled: true,
amapSecurityCodeExposed: false
}
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 73,
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.getByText('默认展示车辆地图、时间窗、里程、导出和告警;三类数据源只作为二级证据。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户主流程 实时地图 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户主流程 时间窗复盘 1 天窗口 查询导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户主流程 里程统计 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户主流程 告警通知 7 告警 通知处理' })).toBeInTheDocument();
expect(screen.getByText('运维证据层')).toBeInTheDocument();
expect(screen.getByText('用于解释车辆服务不可用的链路、队列、缓存、存储和来源证据,不作为客户默认入口。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '运维证据层 链路健康 正常 查看告警' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '运维证据层 消息积压 0 查看运维' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '运维证据层 缓存在线 281 缓存证据' })).toBeInTheDocument();
});
test('dashboard guides customer custom time window service path', 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
}
},
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.getByText('锁定同一辆车和同一时间范围后,按轨迹、里程、历史证据和告警说明形成客户可复盘的交付路径。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗服务路径 1 锁定范围 1 天窗口 应用时间窗' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗服务路径 2 回放轨迹 4 活跃 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗服务路径 3 核对里程 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗服务路径 4 导出证据 1,286,320 帧 数据导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗服务路径 5 告警说明 7 告警 告警通知' })).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: '时间窗下一步 证据状态 1,286,320 帧' })).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();
});
test('time monitor route exposes a dedicated customer time window workspace', async () => {
window.history.replaceState(null, '', '/#/time-monitor?keyword=%E7%B2%A4A%E6%97%B6%E9%97%B4%E7%AA%97&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,
activeConnections: 0,
capacityFindings: [],
redisOnlineKeys: 0,
tdengineWritable: true,
mysqlWritable: true,
runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/dashboard/summary')) {
return {
ok: true,
json: async () => ({
data: {
onlineVehicles: 6,
activeToday: 8,
frameToday: 9988,
issueVehicles: 2,
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: 12,
boundVehicles: 12,
onlineVehicles: 6,
singleSourceVehicles: 3,
multiSourceVehicles: 4,
noDataVehicles: 2,
identityRequiredVehicles: 0,
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.findByRole('heading', { name: '自定义时间监控' })).toBeInTheDocument();
expect(screen.getByText('把客户选择的一辆车和一个时间窗固定下来,同时进入实时地图、轨迹回放、里程统计、历史导出和告警复盘。')).toBeInTheDocument();
expect(screen.getAllByText('时间窗监控').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('当前监控范围:粤A时间窗 / JT808 / 2026-07-01 至 2026-07-03')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 轨迹回放 3 天窗口' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 里程统计 3 天窗口' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 历史导出 9,988 帧' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 告警复盘 2 告警' })).toBeInTheDocument();
expect(screen.getByText('时间窗复盘 SLA 作业台')).toBeInTheDocument();
expect(screen.getByText('把一个时间窗变成可交付作业:谁处理、多久交付、交付哪些证据、异常如何升级。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘SLA 责任动作 客户复盘 复制作业' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘SLA 交付时限 3 天窗口 轨迹复盘' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘SLA 交付物 轨迹/统计/导出/告警 导出证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘SLA 升级条件 2 告警 告警复盘' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制时间窗作业单/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制时间窗作业单/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘作业单】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('作业范围:粤A时间窗 / JT808 / 2026-07-01 至 2026-07-03'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付SLA:3 天窗口内完成轨迹复盘、统计核对、历史导出和告警说明'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹回放链接 / 里程统计链接 / 历史导出链接 / 告警复盘链接'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级条件:2 告警车辆需要先进入告警复盘'));
});
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: '保持实时、轨迹、原始记录和统计链路巡检。',
acceptance: '车辆服务可回查实时、轨迹和原始记录。',
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.getAllByText('车辆服务工作台').length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('region', { name: '客户车辆服务驾驶舱' })).toBeInTheDocument();
expect(screen.getByText('客户进来先看车辆,不先看协议。')).toBeInTheDocument();
expect(screen.getByText('把实时地图、轨迹回放、里程统计、历史导出和告警通知收敛成一个可交付的车辆服务入口。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务驾驶舱 车辆在哪里 0 有定位 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务驾驶舱 轨迹能否回放 4 活跃 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务驾驶舱 里程怎么算 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务驾驶舱 数据怎么导出 1,286,320 今日数据 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务驾驶舱 异常谁处理 7 告警 告警通知' })).toBeInTheDocument();
expect(screen.getByText('客户车辆服务中台蓝图')).toBeInTheDocument();
expect(screen.getByText('参考主流车联网平台的信息架构,把 Live Map、Route Replay、Geofence Alerts、Reports Export 和 Vehicle Health 收敛成客户可直接使用的车辆服务。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中台蓝图 实时地图 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中台蓝图 轨迹回放 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中台蓝图 围栏告警 7 告警 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中台蓝图 报表导出 1,286,320 今日数据 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务中台蓝图 车辆健康 1,033 车辆 车辆中心' })).toBeInTheDocument();
expect(screen.getByRole('region', { name: '客户常用视图库' })).toBeInTheDocument();
expect(screen.getByText('一个车辆服务入口,而不是三个数据源入口。')).toBeInTheDocument();
expect(screen.getByText('把车队范围、时间窗、地图态势、轨迹回放、里程统计、导出模板和告警订阅保存成客户可复用的车辆服务入口。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用视图库 在线车队视图 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用视图库 轨迹日报模板 4 活跃 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用视图库 里程周报模板 1,033 车辆 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户常用视图库 告警订阅视图 7 告警 通知闭环' })).toBeInTheDocument();
expect(screen.getByRole('region', { name: '十秒车辆处置台' })).toBeInTheDocument();
expect(screen.getByText('登录后先知道哪三类车辆最需要处理,再进入地图、车辆中心或告警通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '十秒车辆处置台 告警车辆 7 辆 处理告警' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '十秒车辆处置台 无数据车辆 461 辆 查车辆' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '十秒车辆处置台 身份未绑定 9 辆 维护绑定' })).toBeInTheDocument();
expect(screen.getByText('现代车队运营台')).toBeInTheDocument();
expect(screen.getByText('一屏完成车辆地图、轨迹回放、里程统计、数据导出和告警闭环。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '现代车队运营台 Live Map 208 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '现代车队运营台 Route Replay 4 活跃 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '现代车队运营台 Mileage 1,033 车辆 查里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '现代车队运营台 Export 1,286,320 今日数据 导出数据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '现代车队运营台 Alerts 7 告警 通知闭环' })).toBeInTheDocument();
expect(screen.getByText('高德地图车辆服务底座')).toBeInTheDocument();
expect(screen.getByText('高德能力不单独成为页面目标,而是支撑同一辆车的实时看车、路线回放、区域告警和时间窗数据交付。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 实时看车 0 有定位 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 路线回放 1 天窗口 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 区域告警 7 告警 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 时间窗导出 1,286,320 今日数据 历史导出' })).toBeInTheDocument();
expect(screen.getByText('车辆监控中心')).toBeInTheDocument();
expect(screen.getByText('先看车在哪里,再处理轨迹、统计、告警和导出')).toBeInTheDocument();
expect(screen.getByText('客户主流程围绕车辆本身:地图看车、实时状态、时间窗复盘、里程统计、历史数据导出和告警通知。接入来源只作为追溯证据。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆监控指标 车辆总数 1,033' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆监控指标 在线车辆 208' })).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('客户首先关心的是车辆、位置、时间、里程、告警和导出;GB32960、JT808、MQTT 只放在证据层。')).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();
expect(screen.getByText('车辆服务优先级队列')).toBeInTheDocument();
expect(screen.getByText('先处理影响客户看车、查轨迹、算里程和收通知的问题车辆。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务优先级队列 告警待处理 7 辆 告警事件' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务优先级队列 无有效定位 208 辆 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务优先级队列 身份未绑定 9 辆 维护绑定' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务优先级队列 暂无数据 461 辆 车辆列表' })).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();
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.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.getByRole('button', { name: '客户交付驾驶舱 时间窗复盘 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户交付驾驶舱 数据证据交付 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户交付驾驶舱 告警通知交付 告警事件' })).toBeInTheDocument();
expect(screen.getAllByText('车辆服务指挥台').length).toBeGreaterThan(0);
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.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('按时间窗复盘')).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.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.getAllByRole('button', { name: '复制客户服务说明' }).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('自定义时间窗复盘')).toBeInTheDocument();
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.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.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('这个范围会同步带入轨迹、统计、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).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.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.getByText('运维和证据层')).toBeInTheDocument();
expect(screen.getByText('来源覆盖、地图配置、告警预览和实时证据默认收起,客户主流程先看车辆监控、轨迹、统计、告警和导出。')).toBeInTheDocument();
expect(screen.getAllByText('车辆服务工作台').length).toBeGreaterThanOrEqual(1);
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.getAllByRole('button', { name: '里程统计' })[0]);
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('交付结论:带风险交付;存在告警车辆,交付时需要附带告警闭环和原始证据。'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('时间窗判定:1 天窗口'));
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);
}, 10000);
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('客户车辆监控台')).toBeInTheDocument();
expect(screen.getByText('先回答客户最常问的车辆问题:车在哪里、这段时间怎么跑、里程怎么算、数据怎么导出、异常谁处理。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆监控台 车辆在哪里 1 辆有定位 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆监控台 回放这段时间 4 今日活跃 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆监控台 里程统计 1,033 辆可统计 查里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆监控台 导出客户数据 1,286,320 今日数据 导出数据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆监控台 告警通知 7 告警车辆 通知闭环' })).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.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('告警复盘').length).toBeGreaterThanOrEqual(1);
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.getAllByText('统计证据').length).toBeGreaterThanOrEqual(1);
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.getAllByText('VIN 缺失').length).toBeGreaterThanOrEqual(1);
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.getAllByText('告警车辆').length).toBeGreaterThanOrEqual(1);
});
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: '告警车辆 7' }));
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');
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: 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.getByRole('button', { name: '客户车队指挥台 在线率 40.3% 车辆地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队指挥台 可服务车辆 2 辆 实时监控' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队指挥台 待处理车辆 13 辆 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队指挥台 当前交付 2 辆 数据导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车队指挥台 轨迹复盘 先选车辆 轨迹回放' })).toBeInTheDocument();
expect(screen.getByText('高德地图车辆服务底座')).toBeInTheDocument();
expect(screen.getByText('用地图把车辆监控、轨迹回放、围栏告警、里程统计和历史导出串成客户能理解的一条服务路径。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 实时看车 73 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 轨迹复盘 先选车辆 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 围栏告警 13 待处理 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '高德地图车辆服务底座 数据交付 2 当前页 导出清单' })).toBeInTheDocument();
expect(screen.getByText('客户车辆服务台')).toBeInTheDocument();
expect(screen.getByText('三个接入源最终服务同一辆车,客户从这里进入找车、实时、轨迹、统计、导出和告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务台 车辆池 181 车辆 找车' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务台 实时地图 73 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务台 轨迹回放 先选车辆 回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务台 里程统计 单车后可用 查询里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆服务台 告警通知 13 待处理 查看告警' })).toBeInTheDocument();
expect(screen.getByText('车辆资产运营台')).toBeInTheDocument();
expect(screen.getByText('像车队资产系统一样,先判断车辆能不能服务,再进入地图、实时、轨迹、统计、导出和告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆资产运营台 资产健康 181 车辆 查看车辆池' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆资产运营台 在线车辆 73 在线 打开实时' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆资产运营台 待处理 13 待处理 异常优先' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆资产运营台 服务交付 2 当前页 复制交付包' })).toBeInTheDocument();
expect(screen.getByText('车辆池快捷视图')).toBeInTheDocument();
expect(screen.getByText('按客户服务场景一键切换车辆池:在线运营、离线断链、无来源、身份维护和报表交付。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆池快捷视图 在线运营 73 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆池快捷视图 离线断链 108 离线 排查离线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆池快捷视图 无来源车辆 4 无来源 平台转发' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆池快捷视图 身份维护 9 未绑定 维护绑定' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆池快捷视图 报表交付 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('VIN / 车牌 / 手机号')).toBeInTheDocument();
expect(screen.getByText('轨迹与位置')).toBeInTheDocument();
expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('button', { name: /复制客户查车路径/ })).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.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.getAllByText('异常车辆').length).toBeGreaterThan(0);
expect(screen.getAllByText('4 无来源').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('把车辆地图、实时监控、轨迹回放、里程统计、告警通知和历史导出放在同一份服务包里,客户不用理解 GB32960、808 或 MQTT 的差异。')).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();
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();
fireEvent.click(screen.getAllByRole('button', { name: /复制交付包/ })[0]);
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户车辆服务交付包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务范围:当前车辆池'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆地图:http://localhost:3000/#/map?'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?tab=location'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程统计:http://localhost:3000/#/mileage'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警通知:http://localhost:3000/#/alert-events?coverage=multi'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史数据导出:http://localhost:3000/#/history?tab=raw&includeFields=true'));
});
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.getAllByText('数据通道:JT808').length).toBeGreaterThan(0);
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);
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
expect(window.location.hash).toBe('#/map?keyword=LB9A32A24R0LS1426&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' }));
expect(window.location.hash).toBe('#/realtime?keyword=LB9A32A24R0LS1426&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' }));
expect(window.location.hash).toBe('#/history-query?keyword=LB9A32A24R0LS1426&protocol=JT808&tab=raw');
fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' }));
expect(window.location.hash).toBe('#/alert-events?keyword=LB9A32A24R0LS1426&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '客户任务 轨迹回放 这段时间怎么跑' }));
expect(window.location.hash).toBe('#/history?keyword=LB9A32A24R0LS1426&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '客户任务 里程统计 里程怎么算' }));
expect(window.location.hash).toBe('#/mileage?keyword=LB9A32A24R0LS1426&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '客户任务 数据导出 证据怎么给客户' }));
expect(window.location.hash).toBe('#/history-query?keyword=LB9A32A24R0LS1426&protocol=JT808&tab=raw');
fireEvent.click(screen.getByRole('button', { name: '客户任务 告警通知 异常谁处理' }));
expect(window.location.hash).toBe('#/alert-events?keyword=LB9A32A24R0LS1426&protocol=JT808');
});
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.getAllByText('证据闭环').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('客户告警闭环时间线')).toBeInTheDocument();
expect(screen.getByText('从告警触发、车辆影响、责任通知、证据复核到恢复验收,形成客户可追踪的闭环。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环时间线 1 告警触发 5 条事件 查看事件' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环时间线 2 影响车辆 3 辆车 影响摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环时间线 3 责任通知 接入运维 + 业务责任人 通知策略' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环时间线 4 证据复核 实时/轨迹/里程 证据交接' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环时间线 5 恢复验收 有标准 复制回执' })).toBeInTheDocument();
expect(screen.getByText('客户告警闭环台')).toBeInTheDocument();
expect(screen.getByText('先判断影响车辆,再通知责任人,随后打开实时、轨迹、原始记录完成处置,最后按恢复标准验收。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环台 1 影响车辆 3 辆' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环台 2 通知升级 P0 实时中断' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环台 3 处置证据 实时/轨迹/原始记录' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环台 4 恢复验收 实时/轨迹/里程/导出' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制告警闭环包' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警闭环包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响范围:3 辆 / 5 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略:P0 实时中断 / 接入运维 + 业务责任人 / 站内告警 / 邮件 / 企业微信 / 30 分钟升级'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据入口:'));
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('客户处置:先判断影响车辆和优先级,再通知责任团队,并用实时、轨迹、原始记录、里程证据闭环。', { 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.getAllByText('通知策略').length).toBeGreaterThan(0);
expect(screen.getAllByText('P0 实时中断').length).toBeGreaterThan(0);
expect(screen.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThan(0);
expect(screen.getByText('站内告警 / 邮件 / 企业微信')).toBeInTheDocument();
expect(screen.getAllByText('30 分钟升级').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据').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.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThan(1);
expect(screen.getAllByText('无来源、VIN 缺失、存储不可写').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.getByRole('button', { name: '客户恢复验收台 客户回执 复制回执' })).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(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警恢复验收回执】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('恢复标准:实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前影响:3 辆车 / 5 条告警 / 高风险'));
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.getAllByText('实时确认').length).toBeGreaterThan(0);
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.getAllByText('后端 P0 通知策略').length).toBeGreaterThan(0);
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.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: '客户告警闭环 超时升级 SLA报告' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警闭环 验收恢复 运行手册' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '复制值班清单' })).toBeInTheDocument();
expect(screen.getByText('客户通知编排条')).toBeInTheDocument();
expect(screen.getByText('先把规则命中转成可执行的通知编排:触发条件、受影响车辆、责任方、渠道、SLA 和验收口径在一行里完成确认。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排 告警触发 2 类活跃规则' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排 受影响车辆 1 待通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排 通知对象 接入运维 + 业务责任人' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排 升级时钟 已超时 1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排 恢复验收 来源恢复并持续 10 分钟' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排动作 查看告警 告警队列' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排动作 复制通知 待通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排动作 复制SLA SLA报告' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知编排动作 导出演练 演练包' })).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: '客户通知常问 多久没有恢复会升级? SLA报告' })).toBeInTheDocument();
expect(screen.getByRole('button', { 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.getAllByText('platform-rules-test').length).toBeGreaterThan(0);
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(screen.getByText('通知可达性检查')).toBeInTheDocument();
expect(screen.getByText('避免告警已经触发但无人收到或无法闭环。', { exact: false })).toBeInTheDocument();
expect(screen.getByText('阻断 1 / 风险 0')).toBeInTheDocument();
expect(screen.getAllByText('已超时').length).toBeGreaterThan(0);
expect(screen.getAllByText('1 条待通知').length).toBeGreaterThan(0);
expect(screen.getAllByText('对象:接入运维 + 业务责任人;渠道:邮件 / 企业微信;升级:30 分钟升级').length).toBeGreaterThan(0);
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('原始记录: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. 对第一条待通知告警打开车辆服务、实时、轨迹、原始记录四类证据'));
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: '复制通知可达性检查' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知可达性检查】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('可达结论:阻断 1 / 需跟进 0 / 可触达 1'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台接入 / 已超时'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('渠道:邮件 / 企业微信'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据:1/1 完整'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('下一步:立即升级责任人并同步业务侧'));
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 separated evidence layer for customer vehicle service', 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('客户主流程外的链路、容量、存储和地图配置证据,用来解释车辆实时、轨迹、统计、导出或告警为何不可用。')).toBeInTheDocument();
expect(screen.getByText('客户问题解释路径')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户问题解释路径 车辆不在线 看实时写入' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户问题解释路径 轨迹缺数据 看历史消费' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户问题解释路径 无法导出 看存储/API' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户问题解释路径 地图不可用 看高德配置' })).toBeInTheDocument();
expect(screen.getByText('运维证据只在客户问题需要解释时打开,默认不参与车辆服务主流程。')).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('原始记录: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. 核对实时定位、轨迹证据和原始记录'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('3. 完成后确认车辆服务状态恢复,问题不再命中当前告警'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收标准:车辆能解析到 VIN,实时、历史和原始记录可通过同一车辆服务查询'));
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();
expect(screen.getByText('客户通知 SLA')).toBeInTheDocument();
expect(screen.getByText('把告警翻译成客户能理解的通知、时限和恢复标准')).toBeInTheDocument();
expect(screen.getByText('每一条告警都先回答影响哪些车、通知谁、多久升级、恢复后如何验收;链路、协议和原始记录只作为证据附件。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知SLA 客户影响范围 影响摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知SLA 通知负责人 通知闭环' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知SLA 升级时限 复制策略' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户通知SLA 恢复验收 交接包' })).toBeInTheDocument();
expect(screen.getAllByText('接入运维 + 业务责任人').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('30 分钟升级').length).toBeGreaterThanOrEqual(1);
fireEvent.click(screen.getAllByRole('button', { name: '复制通知交接包' })[0]);
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('原始记录: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('原始记录: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 来源间断,需要核对原始记录'
}],
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();
const rawEvidenceButtons = screen.getAllByRole('button', { name: '原始记录' });
fireEvent.click(rawEvidenceButtons[rawEvidenceButtons.length - 1]);
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: '核对原始' }));
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('客户复盘导航')).toBeInTheDocument();
expect(screen.getByText('像 Trips History 一样,先锁定车辆和时间,再回放路线、核对里程、导出证据。')).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.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('把客户最关心的起止时间、路线跨度、里程差、最高速度和异常提示放在轨迹前面。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '行程复盘摘要 起点 2026-07-03 10:00:00' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '行程复盘摘要 终点 2026-07-03 11:00:00' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '行程复盘摘要 行程里程 22.6 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '行程复盘摘要 最高速度 58.8 km/h' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '行程复盘摘要 异常提示 1 项' })).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.getByRole('button', { name: '轨迹回放指标 轨迹点 2' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '轨迹回放工作台 轨迹复盘 播放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '轨迹回放工作台 客户导出 复制交付' })).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.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. 最后回到依据:异常点必须打开明细证据、字段裁剪和里程统计复核。'));
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.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: 'LB9A32A24R0LS1426' } });
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
expect(await screen.findByText('粤AGQ8398 / LB9A32A24R0LS1426')).toBeInTheDocument();
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&dateFrom=2026-07-01&dateTo=2026-07-03');
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(expect.stringContaining('dateFrom=2026-07-01'), undefined);
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateTo=2026-07-03'), 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.getAllByText('来源证据:JT808').length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: '顶部里程统计' }));
expect(window.location.hash).toBe('#/mileage?keyword=%E7%B2%A4AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' }));
expect(window.location.hash).toBe('#/history-query?keyword=%E7%B2%A4AG18312&protocol=JT808&tab=raw&dateFrom=2026-07-01&dateTo=2026-07-03');
fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' }));
expect(window.location.hash).toBe('#/alert-events?keyword=%E7%B2%A4AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
});
test('topbar customer time window scopes vehicle service tasks', 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: 'VIN-TOPBAR-WINDOW',
plate: '粤A时间窗',
primaryProtocol: 'JT808',
protocols: ['JT808'],
coverageStatus: 'online',
sourceCount: 1,
onlineSourceCount: 1,
realtimeCount: 1,
historyCount: 8,
rawCount: 20,
mileageCount: 3,
qualityIssueCount: 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(screen.getByText('客户时间窗')).toBeInTheDocument();
expect(screen.getByText('同步约束地图、轨迹、里程、导出和告警')).toBeInTheDocument();
expect(screen.getByLabelText('客户时间窗开始日期')).toHaveAttribute('placeholder', 'YYYY-MM-DD');
expect(screen.getByLabelText('客户时间窗结束日期')).toHaveAttribute('placeholder', 'YYYY-MM-DD');
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A时间窗' } });
fireEvent.change(screen.getByLabelText('客户时间窗开始日期'), { target: { value: '2026-07-01' } });
fireEvent.change(screen.getByLabelText('客户时间窗结束日期'), { target: { value: '2026-07-03' } });
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
expect(await screen.findByText('粤A时间窗 / VIN-TOPBAR-WINDOW')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部里程统计' }));
expect(window.location.hash).toBe('#/mileage?keyword=VIN-TOPBAR-WINDOW&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' }));
expect(window.location.hash).toBe('#/history-query?keyword=VIN-TOPBAR-WINDOW&protocol=JT808&tab=raw&dateFrom=2026-07-01&dateTo=2026-07-03');
fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' }));
expect(window.location.hash).toBe('#/alert-events?keyword=VIN-TOPBAR-WINDOW&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
});
test('topbar customer time window presets support common monitoring ranges', async () => {
vi.setSystemTime(new Date('2026-07-05T10:00:00+08:00'));
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: 'VIN-TOPBAR-PRESET',
plate: '粤A快捷窗',
primaryProtocol: 'GB32960',
protocols: ['GB32960'],
coverageStatus: 'online',
sourceCount: 1,
onlineSourceCount: 1,
realtimeCount: 1,
historyCount: 8,
rawCount: 20,
mileageCount: 3,
qualityIssueCount: 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();
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 今天' }));
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-07-05');
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05');
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' }));
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29');
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05');
expect(await screen.findByRole('button', { name: /时间窗服务路径 1 锁定范围 7 天窗口 应用时间窗/ })).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A快捷窗' } });
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
expect(await screen.findByText('粤A快捷窗 / VIN-TOPBAR-PRESET')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' }));
expect(window.location.hash).toBe('#/history-query?keyword=VIN-TOPBAR-PRESET&protocol=GB32960&tab=raw&dateFrom=2026-06-29&dateTo=2026-07-05');
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 清空' }));
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('');
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('');
fireEvent.click(screen.getByRole('button', { name: '顶部里程统计' }));
expect(window.location.hash).toBe('#/mileage?keyword=VIN-TOPBAR-PRESET&protocol=GB32960');
});
test('topbar copies customer vehicle service scope links', async () => {
vi.setSystemTime(new Date('2026-07-05T10:00:00+08:00'));
window.history.replaceState(null, '', '/#/dashboard');
const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
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: 'VIN-SHARE-SCOPE',
plate: '粤A共享窗',
primaryProtocol: 'JT808',
protocols: ['JT808'],
coverageStatus: 'online',
sourceCount: 1,
onlineSourceCount: 1,
realtimeCount: 1,
historyCount: 8,
rawCount: 20,
mileageCount: 3,
qualityIssueCount: 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();
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' }));
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A共享窗' } });
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
expect(await screen.findByText('粤A共享窗 / VIN-SHARE-SCOPE')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部复制客户服务范围' }));
const copied = String(writeText.mock.lastCall?.[0] ?? '');
expect(copied).toContain('【客户车辆服务范围】');
expect(copied).toContain('车辆:粤A共享窗 / VIN-SHARE-SCOPE');
expect(copied).toContain('时间窗:2026-06-29 至 2026-07-05');
expect(copied).toContain('实时地图:http://localhost:3000/#/map?keyword=VIN-SHARE-SCOPE&protocol=JT808');
expect(copied).toContain('轨迹回放:http://localhost:3000/#/history?keyword=VIN-SHARE-SCOPE&protocol=JT808&dateFrom=2026-06-29&dateTo=2026-07-05');
expect(copied).toContain('里程统计:http://localhost:3000/#/mileage?keyword=VIN-SHARE-SCOPE&protocol=JT808&dateFrom=2026-06-29&dateTo=2026-07-05');
expect(copied).toContain('数据导出:http://localhost:3000/#/history-query?keyword=VIN-SHARE-SCOPE&protocol=JT808&tab=raw&dateFrom=2026-06-29&dateTo=2026-07-05');
expect(copied).toContain('告警通知:http://localhost:3000/#/alert-events?keyword=VIN-SHARE-SCOPE&protocol=JT808&dateFrom=2026-06-29&dateTo=2026-07-05');
});
test('topbar saves and restores reusable customer vehicle views', async () => {
vi.setSystemTime(new Date('2026-07-05T10:00:00+08:00'));
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: 'VIN-SAVED-SCOPE',
plate: '粤A复用窗',
primaryProtocol: 'GB32960',
protocols: ['GB32960'],
coverageStatus: 'online',
sourceCount: 1,
onlineSourceCount: 1,
realtimeCount: 1,
historyCount: 8,
rawCount: 20,
mileageCount: 3,
qualityIssueCount: 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();
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' }));
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A复用窗' } });
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
expect(await screen.findByText('粤A复用窗 / VIN-SAVED-SCOPE')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部保存客户视图' }));
expect(screen.getByRole('button', { name: '客户视图 粤A复用窗 / VIN-SAVED-SCOPE 2026-06-29 至 2026-07-05' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 清空' }));
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
expect(window.location.hash).toBe('#/map?keyword=VIN-SAVED-SCOPE&protocol=GB32960');
fireEvent.click(screen.getByRole('button', { name: '客户视图 粤A复用窗 / VIN-SAVED-SCOPE 2026-06-29 至 2026-07-05' }));
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29');
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05');
fireEvent.click(screen.getByRole('button', { name: '顶部里程统计' }));
expect(window.location.hash).toBe('#/mileage?keyword=VIN-SAVED-SCOPE&protocol=GB32960&dateFrom=2026-06-29&dateTo=2026-07-05');
});
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.getAllByText('来源证据:JT808').length).toBeGreaterThan(0);
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.getAllByText('客户导出向导').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('按客户交付顺序完成:先锁定车辆和时间窗,再选择轨迹、明细、字段或统计证据,最后复核质量并复制交付包。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 1 锁定范围 VIN-FIELDS-001 / GB32960' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 2 选择证据 字段证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 3 质量复核 可交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 4 导出交付 复制交付包' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制导出向导包/ })).toBeInTheDocument();
expect(screen.getByText('客户数据交付封面')).toBeInTheDocument();
expect(screen.getByText('先给客户一个可读封面:交付结论、车辆范围、时间窗、交付物、质量提示和下一步动作。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付封面 交付结论 可交付 复制封面' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付封面 轨迹报告 0 点 补查轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付封面 字段证据 2 字段 导出字段' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付封面 客户说明 可复制 复制说明' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制交付封面包/ })).toBeInTheDocument();
expect(screen.getByText('客户数据交付结论条')).toBeInTheDocument();
expect(screen.getByText('先判断这次查询能交付什么,再选择导出位置、明细证据、字段裁剪或复制说明。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付结论 交付状态 可交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付结论 位置历史 0 条' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付结论 明细证据 1 帧' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付结论 字段裁剪 2 字段' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付动作 导出位置 位置CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付动作 导出明细 明细CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付动作 字段裁剪 字段CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付动作 复制说明 交付包' })).toBeInTheDocument();
expect(screen.getByText('导出下一步行动条')).toBeInTheDocument();
expect(screen.getByText('当前查询先决定交付什么、缺什么、怎么复用,避免客户拿到一堆无解释 CSV。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出下一步行动 首要交付 字段证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出下一步行动 轨迹状态 待补轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出下一步行动 证据状态 1 帧 / 2 字段' })).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();
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: '客户交付准备度 范围已锁定 VIN-FIELDS-001 查看范围' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户交付准备度 轨迹待补 0 点 查询轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户交付准备度 字段已裁剪 2 字段 字段导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户交付准备度 说明可复制 可交付 复制说明' })).toBeInTheDocument();
expect(screen.getByText('客户导出验收清单')).toBeInTheDocument();
expect(screen.getByText('客户交付前只看五个门禁:范围、轨迹、明细、字段、异常说明;任何一项不满足都能直接进入补查或导出。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出验收 范围锁定 VIN-FIELDS-001 / GB32960' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出验收 轨迹证据 0 点' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出验收 明细证据 1 帧' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出验收 字段裁剪 2 字段' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出验收 异常说明 可交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出验收动作 复制验收 交付说明' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出验收动作 导出位置 位置CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出验收动作 导出字段 字段CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '导出验收动作 查看明细 明细证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户证据包交付总览 交付范围 VIN-FIELDS-001 / GB32960 查看范围' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户证据包交付总览 轨迹证据 0 点 查询轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户证据包交付总览 明细字段 2 字段 字段导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户证据包交付总览 交付说明 可交付 复制说明' })).toBeInTheDocument();
expect(screen.getByText('客户先选车辆和时间窗,再拿到路线、里程、原始证据、字段裁剪和告警说明。')).toBeInTheDocument();
expect(screen.getAllByText('来源证据:GB32960').length).toBeGreaterThan(0);
expect(screen.getByText('当前服务范围:VIN-FIELDS-001 / GB32960')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆历史服务台 路线复盘 0 点 查询位置' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆历史服务台 里程核对 - 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆历史服务台 明细证据 1 帧 查看证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆历史服务台 字段裁剪 2 字段 字段配置' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆历史服务台 客户导出 可交付 复制交付' })).toBeInTheDocument();
expect(screen.getByText('客户导出中心')).toBeInTheDocument();
expect(screen.getByText('按客户用途选择报表:行程历史、里程对账、字段证据和告警说明。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出中心 行程历史 0 点 导出轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出中心 里程对账 - 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出中心 字段证据 2 字段 导出字段' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出中心 告警说明 无明显异常 复制说明' })).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.getByRole('button', { name: '轨迹回放工作台 明细证据 查看证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '轨迹回放工作台 客户导出 复制交付' })).toBeInTheDocument();
expect(screen.getByText('面向客户的数据导出以车辆服务为中心:同一个筛选范围可以交付轨迹报告、位置历史、明细证据、字段裁剪、CSV 文件和里程统计。')).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();
expect(screen.getByText('报表交付模板')).toBeInTheDocument();
expect(screen.getByText('按客户问题选择导出模板')).toBeInTheDocument();
expect(screen.getByText('车队报表不只是下载 CSV:先选择用途,再生成轨迹报告、明细证据包、字段裁剪包或周期交付计划。')).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();
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('参考 Fleetio、Samsara、Geotab 的报表能力,把导出从一次性下载升级为可复用的交付节奏。')).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();
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('把一次性 CSV 下载变成客户可复用的报表模板:日报看车辆、周报做对账、临时解释处理质疑、字段模板保持口径稳定。')).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.getByRole('button', { name: '保存报表视图 当前筛选 VIN-FIELDS-001 保存视图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存报表视图 字段模板 2 字段 复用字段' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存报表视图 交付节奏 日报/周报 复制计划' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存报表视图 快速分享 可复制 复制链接' })).toBeInTheDocument();
expect(screen.getByText('客户轨迹决策台')).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: /复制决策说明/ }).length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('button', { name: '客户轨迹决策 明细复核 明细证据' })).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.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();
expect(screen.getByRole('button', { name: '客户交付清单 质量提示 复制清单' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制交付封面包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户数据交付封面包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付主题:VIN-FIELDS-001 / GB32960 / 2026-07-03 至 2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 里程统计链接 / 质量提示'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前证据:位置 0 条 / 有效坐标 0 点 / 明细 1 帧 / 字段 2 个'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:优先导出字段证据,并补查轨迹位置。'));
fireEvent.click(screen.getByRole('button', { name: /复制导出向导包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户导出向导包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤1 锁定范围:VIN-FIELDS-001 / GB32960 / 2026-07-03 至 2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤2 选择证据:字段证据'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤3 质量复核:可交付'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤4 导出交付:复制交付包'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('字段裁剪:http://localhost:3000/#/history-query?keyword=VIN-FIELDS-001&protocol=GB32960&tab=fields&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true&fields=gb32960.vehicle.speed_kmh'));
fireEvent.click(screen.getByRole('button', { name: /复制交付清单/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹报告 / 位置历史 / 里程统计 / 明细证据 / 字段裁剪 / 质量提示'));
});
fireEvent.click(screen.getByRole('button', { name: '报表交付模板 周期交付计划 复制计划' }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【历史数据周期交付计划】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('推荐频率:日报用于客户复盘,周报用于客户对账,临时导出用于问题解释。'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付内容:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 里程统计链接 / 质量提示。'));
fireEvent.click(screen.getByRole('button', { name: '保存报表视图 当前筛选 VIN-FIELDS-001 保存视图' }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【保存报表视图】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('视图范围:VIN-FIELDS-001 / GB32960 / 2026-07-03 至 2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('字段模板:gb32960.vehicle.speed_kmh, gb32960.vehicle.soc_percent'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('复用链接:http://localhost:3000/#/history-query?keyword=VIN-FIELDS-001&protocol=GB32960&tab=fields&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true&fields=gb32960.vehicle.speed_kmh%2Cgb32960.vehicle.soc_percent'));
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?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 screen.findByRole('heading', { name: '轨迹回放' });
fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号'), { target: { value: '粤AG18312' } });
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.change(screen.getByPlaceholderText('2026-07-03 00:00:00'), { target: { value: '' } });
fireEvent.change(screen.getByPlaceholderText('2026-07-03 23:59:59'), { target: { value: '' } });
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);
const unscopedRawQueries = fetchMock.mock.calls.filter(([url, init]) => {
if (url !== '/api/history/raw-frames/query') return false;
const body = JSON.parse(String((init as RequestInit | undefined)?.body ?? '{}')) as Record;
return !body.keyword && !body.dateFrom && !body.dateTo && !body.fields;
});
expect(unscopedRawQueries).toHaveLength(0);
});
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.getAllByText('历史明细').length).toBeGreaterThan(0);
expect(screen.getAllByText('字段裁剪').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('客户轨迹影响')).toBeInTheDocument();
expect(screen.getByText('轨迹范围')).toBeInTheDocument();
expect(screen.getAllByText('定位覆盖').length).toBeGreaterThan(0);
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.getAllByText('最高速度').length).toBeGreaterThan(0);
expect(screen.getAllByText('42 km/h').length).toBeGreaterThan(0);
expect(screen.getAllByText('起点').length).toBeGreaterThan(0);
expect(screen.getAllByText('终点').length).toBeGreaterThan(0);
});
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.getAllByText('回放跨度').length).toBeGreaterThan(0);
expect(screen.getAllByText('10 分钟').length).toBeGreaterThan(0);
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.getAllByRole('button', { name: '解析地址' })[0]);
await waitFor(() => {
expect(screen.getAllByText('广东省广州市天河区测试路').length).toBeGreaterThan(0);
});
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.getAllByRole('button', { name: '下一点' })[0]);
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.getAllByRole('button', { name: '上一点' })[0]);
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();
const playTrajectoryButtons = screen.getAllByRole('button', { name: '播放轨迹' });
fireEvent.click(playTrajectoryButtons[playTrajectoryButtons.length - 1]);
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.getAllByRole('button', { name: '播放轨迹' }).length).toBeGreaterThan(0);
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.findAllByText('VIN-EXPORT-LOC')).length).toBeGreaterThan(0);
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.getAllByText('数据通道:GB32960').length).toBeGreaterThan(0);
expect(screen.getByText('里程对账结论')).toBeInTheDocument();
expect(screen.getByText('先回答客户这段时间跑了多少、闭合是否通过、异常是否需要复核,再进入轨迹、历史和导出。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程对账结论 区间总里程 12.3 km 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程对账结论 闭合状态 分页抽样 发布审计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程对账结论 异常记录 0 条 复制决策' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程对账结论 下一步 复核后交付 查看证据' })).toBeInTheDocument();
expect(screen.getByText('客户里程问题')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程问题 这段时间总共跑了多少? 复制摘要' })).toBeInTheDocument();
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();
});
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.getAllByText('数据通道:JT808').length).toBeGreaterThan(0);
});
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.getAllByText('数据通道:JT808').length).toBeGreaterThan(0);
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.findAllByText('数据通道:GB32960')).length).toBeGreaterThan(0);
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.findAllByText('VIN-MILEAGE-ROW')).length).toBeGreaterThan(0);
const serviceButtons = screen.getAllByRole('button', { name: '车辆服务' });
fireEvent.click(serviceButtons[serviceButtons.length - 1]);
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.findAllByText('VIN-MILEAGE-TRACK')).length).toBeGreaterThan(0);
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.findAllByText('VIN-MILEAGE-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-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.findAllByText('VIN-MILEAGE-ANOMALY')).length).toBeGreaterThan(0);
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('历史明细: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.getByText('客户里程交付台')).toBeInTheDocument();
expect(screen.getByText('按自定义时间范围交付里程、闭合校验、异常复核和导出证据。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付台 交付状态 复核后交付 复制交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付台 区间里程 140 km 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付台 闭合校验 闭合通过 发布审计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付台 异常复核 1 条 轨迹复核' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付台 数据导出 4 条明细 导出明细' })).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.getAllByText('明细合计').length).toBeGreaterThanOrEqual(1);
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.getByRole('button', { name: '闭合复核原始' })).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.getByText('客户里程对账总控')).toBeInTheDocument();
expect(screen.getByText('把区间里程、每日闭合、轨迹证明、异常通知和报表导出收敛成客户可验收的对账链路。')).toBeInTheDocument();
expect(screen.getByText('里程报表交付旅程')).toBeInTheDocument();
expect(screen.getByText('按客户报表习惯组织:先确认报表周期和车辆范围,再检查区间闭合、轨迹证明,最后导出或暂缓发布。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程报表交付旅程 1 报表周期 全部日期' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程报表交付旅程 2 区间闭合 分页抽样' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程报表交付旅程 3 轨迹证明 粤A统计服务2' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程报表交付旅程 4 导出发布 复核后交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制报表旅程包/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账总控 区间里程 210 km 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账总控 每日闭合 分页抽样 闭合审计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账总控 轨迹证明 2026-07-02 轨迹复核' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账总控 报表导出 3 条 导出报表' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制对账总控包/ })).toBeInTheDocument();
expect(screen.getByText('客户里程结论条')).toBeInTheDocument();
expect(screen.getByText('先给客户一个可交付结论,再进入轨迹复核、明细导出和告警通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程结论 区间累计 210 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程结论 明细合计 170 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程结论 异常记录 1 条' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程结论 在线影响 1 离线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程动作 复制结论 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程动作 轨迹复核 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程动作 导出明细 导出CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程动作 告警通知 在线态势' })).toBeInTheDocument();
expect(screen.getByText('客户里程口径交付条')).toBeInTheDocument();
expect(screen.getByText('日里程按当天首末总里程差值计算;区间总值必须等于每日里程之和,异常日进入轨迹和历史明细复核。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径交付 区间总值 210 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径交付 明细合计 170 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径交付 闭合差值 需全量' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径交付 异常记录 1 条' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径动作 复制口径 决策说明' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径动作 复制交付 交付包' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径动作 轨迹复核 轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程口径动作 导出明细 CSV' })).toBeInTheDocument();
expect(screen.getByText('客户里程统计工作台')).toBeInTheDocument();
expect(screen.getByText('统计页先回答客户这段时间跑了多少、每日是否闭合、异常在哪里、能否导出。')).toBeInTheDocument();
expect(screen.getByText('来源证据只用于统计可信度:JT808')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程统计工作台 区间里程 210 km 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程统计工作台 每日闭合 分页抽样 发布审计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程统计工作台 轨迹回放 2026-07-02 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程统计工作台 历史导出 3 条 导出明细' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程统计工作台 告警通知 1 离线 在线态势' })).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();
expect(screen.getByText('客户里程对账台')).toBeInTheDocument();
expect(screen.getByText('用同一辆车和同一时间窗完成区间里程、日报闭合、轨迹证据、告警通知和报表导出。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账台 区间里程 210 km 复制摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账台 日报闭合 分页抽样 闭合审计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账台 轨迹证据 2026-07-02 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账台 告警通知 1 离线 在线态势' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程对账台 报表导出 3 条 导出CSV' })).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: '客户里程交付导航 导出明细证据 导出CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户里程交付导航 发送交付结论 复制交付包' })).toBeInTheDocument();
expect(screen.getByText('里程客户验收清单')).toBeInTheDocument();
expect(screen.getByText('客户交付前只看五个门禁:范围、闭合、覆盖、异常、在线影响;任何一项不通过都能直接进入复核动作。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程客户验收 范围确认 VIN-STATS-SERVICE' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程客户验收 区间闭合 分页抽样' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程客户验收 明细覆盖 50%' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程客户验收 异常复核 1 条' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程客户验收 在线影响 1 离线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程验收动作 复制验收 交付说明' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程验收动作 复核轨迹 轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '里程验收动作 导出明细 CSV' })).toBeInTheDocument();
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('先判断能不能交付,再进入轨迹、历史明细和导出复核')).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('面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与历史明细复核。')).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('里程页不再让客户先理解 GB32960、JT808、MQTT 的差异,而是直接给出交付、轨迹回放、历史查询、导出和在线影响的下一步动作。')).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.getAllByText('来源覆盖').length).toBeGreaterThanOrEqual(1);
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('业务影响:当前统计范围需要先复核轨迹、历史明细和离线车辆影响'));
fireEvent.click(screen.getByRole('button', { name: /复制对账总控包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程对账总控包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务链路:区间里程 -> 每日闭合 -> 轨迹证明 -> 报表导出'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('对账范围:VIN-STATS-SERVICE / JT808 / 全部日期'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程:210 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('闭合状态:分页抽样'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('报表导出:http://localhost:3000/#/mileage?keyword=VIN-STATS-SERVICE&protocol=JT808'));
fireEvent.click(screen.getByRole('button', { name: /复制报表旅程包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程报表交付旅程包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('报表周期:全部日期'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间闭合:分页抽样'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证明:粤A统计服务2 / 2026-07-02 / 80 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('导出发布:复核后交付 / 复核后发布'));
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. 离线车辆优先打开车辆服务,核对实时、轨迹、历史明细和告警记录'));
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('发布风险:存在分页未全量、异常记录或闭合差值,需要先复核轨迹和历史明细'));
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.findAllByText('VIN-MILEAGE-EXPORT')).length).toBeGreaterThan(0);
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();
const locationRow = screen.getByText('VIN-JT808-001').closest('tr');
expect(locationRow).not.toBeNull();
fireEvent.click(within(locationRow!).getByRole('button', { name: '车辆服务' }));
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.findAllByText('VIN-HISTORY-MILEAGE')).length).toBeGreaterThan(0);
const locationRow = screen.getByRole('row', { name: /VIN-HISTORY-MILEAGE/ });
fireEvent.click(within(locationRow).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();
const rawRow = screen.getByText('raw-gb32960-001').closest('tr');
expect(rawRow).not.toBeNull();
fireEvent.click(within(rawRow!).getByRole('button', { name: '车辆服务' }));
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);
const rawRow = screen.getByRole('row', { name: /raw-jt808-mileage-001/ });
fireEvent.click(within(rawRow).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.findAllByText('来源证据:JT808')).length).toBeGreaterThan(0);
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('realtime page prioritizes customer vehicle monitoring command center', async () => {
window.history.replaceState(null, '', '/#/realtime');
vi.setSystemTime(new Date('2026-07-03T20:15:00+08:00'));
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,
platformRelease: 'platform-realtime-command-test',
amapWebJsConfigured: true,
amapApiConfigured: true,
amapSecurityProxyEnabled: true,
amapSecurityCodeExposed: false,
amapSecurityServiceHost: '/_AMapService'
}
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/realtime/vehicles')) {
return {
ok: true,
json: async () => ({
data: {
items: [
{
vin: 'VIN-RT-CUSTOMER-001',
plate: '粤A实时1',
phone: '13300000001',
oem: 'G7s',
protocols: ['JT808', 'GB32960'],
sourceStatus: [
{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:14:30', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true },
{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:14:25', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }
],
sourceCount: 2,
onlineSourceCount: 2,
online: true,
primaryProtocol: 'JT808',
longitude: 113.2,
latitude: 23.1,
speedKmh: 36,
socPercent: 78,
totalMileageKm: 119925,
lastSeen: '2026-07-03 20:14:30',
bindingStatus: 'bound',
serviceStatus: {
status: 'healthy',
severity: 'ok',
title: '服务正常',
detail: '车辆实时服务正常',
sourceCount: 2,
onlineSourceCount: 2
}
},
{
vin: 'VIN-RT-CUSTOMER-002',
plate: '粤A实时2',
phone: '13300000002',
oem: 'G7s',
protocols: ['JT808'],
sourceStatus: [
{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 19:40:00', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true },
{ protocol: 'GB32960', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false }
],
sourceCount: 2,
onlineSourceCount: 1,
online: true,
primaryProtocol: 'JT808',
longitude: 0,
latitude: 0,
speedKmh: 0,
socPercent: 0,
totalMileageKm: 8899,
lastSeen: '2026-07-03 19:40:00',
bindingStatus: 'bound',
serviceStatus: {
status: 'degraded',
severity: 'warning',
title: '来源不完整',
detail: '缺少 GB32960',
sourceCount: 2,
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();
expect(await screen.findByText('实时车辆服务总览')).toBeInTheDocument();
expect(screen.getByText('客户先看车辆是否在线、地图是否可见、哪些车辆需要处置,再进入轨迹、统计、导出和告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时车辆服务总览 在线车辆 2 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时车辆服务总览 有效定位 1 辆' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时车辆服务总览 需关注 1 辆' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时车辆服务总览 地图能力 高德可用' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时服务动作 打开地图 地图态势' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时服务动作 轨迹复盘 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时服务动作 导出清单 导出CSV' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时服务动作 告警通知 关注车辆' })).toBeInTheDocument();
expect(await screen.findByText('客户实时监控总控台')).toBeInTheDocument();
expect(screen.getByText('实时页先回答客户能不能看到车、车辆在哪里、哪些车辆异常,再进入轨迹、统计、导出和告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 实时地图 1 有定位 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 在线车辆 2 在线 只看在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 异常车辆 1 关注 关注异常' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 轨迹复盘 粤A实时1 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户实时监控总控台 导出交付 2 当前页 导出CSV' })).toBeInTheDocument();
expect(screen.getByText('来源证据仅用于可信度判断:JT808 / GB32960')).toBeInTheDocument();
expect(screen.getByText('实时处置优先级')).toBeInTheDocument();
expect(screen.getByText('按客户影响排序:先处理不能看车、定位无效、数据通道不完整和更新超时的车辆。')).toBeInTheDocument();
expect(screen.getAllByText('粤A实时2').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('坐标无效')).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('缺少 GB32960')).length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('button', { name: '实时处置优先级 1 粤A实时2 查看告警' })).toBeInTheDocument();
});
test('realtime page treats protocol filters as source evidence for vehicle service', 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-EVIDENCE',
plate: '粤A证据1',
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:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }
],
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.getByText('全部来源证据')).toBeInTheDocument();
expect(screen.getByText('车辆来源证据覆盖')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '筛选来源证据 GB32960' })).toBeInTheDocument();
});
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.getAllByText('高德地图待配置').length).toBeGreaterThan(0);
expect(screen.getByText('客户实时监控')).toBeInTheDocument();
expect(screen.getByText('从实时列表直接完成车辆监控闭环')).toBeInTheDocument();
expect(screen.getByText('实时地图调度台')).toBeInTheDocument();
expect(screen.getByText('像现代车队 Live Map 一样,把地图作为调度指挥台:先看在线位置,再处理轨迹、告警和导出。')).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: '实时地图调度台 导出当前态势 导出CSV' })).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();
expect(screen.getByRole('button', { name: '车辆监控建议 复盘选中车辆 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆监控建议 导出当前实时清单 导出 CSV' })).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: '实时监控路径 查询导出 导出 CSV' })).toBeInTheDocument();
expect(screen.getByText('实时单车服务快照')).toBeInTheDocument();
expect(screen.getByText('客户打开实时监控时,直接围绕选中车辆查看在线、位置、里程、新鲜度,并进入轨迹、统计、历史导出和告警通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时单车服务快照 车辆服务 粤ART002 车辆档案' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时单车服务快照 轨迹回放 更新超时 打开轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时单车服务快照 里程统计 119925 km 查里程' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时单车服务快照 历史导出 GB32960 导出证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '实时单车服务快照 告警通知 服务正常 查看告警' })).toBeInTheDocument();
expect(screen.getAllByText('定位有效').length).toBeGreaterThan(0);
expect(screen.getByText('车辆核心数据')).toBeInTheDocument();
expect(screen.getByText('实时来源证据')).toBeInTheDocument();
expect(screen.getByText('来源覆盖')).toBeInTheDocument();
expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0);
expect(screen.getByText('GB32960 在线')).toBeInTheDocument();
expect(screen.getByText('JT808 在线')).toBeInTheDocument();
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
expect(screen.getByText('车辆监控影响')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制影响报告/ })).toBeInTheDocument();
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('当前页 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('地图能力:高德地图未配置,使用坐标预览'));
expect(screen.getByText('自定义时间窗复盘')).toBeInTheDocument();
expect(screen.getByText('按车辆和时间窗复盘发生了什么')).toBeInTheDocument();
expect(screen.getByText('时间窗监控任务台')).toBeInTheDocument();
expect(screen.getByText('把同一辆车、同一时间窗贯穿到轨迹、里程、历史导出和告警说明,避免客户跨页面重复筛选。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控任务台 1 锁定车辆 粤ART002 已锁定' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控任务台 2 轨迹复盘 1 天窗口 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控任务台 3 里程核对 119925 km 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控任务台 4 历史导出 GB32960 导出证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控任务台 5 告警闭环 服务正常 告警复盘' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘 轨迹回放 打开轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗复盘 历史数据 查询历史' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制时间窗包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史数据:http://localhost:3000/#/history-query?keyword=VIN-RT-SERVICE&protocol=GB32960'));
fireEvent.click(screen.getByRole('button', { name: '时间窗复盘 历史数据 查询历史' }));
await waitFor(() => {
const params = new URLSearchParams(window.location.hash.split('?')[1] ?? '');
expect(window.location.hash).toContain('#/history-query?');
expect(params.get('keyword')).toBe('VIN-RT-SERVICE');
expect(params.get('protocol')).toBe('GB32960');
expect(params.get('dateFrom')).toBeTruthy();
expect(params.get('dateTo')).toBeTruthy();
});
});
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;
}
if (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/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/alert-events')) {
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: { 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.getByText('车辆服务地图中枢')).toBeInTheDocument();
expect(screen.getByText('把 32960、808、MQTT 收敛成同一张车辆地图:看位置、判在线、选车辆、回放轨迹、处理告警和导出证据。')).toBeInTheDocument();
expect(screen.getByText('高德地图底座')).toBeInTheDocument();
expect(screen.getByText('来源证据仅用于筛选和追溯,不作为客户主路径。')).toBeInTheDocument();
expect(screen.getByText('客户车辆地图总控')).toBeInTheDocument();
expect(screen.getByText('把实时找车、轨迹回放、围栏告警和报表导出收敛成一条客户能直接使用的地图服务链路。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆地图总控 实时找车 1 在线 打开地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆地图总控 轨迹回放 粤A超时1 回放轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆地图总控 围栏告警 1 关注 告警通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户车辆地图总控 报表导出 2 当前页 导出报表' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制地图总控包/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务地图任务 实时监控 1 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务地图任务 轨迹回放 粤A超时1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务地图任务 历史查询 JT808' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务地图任务 告警通知 1 关注' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务地图任务 统计导出 2 辆' })).toBeInTheDocument();
expect(screen.getByText('地图状态筛选条')).toBeInTheDocument();
expect(screen.getByText('先按客户最关心的在线、离线、无坐标和需关注分组看车,再进入轨迹、统计、导出或告警。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图状态筛选条 在线车辆 1 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图状态筛选条 离线车辆 1 离线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图状态筛选条 无坐标车辆 0 无坐标' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图状态筛选条 需关注车辆 1 关注' })).toBeInTheDocument();
expect(screen.getByText('地图图层控制台')).toBeInTheDocument();
expect(screen.getByText('把实时地图拆成客户能理解的图层:在线、关注、定位、选车服务和交付导出。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图图层控制台 在线车辆 1 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图图层控制台 关注车辆 1 关注' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图图层控制台 定位车辆 2 定位' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '地图图层控制台 选中车辆 粤A超时1' })).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.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.getByRole('button', { name: '车队服务旅程条 1 实时找车 1 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 2 轨迹复盘 粤A超时1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 3 里程核对 1000 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 4 导出通知 1 关注' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制旅程交付包/ })).toBeInTheDocument();
expect(screen.getByText('Live Map 决策条')).toBeInTheDocument();
expect(screen.getByText('客户进来先判断四件事:哪些车在线、哪些车能定位、哪辆车要先处理、选中车辆下一步做什么。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策条 在线覆盖 1 / 2' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策条 定位覆盖 2 / 2' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策条 优先处理 1 关注' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策条 当前选车 粤A超时1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策动作 只看在线 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策动作 定位关注 关注车' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策动作 轨迹复盘 轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策动作 导出地图 CSV' })).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('车辆:粤A超时1 / VIN-STALE-001 / JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题:'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('天未更新;JT808 离线'));
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 / 总计 2'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务链路:实时找车 -> 轨迹回放 -> 围栏告警 -> 报表导出'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时地图:http://localhost:3000/#/map?'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('选中车辆:粤A超时1 / VIN-STALE-001 / JT808'));
fireEvent.click(screen.getByRole('button', { name: /复制旅程交付包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车队服务旅程交付包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时找车:1 在线 / 2 当前页 / 2 总计'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹复盘:粤A超时1 / VIN-STALE-001 / JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程核对:1000 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('导出通知:1 关注 / 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'));
expect(screen.getByText('自定义时间窗复盘')).toBeInTheDocument();
expect(screen.getByText('按车辆和时间窗复盘发生了什么')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制时间窗包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警通知:http://localhost:3000/#/alert-events?keyword=VIN-STALE-001&protocol=JT808'));
fireEvent.click(screen.getByRole('button', { name: '时间窗复盘 告警通知 查看告警' }));
await waitFor(() => {
const params = new URLSearchParams(window.location.hash.split('?')[1] ?? '');
expect(window.location.hash).toContain('#/alert-events?');
expect(params.get('keyword')).toBe('VIN-STALE-001');
expect(params.get('protocol')).toBe('JT808');
expect(params.get('dateFrom')).toBeTruthy();
expect(params.get('dateTo')).toBeTruthy();
});
});
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.startsWith('#/history?')).toBe(true);
const historyParams = new URLSearchParams(window.location.hash.slice('#/history?'.length));
expect(historyParams.get('keyword')).toBe('VIN-MAP-LINK');
expect(historyParams.get('protocol')).toBe('GB32960');
expect(historyParams.get('dateFrom')).toBeTruthy();
expect(historyParams.get('dateTo')).toBeTruthy();
});
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');
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/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();
expect(screen.getByText('客户告警通知总控')).toBeInTheDocument();
expect(screen.getByText('把影响车辆、通知对象、SLA升级、证据闭环和恢复验收收敛成客户可执行的告警服务链路。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警通知总控 影响车辆 1 辆 复制影响' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警通知总控 通知对象 接入运维 + 业务责任人 复制通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警通知总控 SLA升级 30 分钟升级 通知闭环' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警通知总控 恢复验收 有标准 复制回执' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制告警总控包/ })).toBeInTheDocument();
expect(screen.getByText('告警处置结论栏')).toBeInTheDocument();
expect(screen.getByText('先判断客户受影响车辆,再确认通知、SLA、证据和恢复回执,避免告警停留在技术表格里。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '告警处置结论栏 影响范围 1 辆 复制影响' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '告警处置结论栏 通知策略 P0 1 / P1 0 复制通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '告警处置结论栏 SLA策略 立即处置 通知闭环' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '告警处置结论栏 证据闭环 可复核 复制交接' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '告警处置结论栏 恢复回执 有标准 复制回执' })).toBeInTheDocument();
expect(screen.getByText('客户告警服务台')).toBeInTheDocument();
expect(screen.getByText('客户先看到哪些车受影响、是否要通知、证据是否齐全、恢复后如何验收。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 影响车辆 1 辆 复制影响' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 待通知 P0 1 / P1 0 复制通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 焦点车辆 粤AG18312 / 13307795425 车辆服务' })).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('【客户告警通知总控包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务链路:影响车辆 -> 通知对象 -> SLA升级 -> 证据闭环 -> 恢复验收'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响范围:1 辆 / 1 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('优先级:P0 1 / P1 0'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略:P0 实时中断 / 接入运维 + 业务责任人 / 站内告警 / 邮件 / 企业微信 / 30 分钟升级'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('焦点车辆:粤AG18312 / 13307795425'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通知闭环:http://localhost:3000/#/notification-rules'));
fireEvent.click(screen.getAllByRole('button', { name: '实时状态' })[0]);
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.getAllByText('数据通道:JT808').length).toBeGreaterThan(0);
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?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: [], 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.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.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.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.getAllByText('里程复核').length).toBeGreaterThan(0);
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('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 data 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('单车 Live View')).toBeInTheDocument();
expect(screen.getByText('客户打开一辆车时,先看当前位置、最后上报、速度、SOC、总里程和告警,再进入轨迹回放、里程统计、数据导出和告警闭环。')).toBeInTheDocument();
expect(screen.getByText('客户单车操作台')).toBeInTheDocument();
expect(screen.getByText('围绕这辆车直接完成看车、回放、统计、导出和告警通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户单车操作台 看当前位置 113.2, 23.1 实时地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户单车操作台 回放轨迹 12 条 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户单车操作台 核对里程 3 条 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户单车操作台 导出证据 34 条 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户单车操作台 告警通知 1 项 告警闭环' })).toBeInTheDocument();
expect(screen.getByText('当前位置')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车实时视图 实时位置 查看实时' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车实时视图 数据导出 历史导出' })).toBeInTheDocument();
expect(screen.getByText('客户车辆档案摘要')).toBeInTheDocument();
expect(screen.getByText('把 VIN、车牌、在线状态、实时位置、轨迹、历史数据、告警和统计放在同一个单车入口,协议来源只作为可追溯证据。')).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();
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();
expect(screen.getByRole('button', { name: '单车客户常问 是否需要通知处理? 告警通知' })).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();
expect(screen.getAllByText('单车时间窗交付').length).toBeGreaterThan(0);
expect(screen.getByText('把同一辆车和同一段时间带入轨迹回放、里程统计、历史导出和告警通知,客户复盘时不需要跨页面重复输入。')).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');
const customerPackageButtons = screen.getAllByRole('button', { name: /复制交付包/ });
fireEvent.click(customerPackageButtons[customerPackageButtons.length - 1]);
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');
fireEvent.change(screen.getByPlaceholderText('单车开始日期'), { target: { value: '2026-07-01' } });
fireEvent.change(screen.getByPlaceholderText('单车结束日期'), { target: { value: '2026-07-03' } });
fireEvent.click(screen.getByRole('button', { name: '单车时间窗交付 历史数据导出 2026-07-01 至 2026-07-03' }));
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
const windowParams = new URLSearchParams(window.location.hash.split('?')[1] ?? '');
expect(windowParams.get('keyword')).toBe('VIN-SUMMARY-001');
expect(windowParams.get('protocol')).toBe('JT808');
expect(windowParams.get('dateFrom')).toBe('2026-07-01');
expect(windowParams.get('dateTo')).toBe('2026-07-03');
expect(windowParams.get('tab')).toBe('raw');
expect(windowParams.get('includeFields')).toBe('true');
});
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 data channel', 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.getAllByText('2/3 在线').length).toBeGreaterThan(0);
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);
});