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

276 lines
9.7 KiB
JavaScript

/**
* 计算公式 · 运行期集成测试(FmsModuleEditModal)。
* 覆盖:打开旧单据不重算、依赖变更触发重算、链式公式收敛、覆写与恢复、
* 保存请求携带 moduleCode / formulaRevision / formulaOverrides。
*/
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
const { saveObjectApi } = vi.hoisted(() => ({ saveObjectApi: vi.fn(async () => ({})) }))
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn(async (viewName, condition) => {
// 第二个模块(b_order2)用于验证「被公式引用的隐藏字段(静默字段)」:条件里带模块编码即可区分
const hidden = String(condition || '').includes('b_order2')
if (viewName === 's_module') {
return {
data: [
hidden
? {
b_id: 'b_order2',
b_name: '订单2',
b_module_type: 'data',
b_canuse: 1,
b_view_table: 'b_order2',
b_save_table: 'b_order2',
b_key_field: 'b_id',
}
: {
b_id: 'b_order',
b_name: '订单',
b_module_type: 'data',
b_canuse: 1,
b_view_table: 'b_order',
b_save_table: 'b_order',
b_key_field: 'b_id',
},
],
}
}
if (viewName === 's_field') {
return {
data: hidden
? [
{ b_field: 'b_id', b_name: '单号', b_type: 'input', b_canuse: 1 },
{ b_field: 'b_qty', b_name: '数量', b_type: 'number', b_canuse: 1 },
{ b_field: 'b_price', b_name: '单价', b_type: 'money', b_canuse: 1 },
{ b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 },
{ b_field: 'b_rate', b_name: '系数', b_type: 'number', b_canuse: 1 },
]
: [
{ b_field: 'b_id', b_name: '单号', b_type: 'input', b_canuse: 1 },
{ b_field: 'b_qty', b_name: '数量', b_type: 'number', b_canuse: 1 },
{ b_field: 'b_price', b_name: '单价', b_type: 'money', b_canuse: 1 },
{ b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 },
{ b_field: 'b_tax', b_name: '税额', b_type: 'money', b_canuse: 1 },
],
}
}
if (viewName === 's_module_schema') {
return {
data: [
{
b_module_id: hidden ? 'b_order2' : 'b_order',
b_schema_type: 'edit',
b_schema_json: JSON.stringify(
hidden
? {
schemaVersion: 2,
children: [
{
type: 'row',
children: [
{ field: 'b_qty', span: 12 },
{ field: 'b_price', span: 12 },
],
},
{
type: 'row',
children: [
{
field: 'b_amount',
readonly: true,
formula: 'ROUND(b_qty * b_price * b_rate, 2)',
},
],
},
// 隐藏的依赖字段:只取值、不渲染(静默字段)
{ type: 'row', children: [{ field: 'b_rate', visible: false }] },
],
}
: {
schemaVersion: 2,
children: [
{
type: 'group',
id: 'g1',
title: '基础信息',
children: [
{
type: 'row',
children: [
{ field: 'b_qty', span: 12 },
{ field: 'b_price', span: 12 },
],
},
{
type: 'row',
children: [
{
field: 'b_amount',
readonly: true,
formula: 'ROUND(b_qty * b_price, 2)',
},
{
field: 'b_tax',
readonly: true,
formula: 'b_amount * 0.1',
overridable: true,
},
],
},
],
},
],
},
),
},
],
}
}
return { data: [] }
}),
saveObjectApi,
nextIdApi: vi.fn(async () => ({ data: '1' })),
loadDataBySqlApi: vi.fn(async () => ({ data: [] })),
}))
import FmsModuleEditModal from '@/components/fms-module-edit/FmsModuleEditModal.vue'
/** 弹窗内的输入框(按表单顺序):[b_qty, b_price, b_amount, b_tax] */
function inputs() {
return [...document.querySelectorAll('.fms-modal-body input')]
}
/** 按索引写值并触发 input(v-model 口径),然后等一轮渲染 */
async function setInput(index, value) {
const input = inputs()[index]
input.value = value
input.dispatchEvent(new Event('input', { bubbles: true }))
await nextTick()
}
function buttonByText(text) {
return [...document.querySelectorAll('.fms-modal-footer button, .fms-modal-body button')].find(
(button) => button.textContent.trim() === text,
)
}
async function openModal(record, dataCode = 'b_order') {
const wrapper = mount(FmsModuleEditModal, {
props: { open: false, dataCode, record },
attachTo: document.body,
})
await wrapper.setProps({ open: true })
await new Promise((resolve) => setTimeout(resolve, 40))
await nextTick()
return wrapper
}
const STORED = { b_id: 'O1', b_qty: 9, b_price: '10.00', b_amount: '99', b_tax: '9.9' }
beforeEach(() => {
setActivePinia(createPinia())
document.body.innerHTML = ''
saveObjectApi.mockClear()
})
describe('计算公式 · 运行期', () => {
it('打开旧单据不重算,原有计算值原样保留', async () => {
const wrapper = await openModal({ ...STORED })
expect(document.querySelector('.fme-formula-banner')).toBeNull()
expect(inputs()[2].value).toBe('99')
expect(inputs()[3].value).toBe('9.9')
wrapper.unmount()
})
it('依赖字段变更触发重算,链式公式逐级收敛', async () => {
const wrapper = await openModal({ ...STORED })
await setInput(0, '2')
expect(inputs()[2].value).toBe('20')
expect(inputs()[3].value).toBe('2')
wrapper.unmount()
})
it('覆写后不再被公式覆盖,恢复公式回到计算结果', async () => {
const wrapper = await openModal({ ...STORED })
buttonByText('手工调整').click()
await nextTick()
await setInput(3, '5')
await setInput(0, '3')
expect(inputs()[2].value).toBe('30')
expect(inputs()[3].value).toBe('5') // 覆写值不被覆盖
buttonByText('恢复公式').click()
await nextTick()
expect(inputs()[3].value).toBe('3')
wrapper.unmount()
})
it('保存请求带 moduleCode / formulaRevision / formulaOverrides,且计算值进入更新', async () => {
const wrapper = await openModal({ ...STORED })
await setInput(0, '2')
buttonByText('确定').click()
await new Promise((resolve) => setTimeout(resolve, 20))
const [requests] = saveObjectApi.mock.calls.at(-1)
const request = requests[0]
expect(request.table).toBe('b_order')
expect(request.moduleCode).toBe('b_order')
expect(String(request.formulaRevision).startsWith('fnv1a64:')).toBe(true)
expect(request.formulaOverrides).toBeUndefined()
const updated = request.updates[0]
expect(String(updated.b_qty)).toBe('2')
expect(updated.b_amount).toBe('20')
expect(updated.b_tax).toBe('2')
wrapper.unmount()
})
it('覆写声明随请求提交(不落业务字段)', async () => {
const wrapper = await openModal({ ...STORED })
buttonByText('手工调整').click()
await nextTick()
await setInput(3, '5')
buttonByText('确定').click()
await new Promise((resolve) => setTimeout(resolve, 20))
const [requests] = saveObjectApi.mock.calls.at(-1)
expect(requests[0].formulaOverrides).toEqual(['b_tax'])
expect(requests[0].updates[0].b_tax).toBe('5')
wrapper.unmount()
})
})
describe('计算公式 · 静默字段(被引用的隐藏字段)', () => {
it('隐藏依赖只取值不渲染,并参与重算', async () => {
const wrapper = await openModal(
{ b_id: 'O2', b_qty: 2, b_price: '10.00', b_amount: '10', b_rate: 0.5 },
'b_order2',
)
// 隐藏字段不渲染:只有 数量 / 单价 / 金额 三个输入
expect(inputs().length).toBe(3)
await setInput(0, '4')
expect(inputs()[2].value).toBe('20')
wrapper.unmount()
})
it('隐藏依赖为空时按空值参与运算,不阻断保存', async () => {
const wrapper = await openModal(
{ b_id: 'O2', b_qty: 2, b_price: '10.00', b_amount: '10' },
'b_order2',
)
await setInput(0, '4')
expect(inputs()[2].value).toBe('0')
buttonByText('确定').click()
await new Promise((resolve) => setTimeout(resolve, 20))
const [requests] = saveObjectApi.mock.calls.at(-1)
expect(requests[0].moduleCode).toBe('b_order2')
expect(requests[0].updates[0].b_amount).toBe('0')
wrapper.unmount()
})
})