feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -8,9 +8,11 @@ const mocks = vi.hoisted(() => ({
|
||||
vehicleSourceDiagnostic: vi.fn(), updateVehicleSourcePolicy: vi.fn(),
|
||||
reconciliationSummary: vi.fn(), reconciliationIssues: vi.fn(), reconciliationIssue: vi.fn(), updateReconciliationIssue: vi.fn()
|
||||
}));
|
||||
const layout = vi.hoisted(() => ({ mobile: false }));
|
||||
vi.mock('../../api/client', () => ({ api: mocks }));
|
||||
vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile }));
|
||||
|
||||
afterEach(() => { cleanup(); Object.values(mocks).forEach((mock) => mock.mockReset()); });
|
||||
afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); });
|
||||
|
||||
function seedSession() {
|
||||
mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] });
|
||||
@@ -41,7 +43,7 @@ function seedSession() {
|
||||
test('renders reconciliation queue, loads evidence on demand and records review conclusion', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
linkHealth: [{ name: 'MySQL', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
@@ -57,10 +59,42 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
|
||||
expect(await screen.findByText('数据差异中心')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByText('多来源实时位置漂移'));
|
||||
expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('tabpanel', { name: '差异处置' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('数据差异中心').closest('.semi-card')).toHaveClass('v2-reconcile-center');
|
||||
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.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
||||
const trendButton = screen.getByRole('button', { name: '30 天趋势' });
|
||||
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(trendButton);
|
||||
expect(screen.getByText('近 30 天趋势').closest('.semi-card')).toHaveClass('v2-reconcile-trend');
|
||||
expect(screen.getByText('近 30 天趋势').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
||||
expect(trendButton).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
const issueTitles = await screen.findAllByText('多来源实时位置漂移');
|
||||
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-table-wrap > table')).not.toBeInTheDocument();
|
||||
const reconcileRow = screen.getByTestId('reconcile-row-issue-1');
|
||||
expect(reconcileRow).toHaveAttribute('role', 'button');
|
||||
expect(reconcileRow).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.keyDown(reconcileRow, { key: ' ' });
|
||||
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
||||
expect(reconcileRow).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByText('规则证据').closest('.semi-card')).toHaveClass('v2-reconcile-evidence');
|
||||
expect(screen.getByText('复核结论').closest('.semi-card')).toHaveClass('v2-reconcile-review');
|
||||
expect(screen.getByText('处理履历').closest('.semi-card')).toHaveClass('v2-reconcile-actions');
|
||||
expect(screen.getByText('规则证据').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
||||
expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).toBeInTheDocument();
|
||||
expect(screen.getByText('1286')).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('处置状态'), { target: { value: 'confirmed_source_a' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' }));
|
||||
expect(reconcileRow).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(issueTitles[0]);
|
||||
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('radio', { name: '确认来源 A' }));
|
||||
fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存复核结论' }));
|
||||
await waitFor(() => expect(mocks.updateReconciliationIssue).toHaveBeenCalledWith('issue-1', {
|
||||
@@ -68,20 +102,65 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
||||
}));
|
||||
});
|
||||
|
||||
test('reconciles service identities with bound and identity-required vehicles', async () => {
|
||||
test('renders only the compact mobile reconciliation surface and defers secondary controls', async () => {
|
||||
layout.mobile = true;
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
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>);
|
||||
|
||||
expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).not.toBeInTheDocument();
|
||||
expect(mocks.reconciliationIssues).toHaveBeenCalledWith(expect.objectContaining({ limit: 20 }), expect.any(AbortSignal));
|
||||
const mobileIssueAction = screen.getByRole('button', { name: '查看 粤A00001 差异证据' });
|
||||
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(mobileIssueAction);
|
||||
expect(await screen.findByRole('dialog', { name: '差异证据与处置' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: '关闭差异详情' })).toBeInTheDocument();
|
||||
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' }));
|
||||
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
const toolbar = document.querySelector('.v2-reconcile-toolbar');
|
||||
expect(toolbar).toHaveClass('is-mobile-collapsed');
|
||||
fireEvent.click(screen.getByRole('button', { name: /修改筛选差异/ }));
|
||||
expect(toolbar).not.toHaveClass('is-mobile-collapsed');
|
||||
|
||||
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '30 天趋势' }));
|
||||
const trend = screen.getByText('近 30 天趋势').closest('.v2-reconcile-trend');
|
||||
expect(trend).toBeInTheDocument();
|
||||
expect(screen.getByText(/最近运行/)).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-trend-toggle')).toHaveAttribute('aria-expanded', 'true');
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起趋势' }));
|
||||
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('reconciles service identities with bound and identity-required vehicles', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [{ name: 'MySQL', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
mocks.sourceReadiness.mockResolvedValue({
|
||||
totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234,
|
||||
kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: []
|
||||
kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [{
|
||||
protocol: 'GB32960', role: '仪表盘里程与整车状态', total: 1024, online: 234, severity: 'ok',
|
||||
evidence: '实时来源覆盖稳定', action: '保持在线率监测', acceptance: '在线率与上报周期持续满足阈值'
|
||||
}]
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
|
||||
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
|
||||
for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) {
|
||||
const query = client.getQueryCache().find({ queryKey: key });
|
||||
@@ -91,6 +170,22 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
}
|
||||
expect(screen.getByText('服务身份 / 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
|
||||
expect(screen.getByText('服务身份 / 在线').closest('.semi-card')).toHaveClass('v2-ops-kpi-card');
|
||||
expect(screen.getByText('运行时安全').closest('.semi-card')).toHaveClass('v2-ops-runtime');
|
||||
expect(screen.getByText('协议来源就绪度').closest('.semi-card')).toHaveClass('v2-ops-sources');
|
||||
expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '运行时安全', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '协议来源就绪度', level: 5 })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-ops-link-card.semi-card')).toHaveTextContent('MySQL主库连接正常');
|
||||
expect(document.querySelector('.v2-ops-runtime-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(screen.getByText('容量检查通过').closest('.semi-empty')).toHaveClass('v2-ops-capacity-empty');
|
||||
expect(document.querySelector('.v2-ops-source-list.semi-card-group')).toBeInTheDocument();
|
||||
expect(screen.getByText('GB32960').closest('.semi-card')).toHaveClass('v2-ops-source-card');
|
||||
expect(screen.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card');
|
||||
expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
expect(screen.getByText('先选择一辆车').closest('.semi-empty')).toHaveClass('v2-source-empty');
|
||||
expect(screen.getByText('单车多来源诊断').closest('.semi-card')).toHaveClass('v2-source-diagnostic');
|
||||
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -108,8 +203,10 @@ test('keeps health evidence visible when source readiness fails and retries that
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
|
||||
expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-release')).toBeInTheDocument();
|
||||
expect(screen.getByText('版本 test-release')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /重试/ }));
|
||||
|
||||
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
|
||||
@@ -146,6 +243,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
|
||||
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
|
||||
const candidateButton = await waitFor(() => {
|
||||
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
|
||||
@@ -154,8 +252,21 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
|
||||
});
|
||||
fireEvent.click(candidateButton);
|
||||
expect(await screen.findByText('当前推荐 G7')).toBeInTheDocument();
|
||||
expect(document.querySelectorAll('.v2-source-summary-card.semi-card')).toHaveLength(4);
|
||||
expect(document.querySelectorAll('.v2-source-summary-card')[1]?.querySelector('.semi-tag')).toHaveTextContent('JT808');
|
||||
expect(screen.getByText('推荐说明').closest('.semi-card')).toHaveClass('v2-source-recommendation');
|
||||
expect(screen.getByText('最近策略审计').closest('.semi-card')).toHaveClass('v2-source-audit');
|
||||
expect(screen.getByText('终端 133****0001')).toBeInTheDocument();
|
||||
expect(screen.getByText('10s')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-table-wrap > table')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-mobile-list')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '来源 / 终端' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '运维策略' })).toBeInTheDocument();
|
||||
expect(screen.getByText('优先级 20')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '维护策略' }));
|
||||
expect(await screen.findByRole('dialog', { name: '车辆来源策略' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-policy-sidesheet .semi-sidesheet-inner')).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('G7 提供方'), { target: { value: 'G7s' } });
|
||||
const save = screen.getByRole('button', { name: '保存策略' });
|
||||
expect(save).toBeDisabled();
|
||||
@@ -175,6 +286,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
|
||||
});
|
||||
|
||||
test('allows provider maintenance but keeps canonical source policy read only', async () => {
|
||||
layout.mobile = true;
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
@@ -202,6 +314,7 @@ test('allows provider maintenance but keeps canonical source policy read only',
|
||||
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '沪A' } });
|
||||
const candidateButton = await waitFor(() => {
|
||||
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
|
||||
@@ -210,6 +323,14 @@ test('allows provider maintenance but keeps canonical source policy read only',
|
||||
});
|
||||
fireEvent.click(candidateButton);
|
||||
|
||||
expect(await screen.findByText('协议融合快照只能维护提供方')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-table')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-mobile-card.semi-card')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-source-mobile-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(screen.getByText('优先级 100')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('JT808 优先级')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '维护提供方' }));
|
||||
expect(await screen.findByRole('dialog', { name: '车辆来源策略' })).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('JT808 优先级')).toBeDisabled();
|
||||
expect(screen.getByLabelText('JT808 策略备注')).toBeDisabled();
|
||||
expect(screen.getByRole('checkbox', { name: '启用' })).toBeDisabled();
|
||||
|
||||
Reference in New Issue
Block a user