feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -10,6 +10,42 @@ import { ROUTER_FUTURE } from '../routing/routerConfig';
|
||||
const mocks = vi.hoisted(() => ({ trackPlayback: vi.fn(), reverseGeocode: vi.fn(), vehicles: vi.fn() }));
|
||||
vi.mock('../../api/client', () => ({ api: mocks }));
|
||||
vi.mock('../map/TrackMap', () => ({ TrackMap: ({ activeIndex, followDurationMs, points }: { activeIndex: number; followDurationMs: number; points: unknown[] }) => <div data-testid="track-map" data-active-index={activeIndex} data-follow-duration={followDurationMs} data-point-count={points.length} /> }));
|
||||
vi.mock('@douyinfe/semi-ui', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@douyinfe/semi-ui')>();
|
||||
return {
|
||||
...actual,
|
||||
Slider: ({ value, onChange, min = 0, max = 100, disabled, ...props }: {
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
'aria-label'?: string;
|
||||
}) => <input
|
||||
type="range"
|
||||
aria-label={props['aria-label']}
|
||||
min={min}
|
||||
max={max}
|
||||
value={value ?? min}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange?.(Number(event.target.value))}
|
||||
/>,
|
||||
Select: ({ value, onChange, optionList = [], ...props }: {
|
||||
value?: string | number;
|
||||
onChange?: (value: string) => void;
|
||||
optionList?: Array<{ value: string | number; label: React.ReactNode }>;
|
||||
'aria-label'?: string;
|
||||
'aria-labelledby'?: string;
|
||||
}) => <select
|
||||
aria-label={props['aria-label']}
|
||||
aria-labelledby={props['aria-labelledby']}
|
||||
value={String(value ?? '')}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
>
|
||||
{optionList.map((option) => <option key={String(option.value)} value={String(option.value)}>{option.label}</option>)}
|
||||
</select>
|
||||
};
|
||||
});
|
||||
|
||||
const track = {
|
||||
vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z',
|
||||
@@ -51,10 +87,12 @@ test('preserves the exact monitor return after querying another track range', as
|
||||
viewport: { zoom: 14, bounds: '' }, listOffset: 0, listLimit: 50, hasViewport: true
|
||||
});
|
||||
const initialEntry = withMonitorReturn('/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59', monitorPath);
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={[initialEntry]}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={[initialEntry]}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect(view.container.querySelector('.v2-track-page')).toHaveClass('has-monitor-return');
|
||||
expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath);
|
||||
fireEvent.change(screen.getByLabelText('数据来源'), { target: { value: 'JT808' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '数据来源' }), { target: { value: 'JT808' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /查询轨迹/ }));
|
||||
|
||||
expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath);
|
||||
@@ -62,10 +100,14 @@ test('preserves the exact monitor return after querying another track range', as
|
||||
|
||||
test('keeps committed track criteria authoritative to same-route navigation and browser history', async () => {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackNavigationHarness /></MemoryRouter></QueryClientProvider>);
|
||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackNavigationHarness /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect(view.container.querySelector('.v2-track-page')).not.toHaveClass('has-monitor-return');
|
||||
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '3');
|
||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/07-15 00:00/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
|
||||
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空轨迹路由' }));
|
||||
@@ -76,7 +118,8 @@ test('keeps committed track criteria authoritative to same-route navigation and
|
||||
fireEvent.click(screen.getByRole('button', { name: '后退轨迹路由' }));
|
||||
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-point-count', '3');
|
||||
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
|
||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '前进轨迹路由' }));
|
||||
expect(await screen.findByText('先选择车辆,再开始轨迹回放')).toBeInTheDocument();
|
||||
@@ -98,33 +141,86 @@ test('distinguishes a failed vehicle lookup from an empty result and retries in
|
||||
expect(mocks.vehicles).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('merges duplicate vehicle sources and keeps the active date shortcut visible', async () => {
|
||||
mocks.vehicles.mockResolvedValue({
|
||||
items: [
|
||||
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808' },
|
||||
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'GB32960' }
|
||||
],
|
||||
total: 2,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect(screen.getByRole('button', { name: '今天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: '昨天' }));
|
||||
expect(screen.getByRole('button', { name: '昨天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
const search = screen.getByRole('textbox', { name: '搜索轨迹车辆' });
|
||||
fireEvent.focus(search);
|
||||
fireEvent.change(search, { target: { value: '粤A12345' } });
|
||||
const option = await screen.findByRole('option', { name: '粤A12345 LTEST000000000001 JT808 GB32960 选择' });
|
||||
|
||||
expect(document.querySelectorAll('.v2-track-vehicle-options [role="option"]')).toHaveLength(1);
|
||||
expect(option).toHaveTextContent('粤A12345LTEST000000000001JT808GB32960选择');
|
||||
});
|
||||
|
||||
test('renders a map-first replay workspace and connects stop, event, and panel interactions', async () => {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
||||
expect(view.container.querySelector('.v2-track-query-card.semi-card')).toHaveClass('is-collapsed');
|
||||
expect(view.container.querySelector('.v2-track-rail-result.semi-card')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '修改条件' })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-track-playback-dock.semi-card')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
|
||||
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /停留 00:05:00/ })).toHaveClass('semi-button', 'v2-track-list-action');
|
||||
expect(screen.getByRole('button', { name: /停留 00:05:00/ }).closest('.semi-list-item')).toHaveClass('v2-track-evidence-item');
|
||||
expect(view.container.querySelector('.v2-track-evidence-list.semi-list')).toBeInTheDocument();
|
||||
expect(screen.getByRole('slider', { name: '轨迹播放进度' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
|
||||
expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /事件点/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /急加速/ }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /事件点/ }));
|
||||
const acceleration = screen.getByRole('button', { name: /急加速/ });
|
||||
expect(acceleration).toHaveClass('semi-button', 'v2-track-list-action');
|
||||
expect(acceleration.closest('.semi-list-item')).toHaveClass('v2-track-evidence-item');
|
||||
fireEvent.click(acceleration);
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '1');
|
||||
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '概览' }));
|
||||
expect(view.container.querySelectorAll('.v2-track-overview-card.semi-card')).toHaveLength(3);
|
||||
expect(view.container.querySelector('.v2-track-overview-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-track-source-list.semi-list')).toBeInTheDocument();
|
||||
expect(view.container.querySelectorAll('.v2-track-source-item.semi-list-item')).toHaveLength(1);
|
||||
expect(screen.getByText('完整点集').closest('.semi-tag')).toBeInTheDocument();
|
||||
expect(screen.getByText('通过').closest('.semi-tag')).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '速度' }), { target: { value: '4' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始轨迹播放' }));
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '65');
|
||||
fireEvent.click(screen.getByRole('button', { name: '暂停轨迹播放' }));
|
||||
expect(screen.getByTestId('track-map')).toHaveAttribute('data-follow-duration', '180');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起查询面板' }));
|
||||
expect(screen.getByRole('button', { name: /查询与明细/ })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /查询与明细/ }));
|
||||
expect(screen.getByRole('button', { name: '收起查询面板' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改条件' }));
|
||||
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
|
||||
expect(screen.getByRole('button', { name: '收起条件' })).toHaveAttribute('aria-expanded', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起条件' }));
|
||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '隐藏查询与明细' }));
|
||||
expect(screen.getByRole('button', { name: '展开查询与明细' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开查询与明细' }));
|
||||
expect(screen.getByRole('button', { name: '隐藏查询与明细' })).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(mocks.trackPlayback).toHaveBeenCalledTimes(1));
|
||||
expect(mocks.trackPlayback.mock.calls[0][0].get('maxPoints')).toBe('1600');
|
||||
@@ -135,6 +231,24 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
|
||||
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(0));
|
||||
});
|
||||
|
||||
test('uses a Semi bottom SideSheet for mobile track criteria and evidence', async () => {
|
||||
Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() })) });
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect((await screen.findAllByText('粤A12345')).length).toBeGreaterThan(0);
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开查询与明细' }));
|
||||
|
||||
const sheet = await waitFor(() => {
|
||||
const element = document.querySelector('.v2-track-detail-sidesheet .semi-sidesheet-inner');
|
||||
expect(element).toHaveAttribute('aria-label', '轨迹查询与明细');
|
||||
return element;
|
||||
});
|
||||
expect(sheet).toHaveClass('semi-sidesheet-inner');
|
||||
expect(document.querySelector('.v2-track-detail-sidesheet .v2-track-rail')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '关闭轨迹查询与明细' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('coalesces rapid track scrubbing into one address lookup for the final point', async () => {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/tracks?keyword=LTEST000000000001&dateFrom=2026-07-15T00:00&dateTo=2026-07-15T23:59']}><TrackPage /></MemoryRouter></QueryClientProvider>);
|
||||
@@ -143,6 +257,7 @@ test('coalesces rapid track scrubbing into one address lookup for the final poin
|
||||
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
|
||||
|
||||
const progress = screen.getByRole('slider', { name: '轨迹播放进度' });
|
||||
expect(progress).toHaveAttribute('max', '2');
|
||||
fireEvent.change(progress, { target: { value: '1' } });
|
||||
fireEvent.change(progress, { target: { value: '2' } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user