import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { normalizeRiskObservationDashboard } from '../src/services/riskObservations.js' import { formatExpenseTypeLabel, formatRiskDimensionLabel, formatRiskObservationTitle, formatRiskSignalLabel, formatRiskSourceLabel } from '../src/utils/riskLabels.js' const dashboardComponent = readFileSync( fileURLToPath(new URL('../src/components/dashboard/RiskObservationDashboard.vue', import.meta.url)), 'utf8' ) const overviewViewModel = readFileSync( fileURLToPath(new URL('../src/composables/useOverviewView.js', import.meta.url)), 'utf8' ) const overviewTemplate = readFileSync( fileURLToPath(new URL('../src/views/OverviewView.vue', import.meta.url)), 'utf8' ) const riskLabels = readFileSync( fileURLToPath(new URL('../src/utils/riskLabels.js', import.meta.url)), 'utf8' ) test('risk dashboard normalizes amount, distributions, and ranking fields', () => { const dashboard = normalizeRiskObservationDashboard({ total_observations: 5, risk_clue_count: 2, feedback_sample_count: 3, total_amount: 12800, department_distribution: { 风控部: 3 }, expense_type_distribution: { travel: 2 }, risk_type_distribution: { duplicate_invoice: 2 }, supplier_distribution: { 上海差旅供应商: 1 }, employee_grade_distribution: { P6: 2 }, top_departments: [{ name: '风控部', count: 3, amount: 8800 }], top_employees: [{ name: '风险员工', count: 2, amount: 6200 }], top_suppliers: [{ name: '上海差旅供应商', count: 1, amount: 1200 }], top_expense_types: [{ name: 'travel', count: 2, amount: 4600 }], top_rules: [{ name: 'policy.duplicate_invoice', count: 2, amount: 3000 }] }) assert.equal(dashboard.totalAmount, 12800) assert.equal(dashboard.riskClueCount, 2) assert.equal(dashboard.feedbackSampleCount, 3) assert.equal(dashboard.departmentDistribution['风控部'], 3) assert.equal(dashboard.expenseTypeDistribution.travel, 2) assert.equal(dashboard.riskTypeDistribution.duplicate_invoice, 2) assert.equal(dashboard.supplierDistribution['上海差旅供应商'], 1) assert.equal(dashboard.employeeGradeDistribution.P6, 2) assert.equal(dashboard.topDepartments[0].amount, 8800) assert.equal(dashboard.topRules[0].name, 'policy.duplicate_invoice') }) test('risk dashboard renders overview amount and multi-dimension panels', () => { assert.match(overviewViewModel, /label: '新增风险数'/) assert.match(overviewViewModel, /label: '涉及金额'/) assert.match(overviewViewModel, /label: '已确认风险'/) assert.match(overviewViewModel, /label: '误报数量'/) assert.match(dashboardComponent, /业务维度分布/) assert.match(dashboardComponent, /异常排行/) assert.match(dashboardComponent, /待复核线索/) assert.match(dashboardComponent, /反馈样本/) assert.doesNotMatch(dashboardComponent, /候选规则/) assert.match(dashboardComponent, /departmentDistribution/) assert.match(dashboardComponent, /expenseTypeDistribution/) assert.match(dashboardComponent, /supplierDistribution/) assert.match(dashboardComponent, /employeeGradeDistribution/) assert.match(dashboardComponent, /topDepartments/) assert.match(dashboardComponent, /topEmployees/) assert.match(dashboardComponent, /topSuppliers/) assert.match(dashboardComponent, /topRules/) }) test('risk dashboard localizes backend metric keys before rendering', () => { assert.equal(formatRiskSignalLabel('duplicate_invoice'), '重复发票') assert.equal(formatRiskSignalLabel('policy.duplicate_invoice'), '重复发票') assert.equal(formatExpenseTypeLabel('travel'), '差旅费') assert.equal(formatRiskSourceLabel('rule_center'), '规则中心') assert.equal(formatRiskSourceLabel('financial_risk_graph'), '风险图谱') assert.equal(formatRiskDimensionLabel('policy.duplicate_invoice', 'rule'), '重复发票规则') assert.equal( formatRiskObservationTitle({ title: 'policy.duplicate_invoice', riskSignal: 'duplicate_invoice' }), '重复发票' ) assert.match(riskLabels, /travel: '差旅费'/) assert.match(riskLabels, /rule_center: '规则中心'/) assert.match(overviewViewModel, /formatRiskSignalLabel/) assert.match(overviewViewModel, /formatRiskSourceLabel/) assert.match(dashboardComponent, /formatRiskObservationTitle/) assert.doesNotMatch(dashboardComponent, /text\.replace\(\s*\/_\/g/) }) test('risk dashboard renders exception ranking as chart-led visual summary', () => { assert.match(dashboardComponent, /rankingChartItems/) assert.match(dashboardComponent, /rankingDetailGroups/) assert.match(dashboardComponent, /risk-ranking-visual/) assert.match(dashboardComponent, /risk-ranking-detail-grid/) assert.match(dashboardComponent, /:items="rankingChartItems"/) assert.match(dashboardComponent, /value-suffix="项"/) assert.doesNotMatch(dashboardComponent, /risk-ranking-grid/) }) test('risk dashboard wires window filter to trend, ranking, and cards data source', () => { assert.match(overviewViewModel, /const activeRiskWindowDays = ref\(30\)/) assert.match(overviewViewModel, /windowDays: activeRiskWindowDays\.value/) assert.match(overviewViewModel, /watch\(activeRiskWindowDays/) assert.match(overviewViewModel, /setRiskWindowDays/) assert.match(overviewTemplate, /:window-options="riskWindowOptions"/) assert.match(overviewTemplate, /:active-window-days="activeRiskWindowDays"/) assert.match(overviewTemplate, /@update:window-days="setRiskWindowDays"/) assert.match(dashboardComponent, /EnterpriseSelect/) assert.match(dashboardComponent, /aria-label="风险看板时间窗口"/) assert.match(dashboardComponent, /emit\('update:windowDays', \$event\)/) assert.match(dashboardComponent, /RiskDailyTrendChart/) assert.match(dashboardComponent, /rankingGroups/) }) test('risk dashboard shows loading overlay and realtime refresh status', () => { assert.match(overviewTemplate, /dashboard-loading-overlay/) assert.match(overviewTemplate, /activeDashboardLoadingText/) assert.match(dashboardComponent, /risk-dashboard-loading-overlay/) assert.match(dashboardComponent, /loadingLabel/) assert.match(dashboardComponent, /lastUpdatedLabel/) assert.match(dashboardComponent, /lastUpdatedAt/) assert.match(overviewViewModel, /riskDashboardLastUpdatedAt/) assert.match(overviewViewModel, /startRiskDashboardRealtimeRefresh/) assert.match(overviewViewModel, /setInterval/) assert.match(overviewViewModel, /document\.visibilityState === 'hidden'/) assert.match(overviewViewModel, /riskDashboardRequestSeq/) assert.match(overviewTemplate, /:last-updated-at="riskDashboardLastUpdatedAt"/) }) test('overview dashboards are loaded on demand instead of all at once', () => { assert.match(overviewViewModel, /const activeDashboardKey = computed/) assert.match(overviewViewModel, /const loadActiveDashboard = \(\) =>/) assert.doesNotMatch( overviewViewModel, /onMounted\(\(\) => \{\s*void loadFinanceDashboard\(\)\s*void loadSystemDashboard\(\)\s*void loadRiskDashboard\(\)\s*void loadDigitalEmployeeDashboard\(\)/ ) assert.match(overviewViewModel, /watch\(activeDashboardKey/) assert.match(overviewViewModel, /activeDashboardKey\.value === 'risk'/) assert.match(overviewViewModel, /startRiskDashboardRealtimeRefresh\(\)/) assert.match(overviewViewModel, /stopRiskDashboardRealtimeRefresh\(\)/) })