20260927215912
This commit is contained in:
1 parent
27e49e2e45
commit
f22e67fc85
294 files changed
+29247
-3576
No files matched your search
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 公式引擎单测:直接跑共享测试向量(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)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user