feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user