153 lines
7.3 KiB
JavaScript
153 lines
7.3 KiB
JavaScript
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\(\)/)
|
|
})
|