import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc' const componentFiles = [ '../src/components/commercial/CommercialWorkspace.vue', '../src/components/commercial/CommercialAccountOverview.vue', '../src/components/commercial/CommercialValueProofPanel.vue', '../src/components/commercial/CommercialPricingScenarioPanel.vue', '../src/components/commercial/CommercialMetricCard.vue', '../src/components/commercial/CommercialAdminConsole.vue', '../src/components/commercial/CommercialHistoryPanel.vue', '../src/components/commercial/CommercialMutationDialog.vue' ] for (const relativeFile of componentFiles) { test(`${relativeFile} 可以独立编译`, () => { const filename = fileURLToPath(new URL(relativeFile, import.meta.url)) const source = readFileSync(filename, 'utf8') const parsed = parse(source, { filename }) assert.deepEqual(parsed.errors, []) const id = `commercial-${relativeFile.replace(/\W+/gu, '-')}` const script = compileScript(parsed.descriptor, { id }) const template = compileTemplate({ source: parsed.descriptor.template?.content || '', filename, id, compilerOptions: { bindingMetadata: script.bindings } }) assert.deepEqual(template.errors, []) }) } const workspace = readSource('../src/components/commercial/CommercialWorkspace.vue') const valuePanel = readSource('../src/components/commercial/CommercialValueProofPanel.vue') const pricingPanel = readSource('../src/components/commercial/CommercialPricingScenarioPanel.vue') const metricCard = readSource('../src/components/commercial/CommercialMetricCard.vue') const adminConsole = readSource('../src/components/commercial/CommercialAdminConsole.vue') const mutationDialog = readSource('../src/components/commercial/CommercialMutationDialog.vue') const historyPanel = readSource('../src/components/commercial/CommercialHistoryPanel.vue') const styles = readSource('../src/components/commercial/commercial-workspace.css') const overviewView = readSource('../src/views/OverviewView.vue') const appShell = readSource('../src/views/AppShellRouteView.vue') const topBarOverviewRange = readSource('../src/components/layout/useTopBarOverviewRange.js') const overviewModel = readSource('../src/composables/useOverviewView.js') test('商业工作台具有显式租户选择、双账分离、权限和完整空错状态', () => { assert.match(workspace, /目标租户 ID/u) assert.match(workspace, /平台管理员必须显式选择租户/u) assert.match(workspace, /商业权益不能放宽安全规则/u) assert.match(valuePanel, /客户价值账/u) assert.match(valuePanel, /平台经营账/u) assert.match(valuePanel, /不同币种不会混算/u) assert.match(valuePanel, /!platformAdmin/u) assert.match(valuePanel, /role="alert"/u) assert.match(valuePanel, /这不是零成本或零价值/u) }) test('无真实财务证据显示不可用和缺失输入,不含演示或回退指标', () => { assert.match(metricCard, /commercial-unavailable-value">不可用/u) assert.match(metricCard, /查看缺失证据/u) assert.match(metricCard, /requiredInputs/u) assert.doesNotMatch(workspace, /demo|mock|fallback/iu) assert.doesNotMatch(valuePanel, /demo|mock|fallback/iu) }) test('定价场景具有参数确认、分币种走廊、部分可用与不写套餐语义', () => { assert.match(workspace, /CommercialPricingScenarioPanel/u) for (const label of ['目标贡献毛利率', '最大价值分享比例', '证据截止时间(as_of)', '确认计算条件']) { assert.match(pricingPanel, new RegExp(label, 'u')) } for (const label of ['可持续收费下限', '价值对齐收费上限', '成功费封顶', '推荐商业模式', '缺失输入']) { assert.match(pricingPanel, new RegExp(label, 'u')) } assert.match(pricingPanel, /v-for="row in view\.rows"/u) assert.match(pricingPanel, /row\.currency/u) assert.match(pricingPanel, /evidenceStatus/u) assert.match(pricingPanel, /不可用/u) assert.match(pricingPanel, /不会自动写入套餐/u) assert.match(pricingPanel, /aria-busy/u) assert.match(pricingPanel, /role="alert"/u) assert.match(pricingPanel, /inputmode="decimal"/u) }) test('所有商业写操作都有操作确认、忙碌态、幂等和版本化提示', () => { for (const label of ['创建套餐版本', '激活套餐版本', '创建订阅快照', '重新激活订阅', '变更订阅状态', '配置权益版本', '激活权益版本', '记录用量事件', '记录成本事件']) { assert.match(readSource('../src/components/commercial/commercialWorkspaceModel.js'), new RegExp(label, 'u')) } assert.match(adminConsole, /二次确认/u) assert.match(mutationDialog, /step === 'edit'/u) assert.match(mutationDialog, /确认写入/u) assert.match(mutationDialog, /幂等键/u) assert.match(mutationDialog, /版本化快照/u) assert.match(mutationDialog, /:disabled="busy"/u) assert.match(mutationDialog, /role="alertdialog"/u) }) test('五类历史有按需加载、刷新、空态与订阅状态操作入口', () => { for (const label of ['套餐版本', '订阅历史', '权益版本', '用量事件', '成本事件']) { assert.match(historyPanel, new RegExp(label, 'u')) } assert.match(historyPanel, /加载历史/u) assert.match(historyPanel, /刷新历史/u) assert.match(historyPanel, /这不是零用量或零成本/u) assert.match(historyPanel, /manage-subscription/u) assert.match(historyPanel, /变更状态/u) assert.match(historyPanel, /恢复/u) }) test('商业工作台满足键盘、触控、响应式和减少动画语义', () => { assert.match(styles, /min-height: 44px/u) assert.match(styles, /:focus-visible/u) assert.match(styles, /@media \(max-width: 760px\)/u) assert.match(styles, /@media \(prefers-reduced-motion: reduce\)/u) assert.match(mutationDialog, /@keydown\.esc/u) assert.match(mutationDialog, /aria-label="关闭对话框"/u) assert.match(pricingPanel, /tabindex="-1"/u) assert.match(pricingPanel, /:disabled="loading"/u) }) test('商业工作台已接入分析看板且不会误加载财务看板', () => { assert.match(overviewView, /CommercialWorkspace/u) assert.match(overviewView, /activeDashboard === 'commercial'/u) assert.match(overviewView, /currentUser: \{ type: Object/u) assert.match(appShell, /:current-user="currentUser"/u) assert.match(appShell, /'commercial'/u) assert.match(topBarOverviewRange, /\{ label: '商业化管理', value: 'commercial' \}/u) assert.match(overviewModel, /dashboard === 'commercial'/u) assert.match( overviewModel, /activeDashboardKey\.value === 'commercial' \|\| activeDashboardKey\.value === 'value'/u ) }) function readSource(relativeFile) { return readFileSync(fileURLToPath(new URL(relativeFile, import.meta.url)), 'utf8') }