feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -1,4 +1,4 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -24,6 +24,7 @@ const vehicleCardArgsSpy = vi.hoisted(() => vi.fn());
const monitorDataArgsSpy = vi.hoisted(() => vi.fn());
const qrToDataURLSpy = vi.hoisted(() => vi.fn());
const monitorQueryFlags = vi.hoisted(() => ({ isLoading: false, isFetching: false, isPlaceholderData: false }));
const vehicleCardFixture = vi.hoisted(() => ({ detail: undefined as unknown }));
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
vi.mock('../map/FleetMap', () => ({
@@ -68,7 +69,7 @@ vi.mock('../hooks/useMonitorData', () => ({
},
useMonitorVehicleCard: (...args: unknown[]) => {
vehicleCardArgsSpy(...args);
return { detail: {}, activeAlerts: {}, address: {} };
return { detail: { data: vehicleCardFixture.detail }, activeAlerts: {}, address: {} };
}
}));
@@ -77,6 +78,7 @@ afterEach(() => {
monitorQueryFlags.isLoading = false;
monitorQueryFlags.isFetching = false;
monitorQueryFlags.isPlaceholderData = false;
vehicleCardFixture.detail = undefined;
vehicleCardArgsSpy.mockClear();
monitorDataArgsSpy.mockClear();
qrToDataURLSpy.mockReset();
@@ -85,6 +87,27 @@ afterEach(() => {
vi.restoreAllMocks();
});
test('never labels the latest historical mileage row as today when today has no mileage evidence', () => {
vehicleCardFixture.detail = {
sources: ['JT808'],
sourceStatus: [],
mileage: {
items: [{ vin: vehicles[1].vin, plate: vehicles[1].plate, date: '2026-07-16', startMileageKm: 2100, endMileageKm: 2234, dailyMileageKm: 134, source: 'JT808' }],
total: 1,
limit: 20,
offset: 0
}
};
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
fireEvent.click(screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ }));
const todayMetric = screen.getByText('今日里程').parentElement;
expect(todayMetric).toHaveTextContent('今日里程—');
expect(todayMetric).not.toHaveTextContent('134');
});
test('starts without a selection and supports expand, collapse, reselection, and clear', () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
@@ -92,13 +115,26 @@ test('starts without a selection and supports expand, collapse, reselection, and
const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ });
const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ });
expect(firstVehicle).toHaveClass('semi-button', 'v2-vehicle-row');
expect(view.container.querySelector('.v2-filterbar')).toHaveClass('semi-card');
const kpis = view.container.querySelector('.v2-kpis');
expect(kpis).toHaveClass('semi-card');
expect(Array.from(kpis?.querySelectorAll('.v2-kpi small') ?? []).map((item) => item.textContent)).toEqual([
'车辆总数', '当前在线', '当前离线', '行驶车辆', '静止车辆', '告警车辆', '今日上报', '无实时位置'
]);
expect(kpis?.querySelector('.v2-kpi.is-fleet')).toHaveClass('is-primary');
expect(kpis?.querySelector('.v2-kpi.is-online')).toHaveClass('is-primary');
expect(kpis?.querySelector('.v2-kpi.is-today')).toHaveClass('is-support');
expect(kpis?.querySelector('.v2-kpi:last-child')).toHaveClass('is-missing', 'is-support');
expect(view.container.querySelector('.v2-event-strip')).toHaveClass('semi-card');
expect(view.container.querySelector('.v2-monitor-live-tag')).toHaveClass('semi-tag');
expect(workspace).not.toHaveClass('is-detail-open');
expect(workspace).not.toHaveClass('is-detail-collapsed');
expect(firstVehicle).not.toHaveClass('is-selected');
expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument();
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', '');
expect(screen.getByText('车辆总数')).toBeInTheDocument();
expect(screen.getAllByText('无实时位置').length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('无实时位置')).toBeInTheDocument();
expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效' })).toHaveTextContent('实时筛选');
expect(screen.queryByRole('button', { name: '筛选' })).not.toBeInTheDocument();
expect(screen.queryByText('接入车辆')).not.toBeInTheDocument();
@@ -107,6 +143,18 @@ test('starts without a selection and supports expand, collapse, reselection, and
expect(workspace).toHaveClass('is-detail-open');
expect(firstVehicle).toHaveClass('is-selected');
expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-vehicle-detail')).toHaveClass('semi-card');
expect(screen.getByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument();
expect(screen.getByRole('navigation', { name: '车辆快捷操作' })).toBeInTheDocument();
for (const name of ['最新上报', '实时状态', '车辆信息']) {
expect(screen.getByRole('heading', { name, level: 5 })).toBeInTheDocument();
}
expect(screen.getByText('车辆最近一次有效实时数据')).toBeInTheDocument();
expect(screen.getByText('身份、车型与来源覆盖')).toBeInTheDocument();
expect(view.container.querySelector('.v2-status-text')).toHaveClass('semi-tag');
expect(screen.getByRole('button', { name: '取消选择车辆' })).toHaveClass('semi-button');
expect(screen.getByRole('button', { name: '查看总里程全部来源' })).toHaveClass('semi-button', 'v2-metric-source-link');
expect(screen.getByRole('button', { name: '查看全部位置来源' })).toHaveClass('semi-button', 'v2-detail-source-link');
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', 'LTEST000000000001');
expect(vehicleCardArgsSpy).toHaveBeenLastCalledWith('LTEST000000000001', vehicles[0], true);
expect(screen.getByText('SOC').parentElement).toHaveTextContent('SOC—');
@@ -117,6 +165,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
expect(workspace).toHaveClass('is-detail-collapsed');
expect(firstVehicle).toHaveClass('is-selected');
expect(screen.getByRole('button', { name: '展开车辆详情' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-detail-peek')).toHaveClass('semi-card');
expect(fleetMapRenderSpy).toHaveBeenCalledTimes(mapRendersAfterSelection);
expect(vehicleCardArgsSpy).toHaveBeenCalledTimes(cardCallsAfterSelection);
@@ -142,8 +191,8 @@ test('restores URL-backed monitor context and carries it into every vehicle work
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE} initialEntries={[entry]}><MonitorPage /></MemoryRouter></QueryClientProvider>);
expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue('粤A12345');
expect(screen.getByRole('combobox', { name: '协议' })).toHaveValue('JT808');
expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveValue('online');
expect(screen.getByRole('combobox', { name: '协议' })).toHaveTextContent('JT808');
expect(screen.getByRole('combobox', { name: '在线状态' })).toHaveTextContent('在线');
expect(screen.getByRole('button', { name: '展开车辆详情' })).toBeInTheDocument();
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-initial-zoom', '13');
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-initial-bounds', '113.100000,23.000000,113.400000,23.300000');
@@ -164,16 +213,35 @@ test('switches to a lightweight realtime list and resolves addresses only on dem
const reverseGeocode = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市天河区测试路' });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
fireEvent.click(screen.getByRole('button', { name: /列表/ }));
const modeGroup = screen.getByRole('group', { name: '监控视图' });
const mapMode = within(modeGroup).getByRole('button', { name: /地图/ });
const listMode = within(modeGroup).getByRole('button', { name: /列表/ });
expect(mapMode).toHaveAttribute('aria-pressed', 'true');
expect(mapMode).toHaveClass('is-active');
expect(listMode).toHaveAttribute('aria-pressed', 'false');
fireEvent.click(listMode);
expect(mapMode).toHaveAttribute('aria-pressed', 'false');
expect(listMode).toHaveAttribute('aria-pressed', 'true');
expect(listMode).toHaveClass('is-active');
expect(await screen.findByText('车辆实时列表')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '车辆实时列表', level: 5 })).toBeInTheDocument();
expect(view.container.querySelector('.v2-monitor-table-panel')).toHaveClass('semi-card');
expect(view.container.querySelector('.v2-monitor-table.semi-table-wrapper')).toBeInTheDocument();
expect(view.container.querySelector('.v2-monitor-table-scroll > table')).not.toBeInTheDocument();
expect(screen.getByText('覆盖全部授权车辆;实时字段缺失时显示“—”,文字地址按需解析')).toBeInTheDocument();
expect(await screen.findAllByText('粤A12345')).toHaveLength(1);
expect(screen.getAllByText('42')).toHaveLength(1);
expect(screen.getAllByText(/1,234/)).toHaveLength(1);
expect(screen.getAllByText(/18\.6/)).toHaveLength(1);
expect(screen.getAllByText(/1,234/)).toHaveLength(1);
expect(screen.getByText('JT808')).toBeInTheDocument();
expect(screen.getAllByText(/113\.260000/)).toHaveLength(1);
expect(screen.queryByText('推荐来源')).not.toBeInTheDocument();
expect(screen.getByText('当日里程')).toBeInTheDocument();
expect(screen.getByText('协议来源')).toBeInTheDocument();
expect(screen.queryByText('最新上报')).not.toBeInTheDocument();
expect(reverseGeocode).not.toHaveBeenCalled();
expect(screen.getByRole('button', { name: '解析粤A12345位置' })).toHaveClass('semi-button', 'v2-monitor-address-action');
fireEvent.click(screen.getByRole('button', { name: '解析粤A12345位置' }));
expect(await screen.findByText('广东省广州市天河区测试路')).toBeInTheDocument();
expect(screen.getByText(/解析于 \d{2}:\d{2}/)).toBeInTheDocument();
@@ -218,7 +286,6 @@ test('keeps authorized vehicles without realtime coordinates in the list without
expect(await screen.findByText('粤A无定位')).toBeInTheDocument();
expect(screen.getByText('暂无实时位置')).toBeInTheDocument();
expect(screen.getByText('从未上报')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /解析粤A无定位位置/ })).not.toBeInTheDocument();
expect(reverseGeocode).not.toHaveBeenCalled();
});
@@ -260,13 +327,15 @@ test('closes the mobile entry with Escape and reports clipboard success', async
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
fireEvent.click(screen.getByRole('button', { name: /手机端/ }));
expect(await screen.findByRole('dialog', { name: '手机端全局监控' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '关闭手机端入口' })).toBeInTheDocument();
expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制访问地址' }));
expect(await screen.findByRole('button', { name: '已复制访问地址' })).toBeInTheDocument();
expect(writeText).toHaveBeenCalledWith(expect.stringMatching(/\/monitor$/));
fireEvent.keyDown(window, { key: 'Escape' });
expect(screen.queryByRole('dialog', { name: '手机端入口' })).not.toBeInTheDocument();
fireEvent.keyDown(document.body, { key: 'Escape', code: 'Escape', keyCode: 27 });
await waitFor(() => expect(screen.queryByRole('dialog', { name: '手机端全局监控' })).not.toBeInTheDocument());
});
test('shows a retry action when on-demand QR generation fails', async () => {
@@ -301,7 +370,7 @@ test('mounts only the mobile list representation and removes its viewport listen
fireEvent.click(screen.getByRole('button', { name: /列表/ }));
expect(await screen.findByText('车辆实时列表')).toBeInTheDocument();
await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards article')).toHaveLength(1));
await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards .v2-monitor-mobile-card.semi-card')).toHaveLength(1));
expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument();
expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function));