feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { HistoryDataResponse, HistorySeriesResponse } from '../../api/types';
|
||||
@@ -16,13 +16,16 @@ const mocks = vi.hoisted(() => ({
|
||||
downloadHistoryExport: vi.fn()
|
||||
}));
|
||||
const auth = vi.hoisted(() => ({ role: 'admin' }));
|
||||
const layout = vi.hoisted(() => ({ mobile: false }));
|
||||
|
||||
vi.mock('../../api/client', () => ({ api: mocks }));
|
||||
vi.mock('../auth/AuthGate', () => ({ usePlatformSession: () => ({ session: { role: auth.role } }) }));
|
||||
vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile }));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
auth.role = 'admin';
|
||||
layout.mobile = false;
|
||||
Object.values(mocks).forEach((mock) => mock.mockReset());
|
||||
});
|
||||
|
||||
@@ -83,9 +86,38 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
|
||||
? Promise.resolve(historySeries('OLDVIN', '旧车牌', 'old-as-of'))
|
||||
: new Promise<HistorySeriesResponse>((resolve) => { resolveNewSeries = resolve; }));
|
||||
|
||||
renderPage();
|
||||
const view = renderPage();
|
||||
expect((await screen.findAllByText('旧车牌')).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('OLDVIN-evidence')).not.toBeInTheDocument();
|
||||
for (const className of ['v2-history-filter-card', 'v2-history-metrics-card', 'v2-history-summary', 'v2-history-trend', 'v2-history-table-card']) {
|
||||
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
|
||||
}
|
||||
expect(view.container.querySelector('.v2-history-evidence')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-workspace')).not.toHaveClass('is-inspector-open');
|
||||
expect(screen.getByRole('heading', { level: 5, name: '聚合趋势' })).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-trend')).toHaveClass('is-collapsed');
|
||||
expect(screen.getByRole('button', { name: /展开趋势/ })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByRole('heading', { level: 5, name: '导出任务' })).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-data-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-table-scroll > table')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-mobile-list')).not.toBeInTheDocument();
|
||||
const desktopHistoryRow = screen.getByTestId('history-row-OLDVIN-row');
|
||||
expect(desktopHistoryRow).toHaveAttribute('role', 'button');
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.keyDown(desktopHistoryRow, { key: 'Enter' });
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await screen.findByText('OLDVIN-evidence')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-evidence-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-evidence-values.semi-descriptions')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-quality-tag.semi-tag')).toHaveTextContent('正常');
|
||||
expect(view.container.querySelector('.v2-history-workspace')).toHaveClass('is-inspector-open');
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭数据详情' }));
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(view.container.querySelector('.v2-history-workspace')).not.toHaveClass('is-inspector-open');
|
||||
fireEvent.click(desktopHistoryRow);
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: /展开趋势/ }));
|
||||
await waitFor(() => expect(mocks.historySeries).toHaveBeenCalledTimes(1));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('车牌 / VIN,多台用逗号分隔'), { target: { value: 'NEWVIN' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
||||
@@ -100,10 +132,54 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
|
||||
resolveNewSeries(historySeries('NEWVIN', '新车牌', 'new-as-of'));
|
||||
});
|
||||
expect((await screen.findAllByText('新车牌')).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('NEWVIN-evidence')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('history-row-NEWVIN-row'));
|
||||
expect(await screen.findByText('NEWVIN-evidence')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('renders only selectable Semi history cards on mobile', async () => {
|
||||
layout.mobile = true;
|
||||
mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] });
|
||||
mocks.historyData.mockResolvedValue(historyData('MOBILEVIN', '粤A移动历史', 'mobile-as-of'));
|
||||
mocks.historySeries.mockResolvedValue(historySeries('MOBILEVIN', '粤A移动历史', 'mobile-as-of'));
|
||||
mocks.historyExports.mockResolvedValue([]);
|
||||
const view = renderPage('/history?keywords=MOBILEVIN&dateFrom=2026-07-16T00%3A00&dateTo=2026-07-16T05%3A00');
|
||||
|
||||
const action = await screen.findByRole('button', { name: '查看 粤A移动历史 2026-07-16 04:00:00 数据详情' });
|
||||
expect(action).toHaveClass('semi-button', 'v2-history-mobile-action');
|
||||
expect(action.closest('.semi-card')).toHaveClass('v2-history-mobile-card');
|
||||
expect(view.container.querySelector('.v2-history-data-table')).not.toBeInTheDocument();
|
||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByText('MOBILEVIN-evidence')).not.toBeInTheDocument();
|
||||
expect((mocks.historyData.mock.calls[0]?.[0] as URLSearchParams).get('limit')).toBe('20');
|
||||
expect(screen.queryByLabelText('每页数量')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('表格密度')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-side')).not.toBeInTheDocument();
|
||||
expect(mocks.historyExports).not.toHaveBeenCalled();
|
||||
fireEvent.click(action);
|
||||
expect(action).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await screen.findByRole('dialog', { name: '历史数据详情' })).toBeInTheDocument();
|
||||
expect(await screen.findByText('MOBILEVIN-evidence')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据详情' }));
|
||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
expect(screen.queryByText('MOBILEVIN-evidence')).not.toBeInTheDocument();
|
||||
|
||||
const exportJobsButton = screen.getByRole('button', { name: '查看导出任务' });
|
||||
fireEvent.click(exportJobsButton);
|
||||
expect(await screen.findByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByText('暂无导出任务')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据导出任务' }));
|
||||
expect(exportJobsButton).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
expect(screen.queryByText('暂无导出任务')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('releases export job state immediately after leaving the history page', async () => {
|
||||
mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] });
|
||||
mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of'));
|
||||
@@ -111,6 +187,7 @@ test('releases export job state immediately after leaving the history page', asy
|
||||
mocks.historyExports.mockResolvedValue([{ id: 'export-running', name: '运行中导出', status: 'running', progress: 30, format: 'csv', category: 'location', keywords: ['OLDVIN'], rowCount: 0, totalRows: 100, processedRows: 30, fileSizeBytes: 0, createdAt: '2026-07-16T04:00:00Z', updatedAt: '2026-07-16T04:00:01Z', evidence: 'test export' }]);
|
||||
|
||||
const { client, unmount } = renderPage();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
|
||||
expect(await screen.findByText('运行中导出')).toBeInTheDocument();
|
||||
expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeDefined();
|
||||
|
||||
@@ -126,20 +203,30 @@ test('opens a working column visibility panel and preserves non-hideable identit
|
||||
mocks.historyData.mockResolvedValue(data);
|
||||
mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of'));
|
||||
mocks.historyExports.mockResolvedValue([]);
|
||||
renderPage();
|
||||
const view = renderPage();
|
||||
|
||||
expect(await screen.findByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '列设置' }));
|
||||
const metricScroll = view.container.querySelector('.v2-history-metric-scroll');
|
||||
const manageMetrics = screen.getByRole('button', { name: '管理字段' });
|
||||
expect(metricScroll).toBeInTheDocument();
|
||||
expect(metricScroll).toHaveAttribute('aria-label', '当前显示字段');
|
||||
expect(metricScroll?.contains(screen.getByLabelText(/已选字段 速度/))).toBe(true);
|
||||
expect(metricScroll?.contains(manageMetrics)).toBe(false);
|
||||
expect(manageMetrics.parentElement).toHaveClass('v2-history-metrics');
|
||||
const columnSettingsButton = screen.getByRole('button', { name: '列设置' });
|
||||
fireEvent.click(columnSettingsButton);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '列显示设置' })).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索字段名 / 字段 key'), { target: { value: 'soc' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索中文、状态含义或字段 key'), { target: { value: 'soc' } });
|
||||
expect(screen.getByRole('checkbox', { name: /SOC/ })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('checkbox', { name: /速度/ })).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索字段名 / 字段 key'), { target: { value: '' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索中文、状态含义或字段 key'), { target: { value: '' } });
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /速度/ }));
|
||||
expect(screen.queryByRole('columnheader', { name: '速度 (km/h)' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'VIN' })).toBeInTheDocument();
|
||||
const historyTable = document.querySelector<HTMLElement>('.v2-history-table-scroll table');
|
||||
expect(historyTable).toBeInTheDocument();
|
||||
expect(within(historyTable!).getByRole('columnheader', { name: 'VIN' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '显示全部' }));
|
||||
expect(screen.getByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument();
|
||||
@@ -148,7 +235,8 @@ test('opens a working column visibility panel and preserves non-hideable identit
|
||||
expect(screen.getByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭列显示设置' }));
|
||||
expect(screen.queryByRole('dialog', { name: '列显示设置' })).not.toBeInTheDocument();
|
||||
expect(columnSettingsButton).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
});
|
||||
|
||||
test('keeps history readable without mounting operator-only export requests for a viewer', async () => {
|
||||
@@ -156,10 +244,11 @@ test('keeps history readable without mounting operator-only export requests for
|
||||
mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] });
|
||||
mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of'));
|
||||
mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of'));
|
||||
renderPage();
|
||||
const view = renderPage();
|
||||
|
||||
expect((await screen.findAllByText('旧车牌')).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('只读 · 导出需操作员权限')).toBeInTheDocument();
|
||||
expect(screen.getByText('只读').closest('.semi-tag')).toHaveClass('v2-history-permission-tag');
|
||||
expect(view.container.querySelector('.v2-history-toolbar-actions')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /创建导出/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('导出任务')).not.toBeInTheDocument();
|
||||
expect(mocks.historyExports).not.toHaveBeenCalled();
|
||||
@@ -180,8 +269,9 @@ test('shows only the authenticated customer export workspace with owner and scop
|
||||
}]);
|
||||
renderPage();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
|
||||
expect(await screen.findByText(/customer-a · 1 辆/)).toBeInTheDocument();
|
||||
expect(screen.getByText('导出任务')).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /创建导出/ })).toBeInTheDocument();
|
||||
expect(mocks.historyExports).toHaveBeenCalled();
|
||||
});
|
||||
@@ -199,11 +289,13 @@ test('uses selected chartable fields for trends and omits empty RAW evidence UI'
|
||||
renderPage();
|
||||
|
||||
expect((await screen.findAllByText('旧车牌')).length).toBeGreaterThan(0);
|
||||
expect(mocks.historySeries).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: /展开趋势/ }));
|
||||
await waitFor(() => expect(mocks.historySeries).toHaveBeenCalled());
|
||||
expect((mocks.historySeries.mock.calls[mocks.historySeries.mock.calls.length - 1]?.[0] as URLSearchParams).get('metrics')).toBe('speedKmh,totalMileageKm');
|
||||
expect(screen.queryByText('RAW 证据')).not.toBeInTheDocument();
|
||||
|
||||
const mileageToggle = screen.getAllByTitle('点击取消显示').find((button) => button.textContent?.includes('总里程'));
|
||||
const mileageToggle = screen.getAllByLabelText(/已选字段 .*点击取消显示/).find((button) => button.textContent?.includes('总里程'));
|
||||
expect(mileageToggle).toBeDefined();
|
||||
fireEvent.click(mileageToggle!);
|
||||
await waitFor(() => expect((mocks.historySeries.mock.calls[mocks.historySeries.mock.calls.length - 1]?.[0] as URLSearchParams).get('metrics')).toBe('speedKmh'));
|
||||
|
||||
Reference in New Issue
Block a user