Files
workspace/code/fms/fms-vue/tests/unit/module-save-orchestration.spec.js
T
2026-09-03 17:30:39 +08:00

214 lines
7.9 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.
/**
* useModuleSave 保存编排测试:守护跨表保存的关键不变量。
* 这些顺序约束此前只存在于注释中,被改动时不会有任何报警:
* 1. 校验失败时不产生任何保存请求,且定位到对应分节;
* 2. s_module 有修改走 updates、新建走 inserts;
* 3. 临时 id 先分配真实主键,再跨表重映射 b_field_id(req 行与草稿行同引用);
* 4. s_autocode 的 updates 剥离服务端维护的 b_currentperiod / b_currentvalue;
* 5. i18n inserts 分配真实主键并回写面板,避免二次保存重复插入。
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref } from 'vue'
import { useModuleSave } from '@/views/module/module-management/composables/useModuleSave'
import { saveObjectApi, nextIdApi } from '@/services/api'
import { Message } from '@/components/ui'
vi.mock('@/services/api', () => ({
saveObjectApi: vi.fn(),
nextIdApi: vi.fn(),
}))
vi.mock('@/components/ui', () => ({
Message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
}))
let idSeq
beforeEach(() => {
vi.clearAllMocks()
idSeq = 9000
nextIdApi.mockImplementation(async (count) => {
const ids = []
for (let i = 0; i < count; i++) ids.push(String(++idSeq))
return { data: ids }
})
})
function baseDrafts(over = {}) {
const drafts = {
// b_i18n 与 buildModuleI18nKey('data', 'demo') 派生值一致:saveModule 每次保存都会物化该键
module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data', b_i18n: 'module.demo' },
fields: [],
groups: [],
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
}
return Object.assign(drafts, over)
}
function setup({ drafts, originals, basicPanel = null, autoCodePanel = null, i18nPanel = null }) {
const unsavedModuleIds = new Set()
const deps = {
selectedModule: ref({ b_id: '1', b_code: 'demo', b_name: '演示模块' }),
drafts: ref(drafts),
originals: ref(originals),
loadedModuleId: ref('1'),
moduleSelectedId: ref('1'),
unsavedModuleIds,
configSaving: ref(false),
busy: ref(false),
activeSection: ref('basic'),
moduleTreeData: ref([]),
findModuleNode: () => null,
basicPanelRef: ref(basicPanel),
autoCodePanelRef: ref(autoCodePanel),
i18nPanelRef: ref(i18nPanel),
getFinalI18nItems: () => [{ key: 'module.demo', managed: true, id: 'module:demo' }],
}
return { saveModule: useModuleSave(deps).saveModule, unsavedModuleIds, activeSection: deps.activeSection }
}
function reqOf(table) {
return saveObjectApi.mock.calls.at(-1)[0].find((req) => req.table === table)
}
describe('useModuleSave 保存编排', () => {
it('无变更时不发请求并提示', async () => {
const drafts = baseDrafts()
const { saveModule } = setup({ drafts, originals: JSON.parse(JSON.stringify(drafts)) })
const ok = await saveModule()
expect(ok).toBe(true)
expect(saveObjectApi).not.toHaveBeenCalled()
expect(Message.info).toHaveBeenCalledWith('没有需要保存的变更')
})
it('基础面板校验失败:不发请求,定位到 basic 分节', async () => {
const drafts = baseDrafts()
const { saveModule, activeSection } = setup({
drafts,
originals: JSON.parse(JSON.stringify(drafts)),
basicPanel: { validate: async () => ['编码不能为空'] },
})
const ok = await saveModule()
expect(ok).toBe(false)
expect(saveObjectApi).not.toHaveBeenCalled()
expect(activeSection.value).toBe('basic')
expect(Message.warning).toHaveBeenCalled()
})
it('s_module 修改走 updates,新建走 inserts', async () => {
const drafts = baseDrafts({ module: { b_id: '1', b_code: 'demo', b_name: '改名后' } })
const originals = baseDrafts()
const { saveModule } = setup({ drafts, originals })
await saveModule()
expect(reqOf('s_module').updates[0].b_name).toBe('改名后')
const { saveModule: saveNew, unsavedModuleIds } = setup({
drafts: baseDrafts(),
originals: baseDrafts(),
})
unsavedModuleIds.add('1')
await saveNew()
expect(reqOf('s_module').inserts[0].b_code).toBe('demo')
})
it('临时 id 分配真实主键并跨表重映射 b_field_id', async () => {
// 新字段(临时 id -1)+ 引用它的列表列:编辑行同引用,重映射必须同时落到请求与草稿
const listRow = { b_id: '100', b_module_id: '1', b_field_id: '-1', b_visible: 1, b_xh: 10 }
const drafts = baseDrafts({
fields: [{ b_id: '-1', b_module_id: '1', b_field: 'name', b_name: '姓名' }],
listConfig: [listRow],
})
const originals = baseDrafts({
listConfig: [{ b_id: '100', b_module_id: '1', b_field_id: '9999', b_visible: 1, b_xh: 10 }],
})
const { saveModule } = setup({ drafts, originals })
const ok = await saveModule()
expect(ok).toBe(true)
expect(nextIdApi).toHaveBeenCalled()
const fieldReq = reqOf('s_field')
expect(fieldReq.inserts[0].b_id).toBe('9001')
// 列表列由旧字段改挂新字段:update 行内的临时外键必须已换成真实 id
const viewReq = reqOf('s_field_view')
expect(viewReq.updates[0].b_field_id).toBe('9001')
// 草稿行与请求行同引用:重映射后草稿也持有真实 id
expect(drafts.listConfig[0].b_field_id).toBe('9001')
})
it('s_autocode updates 剥离服务端维护的期间/当前值', async () => {
const rule = {
b_id: '200',
b_module_id: '1',
b_field_id: '100',
b_canuse: 1,
b_currentperiod: 2026,
b_currentvalue: 5,
}
const drafts = baseDrafts({
autoCode: [rule],
module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data', b_i18n: 'module.demo', b_savetable: 't_demo' },
})
const { saveModule } = setup({
drafts,
// 原件行与草稿行存在差异(当前值不同)才会产生 update,从而走到剥离逻辑
originals: baseDrafts({ autoCode: [{ ...rule, b_currentvalue: 4 }] }),
})
await saveModule()
const update = reqOf('s_autocode').updates[0]
expect(update).not.toHaveProperty('b_currentperiod')
expect(update).not.toHaveProperty('b_currentvalue')
expect(update.b_field_id).toBe('100')
})
it('i18n inserts 分配真实主键并回写面板,事务内提交', async () => {
const drafts = baseDrafts({ module: { b_id: '1', b_code: 'demo', b_name: '新名称' } })
const originals = baseDrafts()
const i18nPanel = {
reconcileManagedRows: vi.fn(),
buildSaveRequest: vi.fn(() => ({
table: 's_i18n',
key_field: 'b_id',
inserts: [{ b_id: '-9', b_key: 'module.demo', b_locale: 'zh-CN', b_value: '新名称' }],
})),
applyInsertedIds: vi.fn(),
markSaved: vi.fn(),
}
const { saveModule } = setup({ drafts, originals, i18nPanel })
const ok = await saveModule()
expect(ok).toBe(true)
// 受管键 reconcile 在构建请求前执行
expect(i18nPanel.reconcileManagedRows).toHaveBeenCalledWith([
{ key: 'module.demo', managed: true, id: 'module:demo' },
])
// 临时主键分配后回写面板,避免二次保存重复插入
expect(i18nPanel.applyInsertedIds).toHaveBeenCalledWith([
expect.objectContaining({ b_id: '9001', b_key: 'module.demo' }),
])
expect(reqOf('s_i18n').inserts[0].b_id).toBe('9001')
expect(i18nPanel.markSaved).toHaveBeenCalled()
})
it('跨面板校验前置:启用自动编码但未配置保存表则拦截', async () => {
const rule = { b_id: '200', b_module_id: '1', b_field_id: '100', b_canuse: 1 }
const drafts = baseDrafts({ autoCode: [rule] })
const { saveModule } = setup({ drafts, originals: baseDrafts() })
const ok = await saveModule()
expect(ok).toBe(false)
expect(saveObjectApi).not.toHaveBeenCalled()
expect(Message.warning).toHaveBeenCalledWith('启用自动编码前请先配置保存表')
})
})