148 lines
5.5 KiB
JavaScript
148 lines
5.5 KiB
JavaScript
/**
|
||
* 公式引擎单测:直接跑共享测试向量(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)
|
||
})
|
||
})
|