Files
workspace/code/fms/fms-vue/tests/unit/formula-engine.spec.js
T
2026-09-27 21:59:13 +08:00

148 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 公式引擎单测:直接跑共享测试向量(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)
})
})