/** * 计算公式 · 运行期集成测试(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() }) })