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 riskDailyTrendChart = readFileSync( fileURLToPath(new URL('../src/components/charts/RiskDailyTrendChart.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 topBarComponent = readFileSync( fileURLToPath(new URL('../src/components/layout/TopBar.vue', import.meta.url)), 'utf8' ) const appShellComposable = readFileSync( fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)), 'utf8' ) const legacyAppScript = readFileSync( fileURLToPath(new URL('../src/scripts/App.js', 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: { RiskOps: 3 }, expense_type_distribution: { travel: 2 }, risk_type_distribution: { duplicate_invoice: 2 }, supplier_distribution: { VendorA: 1 }, employee_grade_distribution: { P6: 2 }, top_departments: [{ name: 'RiskOps', count: 3, amount: 8800 }], top_employees: [{ name: 'RiskUser', count: 2, amount: 6200 }], top_suppliers: [{ name: 'VendorA', 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.RiskOps, 3) assert.equal(dashboard.expenseTypeDistribution.travel, 2) assert.equal(dashboard.riskTypeDistribution.duplicate_invoice, 2) assert.equal(dashboard.supplierDistribution.VendorA, 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 multi-dimension and ranking panels', () => { assert.match(dashboardComponent, /risk-dimension-grid/) assert.match(dashboardComponent, /risk-composition-panel/) assert.match(dashboardComponent, /risk-ranking-visual/) assert.match(dashboardComponent, /risk-ranking-chart-block/) assert.match(dashboardComponent, /\.risk-ranking-panel\s*\{\s*grid-column:\s*span 12;/) assert.match(dashboardComponent, /rankingChartItems/) assert.match(dashboardComponent, /rankingDetailGroups/) 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/) assert.doesNotMatch(dashboardComponent, /risk-effect-panel/) assert.doesNotMatch(dashboardComponent, /risk-recent-panel/) }) test('risk dashboard localizes backend metric keys before rendering', () => { assert.notEqual(formatRiskSignalLabel('duplicate_invoice'), 'duplicate_invoice') assert.notEqual(formatRiskSignalLabel('budget_pressure'), 'budget_pressure') assert.notEqual(formatRiskSignalLabel('missing_material'), 'missing_material') assert.notEqual(formatExpenseTypeLabel('travel'), 'travel') assert.notEqual(formatRiskSourceLabel('rule_center'), 'rule_center') assert.notEqual(formatRiskSourceLabel('financial_risk_graph'), 'financial_risk_graph') assert.notEqual(formatRiskDimensionLabel('simulation', 'risk_type'), 'simulation') assert.notEqual(formatRiskDimensionLabel('policy.duplicate_invoice', 'rule'), 'policy.duplicate_invoice') assert.notEqual( formatRiskObservationTitle({ title: 'policy.duplicate_invoice', riskSignal: 'duplicate_invoice' }), 'policy.duplicate_invoice' ) assert.match(riskLabels, /travel:/) assert.match(riskLabels, /rule_center:/) assert.match(overviewViewModel, /formatRiskSignalLabel/) assert.match(overviewViewModel, /riskCompositionLegend/) assert.match(overviewViewModel, /signalDistribution/) assert.doesNotMatch(overviewViewModel, /formatRiskSourceLabel/) assert.doesNotMatch(dashboardComponent, /formatRiskObservationTitle/) assert.doesNotMatch(dashboardComponent, /text\.replace\(\s*\/_\/g/) }) test('risk dashboard follows the top overview range without card-level selectors', () => { assert.match(overviewViewModel, /const topRangeDays = computed/) assert.match(overviewViewModel, /windowDays: topRangeDays\.value/) assert.match(overviewViewModel, /options\.activeRange/) assert.doesNotMatch(overviewViewModel, /activeRiskWindowDays/) assert.doesNotMatch(overviewViewModel, /setRiskWindowDays/) assert.doesNotMatch(overviewTemplate, /:window-options="riskWindowOptions"/) assert.doesNotMatch(overviewTemplate, /:active-window-days="activeRiskWindowDays"/) assert.doesNotMatch(overviewTemplate, /@update:window-days="setRiskWindowDays"/) assert.doesNotMatch(dashboardComponent, /EnterpriseSelect/) assert.doesNotMatch(dashboardComponent, /risk-window-select/) assert.doesNotMatch(dashboardComponent, /emit\('update:windowDays', \$event\)/) assert.match(dashboardComponent, /dashboard\.windowDays/) assert.match(dashboardComponent, /RiskDailyTrendChart/) }) test('overview custom date defaults use current year instead of hard-coded legacy dates', () => { assert.match(topBarComponent, /createCurrentYearDateRange/) assert.match(topBarComponent, /formatDateValue/) assert.match(appShellComposable, /createCurrentYearDateRange\(\)/) assert.match(legacyAppScript, /createCurrentYearDateRange\(\)/) assert.doesNotMatch(topBarComponent, /2024-07-06|2024-07-12/) assert.doesNotMatch(appShellComposable, /2024-07-06|2024-07-12/) assert.doesNotMatch(legacyAppScript, /2024-07-06|2024-07-12/) }) test('risk daily trend is bucketed for long ranges and keeps chart labels readable', () => { assert.match(overviewViewModel, /RISK_DAILY_TREND_MAX_BUCKETS = 14/) assert.match(overviewViewModel, /aggregateRiskDailyTrendRows/) assert.match(overviewViewModel, /Math\.ceil\(normalizedRows\.length \/ maxBuckets\)/) assert.match(overviewViewModel, /buildRiskTrendBucketLabel/) assert.doesNotMatch(overviewViewModel, /rows\.slice\(-7\)/) assert.match(riskDailyTrendChart, /const barWidth = computed/) assert.match(riskDailyTrendChart, /barMaxWidth: 14/) assert.match(riskDailyTrendChart, /bottomGridSize/) assert.match(riskDailyTrendChart, /replace\('~', '\\n~'\)/) }) test('risk dashboard shows loading overlay and realtime refresh status', () => { assert.match(overviewTemplate, /dashboard-loading-state/) assert.match(overviewTemplate, /floating/) assert.match(overviewTemplate, /TableLoadingState/) assert.match(overviewTemplate, /activeDashboardLoadingText/) assert.match(dashboardComponent, /risk-dashboard-loading-state/) assert.match(dashboardComponent, /floating/) assert.match(dashboardComponent, /TableLoadingState/) 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\(\)/) })