polish operations quality workspace

This commit is contained in:
lingniu
2026-07-18 11:53:19 +08:00
parent 09e16e8d26
commit f423240e6f
4 changed files with 93 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { afterEach, expect, test, vi } from 'vitest';
import OperationsPage from './OperationsPage';
@@ -14,6 +15,15 @@ vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobil
afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); });
function LocationProbe() {
const location = useLocation();
return <output data-testid="operations-location">{location.pathname}{location.search}</output>;
}
function renderOperations(client: QueryClient, initialEntries = ['/operations']) {
return render(<MemoryRouter initialEntries={initialEntries}><QueryClientProvider client={client}><OperationsPage /><LocationProbe /></QueryClientProvider></MemoryRouter>);
}
function seedSession() {
mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] });
mocks.reconciliationSummary.mockResolvedValue({
@@ -56,7 +66,7 @@ test('renders reconciliation queue, loads evidence on demand and records review
version: 2
});
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client);
expect(await screen.findByText('质量问题队列')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '运行与数据质量', level: 5 })).toBeInTheDocument();
@@ -64,8 +74,8 @@ test('renders reconciliation queue, loads evidence on demand and records review
const navigation = screen.getByRole('navigation', { name: '运维质量视图' });
expect(navigation).toHaveClass('v2-ops-navigation');
expect(within(navigation).getByText('每日自动对账')).toBeInTheDocument();
expect(within(navigation).getByText('证据处置')).toBeInTheDocument();
expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button');
expect(await within(navigation).findByText('1 项待复核')).toBeInTheDocument();
expect(screen.getByRole('tab', { name: '差异处置 1', selected: true })).toHaveClass('semi-button');
expect(screen.getByRole('tabpanel', { name: '差异处置' })).toHaveAttribute('tabindex', '0');
expect(document.querySelector('.v2-ops-page')).toHaveClass('is-reconciliation');
expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument();
@@ -73,7 +83,8 @@ test('renders reconciliation queue, loads evidence on demand and records review
expect(screen.getByText('质量问题队列').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading');
expect(document.querySelector('.v2-reconcile-kpi-card small')?.textContent).toBe('待复核');
expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(4);
expect(screen.getByText('超过 24 小时').closest('.semi-card')).toHaveClass('v2-reconcile-sla');
expect(screen.getByText('SLA 超时').closest('.semi-card')).toHaveClass('v2-reconcile-kpi-card', 'is-sla', 'is-overdue');
expect(screen.queryByText('活跃问题', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
const trendButton = screen.getByRole('button', { name: '30 天趋势' });
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
@@ -125,7 +136,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar
});
mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client);
expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument();
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument();
@@ -173,9 +184,10 @@ test('reconciles service identities with bound and identity-required vehicles',
}]
});
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client, ['/operations?view=health']);
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
expect(screen.getByRole('tab', { name: '全局健康', selected: true })).toBeInTheDocument();
expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=health');
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('15 秒刷新运行证据')).toBeInTheDocument();
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('运行正常')).toBeInTheDocument();
@@ -207,6 +219,7 @@ test('reconciles service identities with bound and identity-required vehicles',
expect(screen.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card');
expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=diagnostic');
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('按需读取单车来源')).toBeInTheDocument();
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('精确诊断')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '诊断车辆', level: 5 })).toBeInTheDocument();
@@ -228,7 +241,7 @@ test('keeps health evidence visible when source readiness fails and retries that
kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: []
});
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client);
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
@@ -269,7 +282,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic);
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client);
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
const candidateButton = await waitFor(() => {
@@ -341,7 +354,7 @@ test('allows provider maintenance but keeps canonical source policy read only',
mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic);
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
renderOperations(client);
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '沪A' } });
const candidateButton = await waitFor(() => {