/** * 公式引擎单测:直接跑共享测试向量(tests/fixtures/formula-vectors.json), * 同一份向量由 Java 端 JUnit 复用,保证前后端语义一致。 */ import { describe, expect, it } from 'vitest' import vectors from '../fixtures/formula-vectors.json' import { buildGraph, cycleText, evaluate, fieldNodeId, formulaRevision, parse, refsOf, validateFormula, validateFormulas, } from '@/components/fms-module-common/formula' import { rawValue } from '@/components/fms-module-common/formula/values' import { fieldTypeMapOf } from '@/components/fms-module-common/formula' const fieldTypes = fieldTypeMapOf(vectors.fields) const fieldMeta = Object.fromEntries(vectors.fields.map((field) => [field.b_field, field])) describe('公式引擎 · 求值向量', () => { for (const testCase of vectors.evaluate) { it(`${testCase.id}:${testCase.formula}`, () => { const { ast, errors } = parse(testCase.formula) expect(errors).toEqual([]) const { value, error } = evaluate(ast, { fields: testCase.values || {}, fieldTypes }) if (testCase.expectError) { expect(error?.code).toBe(testCase.expectError) return } expect(error).toBeNull() expect(rawValue(value)).toEqual(testCase.expect) }) } }) describe('公式引擎 · 校验向量', () => { for (const testCase of vectors.validate) { it(`${testCase.id}:${testCase.formula}`, () => { const { errors } = validateFormula(testCase.formula, { fields: fieldMeta, targetType: testCase.targetType, }) expect(errors.map((issue) => issue.code)).toEqual(testCase.expect) }) } }) describe('公式引擎 · 依赖图', () => { it('链式公式检测出循环引用', () => { const config = vectors.graph.chain const nodes = Object.entries(config.formulas).map(([field, formula]) => ({ field, formula })) const { graph } = validateFormulas(nodes, { fields: fieldMeta }) expect(graph.hasCycle).toBe(config.expectCycle) expect(graph.cycles.length).toBeGreaterThan(0) expect(cycleText(graph.cycles[0]).length).toBeGreaterThan(0) }) it('无环公式给出拓扑序,节点 ID 带作用域', () => { const config = vectors.graph.acyclic const nodes = Object.entries(config.formulas).map(([field, formula]) => ({ field, formula })) const { graph, errors } = validateFormulas(nodes, { fields: fieldMeta }) expect(errors).toEqual([]) expect(graph.hasCycle).toBe(config.expectCycle) expect(graph.order).toEqual(config.expectOrder) }) it('依赖抽取带作用域前缀', () => { const { ast } = parse('b_qty * b_price') const { refs, usesRow } = refsOf(ast) expect(usesRow).toBe(false) expect(refs.map((ref) => ref.id)).toEqual([ fieldNodeId('main', null, 'b_qty'), fieldNodeId('main', null, 'b_price'), ]) }) it('buildGraph 对自环给出环路', () => { const graph = buildGraph([{ id: 'main:a', deps: ['main:a'] }]) expect(graph.hasCycle).toBe(true) expect(graph.order).toEqual([]) }) }) describe('公式引擎 · 配置摘要', () => { it('与共享向量一致(Java 端使用同一期望值)', () => { expect(formulaRevision(vectors.revision.nodes)).toBe(vectors.revision.expect) }) it('公式变化摘要变化,节点顺序不影响摘要', () => { const nodes = vectors.revision.nodes const reordered = [...nodes].reverse() expect(formulaRevision(reordered)).toBe(formulaRevision(nodes)) const changed = nodes.map((node, index) => index === 0 ? { ...node, formula: 'b_qty * b_price + 1' } : node, ) expect(formulaRevision(changed)).not.toBe(formulaRevision(nodes)) }) }) describe('公式引擎 · 运行模型', () => { it('按依赖顺序重算下游字段,且只算受影响字段', async () => { const { createFormulaModel } = await import('@/components/fms-module-common/formula') const editSchema = { children: [ { type: 'row', children: [{ field: 'b_qty' }, { field: 'b_price' }] }, { type: 'row', children: [{ field: 'b_amount', formula: 'ROUND(b_qty * b_price, 2)', readonly: true }], }, { type: 'row', children: [ { field: 'b_rate', formula: 'b_amount / 2', readonly: true, overridable: true }, ], }, ], } const model = createFormulaModel({ editSchema, fields: vectors.fields }) expect(model.errors).toEqual([]) expect(model.revision.startsWith('fnv1a64:')).toBe(true) const ctx = { fields: { b_qty: 3, b_price: '12.50' } } expect(model.affectedFields(['b_qty'])).toEqual(['b_amount', 'b_rate']) const { values, errors } = model.recalculate(['b_qty'], ctx) expect(errors).toEqual([]) expect(values.get('b_amount')).toBe('37.5') expect(values.get('b_rate')).toBe('18.75') expect(model.affectedFields(['b_name'])).toEqual([]) }) it('循环引用时模型报告错误且不静默通过', async () => { const { createFormulaModel } = await import('@/components/fms-module-common/formula') const editSchema = { children: [ { type: 'row', children: [{ field: 'b_amount', formula: 'b_price + 1', readonly: true }] }, { type: 'row', children: [{ field: 'b_price', formula: 'b_amount + 1', readonly: true }] }, ], } const model = createFormulaModel({ editSchema, fields: vectors.fields }) expect(model.hasCycle).toBe(true) expect(model.errors.some((issue) => issue.code === 'E_CYCLE')).toBe(true) }) })