703 lines
24 KiB
JavaScript
703 lines
24 KiB
JavaScript
/**
|
||
* 回归用例:模块管理 - 修改基础信息 → 保存 → 切到其它树节点仍提示「未保存」。
|
||
*
|
||
* 成因:查询配置面板(ModuleQueryConfigPanel)的「行草稿 → query JSON」回写在配置
|
||
* 重载时被触发。保存走 afterSave → loadModuleConfig(同一模块重载,面板不卸载),
|
||
* schema ref 被换成新对象 → 面板重新解析出行草稿 → 回写监听发现
|
||
* rowsToQuerySchema(rows) 与当前 schema 文本不等(补齐 quick/groups、给条件补默认
|
||
* component 等「规范化」差异),于是把规范化后的 query JSON 写回草稿:
|
||
* - 草稿与库中基线文本不一致 → hasUnsavedChanges() 恒为真,切节点必弹确认框;
|
||
* - 编辑器未建模的属性(运行时「保存为通用」写入的 condition.area 等)被静默改写。
|
||
* 修复:回写前先与「当前配置的规范化结果」比较,仅用户真实改动才回写
|
||
* (与 TableDesignEditor / FormDesignEditor 的回写策略一致)。
|
||
*/
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { mount, flushPromises } from '@vue/test-utils'
|
||
import ModuleManagement from '@/views/module/module-management/index.vue'
|
||
import { createTestPinia, createTestRouter } from '../helpers/app.js'
|
||
import { createDefaultSchema, schemaToJsonText } from '@/utils/schemaJson'
|
||
|
||
vi.mock('@/services/api', () => ({
|
||
loadDataApi: vi.fn(),
|
||
saveObjectApi: vi.fn(),
|
||
nextIdApi: vi.fn(() => Promise.resolve({ data: 9001, code: 0, success: true })),
|
||
describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })),
|
||
}))
|
||
|
||
vi.mock('@/components/ui/message/message-manager.js', () => {
|
||
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
|
||
return { Message: m, default: m }
|
||
})
|
||
|
||
import { loadDataApi, saveObjectApi } from '@/services/api'
|
||
import { Message } from '@/components/ui/message/message-manager.js'
|
||
|
||
/** 库中的一条关系:demo.b_name → other.b_name(一对多,源侧即当前模块) */
|
||
function relationRow() {
|
||
return {
|
||
b_source_module_id: 'demo',
|
||
b_source_field: 'b_name',
|
||
b_target_module_id: 'other',
|
||
b_target_field: 'b_name',
|
||
b_relation_type: 'one_to_many',
|
||
b_on_delete: 'none',
|
||
b_canuse: 1,
|
||
b_xh: 10,
|
||
}
|
||
}
|
||
|
||
function moduleRow(id, name) {
|
||
return {
|
||
b_id: id,
|
||
b_parent_id: null,
|
||
b_depth: 0,
|
||
b_path: `/${id}/`,
|
||
b_name: name,
|
||
b_i18n: `module.${id}`,
|
||
b_module_type: 'data',
|
||
b_view_table: `v_${id}`,
|
||
b_save_table: `t_${id}`,
|
||
b_key_field: 'b_id',
|
||
b_order_sql: null,
|
||
b_query_sql: null,
|
||
b_config_json: null,
|
||
b_canuse: 1,
|
||
b_xh: 10,
|
||
b_bz: null,
|
||
// 迁移后 s_module 才有这一列(见 sql/fms_module_updated_at.sql)
|
||
b_updated_at: '2026-09-01 08:00:00',
|
||
}
|
||
}
|
||
|
||
/** 库中历史查询配置:缺 quick 键、条件未显式写 component,且带运行时写入的 area 标记 */
|
||
const legacyQuerySchema = {
|
||
schemaVersion: 1,
|
||
groups: {
|
||
logic: 'and',
|
||
conditions: [{ field: 'b_name', operator: 'like', area: 'hidden' }],
|
||
},
|
||
}
|
||
|
||
function schemaRow(moduleId, type, json) {
|
||
return {
|
||
b_module_id: moduleId,
|
||
b_schema_type: type,
|
||
b_schema_json: schemaToJsonText(type, json),
|
||
b_canuse: 1,
|
||
}
|
||
}
|
||
|
||
let db
|
||
let savedRequests
|
||
|
||
function buildDb() {
|
||
const fields = ['demo', 'other'].map((id) => ({
|
||
b_module_id: id,
|
||
b_field: 'b_name',
|
||
b_name: '名称',
|
||
b_i18n: `field.${id}.b_name`,
|
||
b_xh: 10,
|
||
b_canuse: 1,
|
||
}))
|
||
const schemas = [
|
||
schemaRow('demo', 'view', createDefaultSchema('view')),
|
||
schemaRow('demo', 'edit', createDefaultSchema('edit')),
|
||
schemaRow('demo', 'query', { schemaVersion: 1, groups: [legacyQuerySchema.groups] }),
|
||
]
|
||
return {
|
||
s_module: [moduleRow('demo', '示例模块'), moduleRow('other', '另一个模块')],
|
||
s_field: fields,
|
||
s_module_schema: schemas,
|
||
s_autocode: [],
|
||
s_power: [],
|
||
s_relation: [],
|
||
s_delete_rule: [],
|
||
s_i18n: [{ b_key: 'module.demo', b_locale: 'en-US', b_value: 'Demo module' }],
|
||
}
|
||
}
|
||
|
||
const rowsOf = (view) => (db[view] || []).map((row) => ({ ...row }))
|
||
|
||
beforeEach(() => {
|
||
db = buildDb()
|
||
savedRequests = []
|
||
|
||
loadDataApi.mockReset()
|
||
loadDataApi.mockImplementation((view, condition) => {
|
||
if (view === 's_i18n_type')
|
||
return Promise.resolve({
|
||
data: [
|
||
{ b_id: 'zh-CN', b_name: '简体中文', b_default: 1 },
|
||
{ b_id: 'en-US', b_name: 'English', b_default: 0 },
|
||
],
|
||
})
|
||
return Promise.resolve({ data: rowsOf(view) })
|
||
})
|
||
|
||
saveObjectApi.mockReset()
|
||
saveObjectApi.mockImplementation((requests) => {
|
||
savedRequests.push(...(requests || []))
|
||
for (const request of requests || []) {
|
||
const table = request.table
|
||
if (!db[table]) continue
|
||
for (const row of request.updates || []) {
|
||
const keyFields = String(request.key_field || 'b_id').split(',')
|
||
const target = db[table].find((item) =>
|
||
keyFields.every((field) => String(item[field]) === String(row[field])),
|
||
)
|
||
if (target) Object.assign(target, row)
|
||
}
|
||
for (const row of request.inserts || []) db[table].push({ ...row })
|
||
}
|
||
return Promise.resolve({ code: 0, success: true })
|
||
})
|
||
})
|
||
|
||
/**
|
||
* 保存后的重载(afterSave → loadModuleConfig)不是 await 的,断言前先等它把新基线装载完,
|
||
* 避免并发跑用例时时序抖动
|
||
*/
|
||
async function settleConfigReload(wrapper) {
|
||
for (let i = 0; i < 50 && wrapper.vm.configLoading; i += 1) {
|
||
await flushPromises()
|
||
}
|
||
await flushPromises()
|
||
}
|
||
|
||
async function mountManagement() {
|
||
const pinia = createTestPinia()
|
||
const router = createTestRouter('/')
|
||
await router.isReady()
|
||
return mount(ModuleManagement, { global: { plugins: [pinia, router] } })
|
||
}
|
||
|
||
describe('模块管理:保存后的未保存判定', () => {
|
||
it('保存基础信息后不应再判定为未保存,切换树节点不再弹确认框', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
// 打开即干净:加载时的配置规范化不得算作改动
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
expect(wrapper.vm.maindata.b_name).toBe('改名后')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
|
||
// 切到其它节点:直接切换,不弹「未保存」确认框
|
||
wrapper.vm.onModuleSelect('other')
|
||
await flushPromises()
|
||
expect(wrapper.vm.switchConfirm).toBe(null)
|
||
expect(String(wrapper.vm.moduleSelectedId)).toBe('other')
|
||
})
|
||
|
||
it('未被用户编辑的查询配置不应随保存写回(不静默改写 area 等属性)', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
// 只提交主表变更;查询配置(s_module_schema.query)不参与
|
||
expect(savedRequests.some((request) => request.table === 's_module')).toBe(true)
|
||
expect(savedRequests.some((request) => request.table === 's_module_schema')).toBe(false)
|
||
|
||
// 库中 query JSON 保持原样(含 area 标记)
|
||
const queryRow = db.s_module_schema.find((row) => row.b_schema_type === 'query')
|
||
expect(JSON.parse(queryRow.b_schema_json).groups[0].conditions[0].area).toBe('hidden')
|
||
// 草稿里的查询配置也不得被「规范化」改写(否则下一次保存就会把它落库)
|
||
expect(wrapper.vm.schemaCurrent.query.value).toEqual({
|
||
schemaVersion: 1,
|
||
groups: [
|
||
{ logic: 'and', conditions: [{ field: 'b_name', operator: 'like', area: 'hidden' }] },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('多语言的人工翻译应计入未保存,保存后不再提示', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
const row = wrapper.vm.i18n.find((item) => item.b_key === 'module.demo')
|
||
expect(row, '库中的模块名多语言行应加载到草稿').toBeTruthy()
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
|
||
row['en-US'] = 'Demo module v2'
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
expect(
|
||
db.s_i18n.find((item) => item.b_key === 'module.demo' && item.b_locale === 'en-US')?.b_value,
|
||
).toBe('Demo module v2')
|
||
})
|
||
|
||
it('权限面板:库中历史派生列不算未保存改动,仅在保存前物化', async () => {
|
||
db.s_power = [
|
||
{
|
||
b_id: 'module.demo.access',
|
||
b_object_type: 'module',
|
||
b_object_id: 'demo',
|
||
b_action: 'access',
|
||
b_type: 'action',
|
||
b_name: '模块使用',
|
||
b_canuse: 1,
|
||
b_xh: 100,
|
||
},
|
||
]
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
// 打开即干净:加载阶段不得把库里的派生列改写进草稿
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
expect(wrapper.vm.powers[0].b_id).toBe('module.demo.access')
|
||
expect(wrapper.vm.powers[0].b_xh).toBe(100)
|
||
|
||
wrapper.vm.powers[0].b_action = 'audit'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
const powerReq = savedRequests.find((request) => request.table === 's_power')
|
||
expect(powerReq, '动作变化应提交权限点变更').toBeTruthy()
|
||
// 权限编码即主键:动作变化会换编码,落库表现为「删旧码 + 插新码」
|
||
expect(powerReq.deletes).toEqual([{ b_id: 'module.demo.access' }])
|
||
expect(powerReq.inserts[0]).toMatchObject({
|
||
b_id: 'action.demo.audit',
|
||
b_object_type: 'module',
|
||
b_object_id: 'demo',
|
||
b_type: 'action',
|
||
b_xh: 10,
|
||
})
|
||
})
|
||
|
||
it('保存当前模块不得丢弃其它未保存的新建草稿节点', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
await wrapper.vm.addModule(null, 'module')
|
||
await flushPromises()
|
||
const draftId = String(wrapper.vm.moduleSelectedId)
|
||
expect(draftId.startsWith('-'), '新建模块用本地临时负数 id 占位').toBe(true)
|
||
|
||
wrapper.vm.onModuleSelect('demo')
|
||
await flushPromises()
|
||
await flushPromises()
|
||
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
expect(wrapper.vm.unsavedModuleIds.has(draftId), '草稿仍应标记为未保存').toBe(true)
|
||
expect(
|
||
wrapper.vm.moduleTreeData.find((node) => String(node.b_id) === draftId),
|
||
'草稿节点应保留在树中',
|
||
).toBeTruthy()
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('删除其它节点不得丢掉当前未保存的草稿', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
wrapper.vm.deleteModule('other')
|
||
await flushPromises()
|
||
await wrapper.vm.onConfirmOk()
|
||
await flushPromises()
|
||
|
||
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
|
||
expect(wrapper.vm.maindata.b_name).toBe('改名后')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
})
|
||
|
||
it('父模块尚未保存时不允许先保存子模块(避免插入指向临时 id 的孤儿行)', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
await wrapper.vm.addModule(null, 'module')
|
||
await flushPromises()
|
||
const parentDraftId = String(wrapper.vm.moduleSelectedId)
|
||
|
||
await wrapper.vm.addModule(parentDraftId, 'module')
|
||
await flushPromises()
|
||
wrapper.vm.maindata.b_id = 'sub_module'
|
||
wrapper.vm.maindata.b_name = '子模块'
|
||
|
||
const ok = await wrapper.vm.handleSave()
|
||
await flushPromises()
|
||
|
||
expect(ok).toBe(false)
|
||
expect(Message.warning).toHaveBeenCalledWith('父模块尚未保存,请先保存父模块')
|
||
expect(savedRequests).toEqual([])
|
||
})
|
||
|
||
it('删除模块应一并清理菜单绑定(s_menu_module)', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.deleteModule('demo')
|
||
await flushPromises()
|
||
expect(wrapper.vm.confirmVisible).toBe(true)
|
||
|
||
await wrapper.vm.onConfirmOk()
|
||
await flushPromises()
|
||
|
||
const menuReq = savedRequests.find((request) => request.table === 's_menu_module')
|
||
expect(menuReq, '模块删除必须清理 s_menu_module 绑定行').toBeTruthy()
|
||
expect(menuReq.key_field).toBe('b_module_id')
|
||
expect(menuReq.deletes).toEqual([{ b_module_id: 'demo' }])
|
||
expect(
|
||
savedRequests.some(
|
||
(request) => request.table === 's_module' && request.deletes?.[0]?.b_id === 'demo',
|
||
),
|
||
).toBe(true)
|
||
})
|
||
|
||
it('模块关联:加载拆分源/目标两侧,保存只提交源侧行(四列复合主键)', async () => {
|
||
db.s_relation = [
|
||
{
|
||
b_source_module_id: 'demo',
|
||
b_source_field: 'b_name',
|
||
b_target_module_id: 'other',
|
||
b_target_field: 'b_name',
|
||
b_relation_type: 'one_to_many',
|
||
b_canuse: 1,
|
||
b_xh: 10,
|
||
},
|
||
{
|
||
b_source_module_id: 'other',
|
||
b_source_field: 'b_name',
|
||
b_target_module_id: 'demo',
|
||
b_target_field: 'b_name',
|
||
b_relation_type: 'many_to_one',
|
||
b_canuse: 1,
|
||
b_xh: 20,
|
||
},
|
||
]
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
// 源侧进可编辑草稿,目标侧只读展示;打开即干净(本地行键不得算作改动)
|
||
expect(wrapper.vm.relations).toHaveLength(1)
|
||
expect(wrapper.vm.reverseRelations).toHaveLength(1)
|
||
expect(String(wrapper.vm.reverseRelations[0].b_source_module_id)).toBe('other')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
|
||
wrapper.vm.relations[0].b_canuse = 0
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
const relationReq = savedRequests.find((request) => request.table === 's_relation')
|
||
expect(relationReq, '关联变更应提交 s_relation').toBeTruthy()
|
||
expect(relationReq.key_field).toBe(
|
||
'b_source_module_id,b_source_field,b_target_module_id,b_target_field',
|
||
)
|
||
expect(relationReq.updates).toEqual([
|
||
{
|
||
b_source_module_id: 'demo',
|
||
b_source_field: 'b_name',
|
||
b_target_module_id: 'other',
|
||
b_target_field: 'b_name',
|
||
b_relation_type: 'one_to_many',
|
||
b_canuse: 0,
|
||
b_xh: 10,
|
||
},
|
||
])
|
||
// 反向行不属于本模块草稿,保存不得把它算作删除
|
||
expect(relationReq.deletes ?? []).toEqual([])
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('删除模块应一并清理双向模块关联(s_relation)', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.deleteModule('demo')
|
||
await flushPromises()
|
||
await wrapper.vm.onConfirmOk()
|
||
await flushPromises()
|
||
|
||
const relationReqs = savedRequests.filter((request) => request.table === 's_relation')
|
||
expect(relationReqs).toHaveLength(2)
|
||
expect(relationReqs).toEqual([
|
||
{
|
||
table: 's_relation',
|
||
key_field: 'b_source_module_id',
|
||
deletes: [{ b_source_module_id: 'demo' }],
|
||
},
|
||
{
|
||
table: 's_relation',
|
||
key_field: 'b_target_module_id',
|
||
deletes: [{ b_target_module_id: 'demo' }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it('删除 SQL 规则:加载不脏,只提交变更(雪花主键)', async () => {
|
||
db.s_relation = [relationRow()]
|
||
db.s_delete_rule = [
|
||
{
|
||
b_id: '1900000000000000001',
|
||
b_module_id: 'demo',
|
||
b_sql: 'select b_id from demo where b_id in (:ids) and b_name like N\'a\'',
|
||
b_message: '示例模块记录已锁定,不能删除',
|
||
b_canuse: 1,
|
||
b_xh: 10,
|
||
},
|
||
]
|
||
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
// 规则进 rules;打开即干净
|
||
expect(wrapper.vm.rules).toHaveLength(1)
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
|
||
wrapper.vm.rules[0].b_canuse = 0
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
const ruleReq = savedRequests.find((request) => request.table === 's_delete_rule')
|
||
expect(ruleReq, '规则变更应提交 s_delete_rule').toBeTruthy()
|
||
expect(ruleReq.key_field).toBe('b_id')
|
||
expect(ruleReq.updates?.[0]).toMatchObject({
|
||
b_id: '1900000000000000001',
|
||
b_module_id: 'demo',
|
||
b_canuse: 0,
|
||
})
|
||
// 关系未变:不产生 s_relation 请求
|
||
expect(savedRequests.some((request) => request.table === 's_relation')).toBe(false)
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('关联删除处理:改 b_on_delete 随 s_relation 一起提交,不产生规则行', async () => {
|
||
db.s_relation = [relationRow()]
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
expect(wrapper.vm.rules).toEqual([])
|
||
wrapper.vm.relations[0].b_on_delete = 'cascade'
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(true)
|
||
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
const relationReq = savedRequests.find((request) => request.table === 's_relation')
|
||
expect(relationReq?.updates?.[0]).toMatchObject({ b_on_delete: 'cascade' })
|
||
// 关联处置收敛到 s_relation,不再生成任何规则表请求
|
||
expect(savedRequests.some((request) => request.table === 's_delete_rule')).toBe(false)
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('删除模块应一并清理删除 SQL 规则(s_delete_rule)', async () => {
|
||
db.s_relation = [relationRow()]
|
||
db.s_delete_rule = [
|
||
{
|
||
b_id: '1900000000000000001',
|
||
b_module_id: 'demo',
|
||
b_sql: 'select 1 as b_id',
|
||
b_message: '不能删除',
|
||
},
|
||
{
|
||
b_id: '1900000000000000002',
|
||
b_module_id: 'other',
|
||
b_sql: 'select 1 as b_id',
|
||
b_message: '不能删除',
|
||
},
|
||
]
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.deleteModule('demo')
|
||
await flushPromises()
|
||
await wrapper.vm.onConfirmOk()
|
||
await flushPromises()
|
||
|
||
const ruleReqs = savedRequests.filter((request) => request.table === 's_delete_rule')
|
||
const moduleReq = ruleReqs.find((request) => request.deletes?.[0]?.b_module_id === 'demo')
|
||
expect(moduleReq?.deletes).toEqual([{ b_module_id: 'demo' }])
|
||
// 只按模块列清理,不误伤其他模块的规则
|
||
expect(ruleReqs.every((request) => request.deletes.every((row) => row.b_module_id === 'demo'))).toBe(
|
||
true,
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('模块管理:卡头信息与刷新', () => {
|
||
it('保存主表变更时前端不提交审计列,草稿也不会因此变脏', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
const moduleReq = savedRequests.find((request) => request.table === 's_module')
|
||
expect(moduleReq.updates?.[0]).toMatchObject({ b_id: 'demo', b_name: '改名后' })
|
||
// 审计列(b_updated_at 等)由后端按表列存在性统一维护(核心表结构设计 §1.6)
|
||
expect(moduleReq.updates?.[0]).not.toHaveProperty('b_updated_at')
|
||
expect(moduleReq.updates?.[0]).not.toHaveProperty('b_updated_by')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('只改子表(多语言)时也触碰主表行,让后端刷新「最后保存时间」', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
const row = wrapper.vm.i18n.find((item) => item.b_key === 'module.demo')
|
||
row['en-US'] = 'Demo module v2'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
// 主表行本身没有变化:补一条只带主键的 update 触碰它,时间戳由后端补
|
||
const moduleReq = savedRequests.find((request) => request.table === 's_module')
|
||
expect(moduleReq.updates).toEqual([{ b_id: 'demo' }])
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('库里还没有 b_updated_at 列时(迁移未执行)不触碰主表行', async () => {
|
||
delete db.s_module.find((item) => item.b_id === 'demo').b_updated_at
|
||
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
const row = wrapper.vm.i18n.find((item) => item.b_key === 'module.demo')
|
||
row['en-US'] = 'Demo module v2'
|
||
await wrapper.vm.handleSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
// 只带主键的 update 在后端补不上任何可写列(审计列不存在),会被「没有可修改的字段」拒绝
|
||
expect(savedRequests.some((request) => request.table === 's_module')).toBe(false)
|
||
})
|
||
|
||
it('刷新:有未保存草稿时先弹确认,放弃后原地重载并丢弃草稿', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
wrapper.vm.requestReloadConfig()
|
||
await flushPromises()
|
||
|
||
expect(wrapper.vm.switchConfirm).toMatchObject({ pendingId: 'demo', reload: true })
|
||
|
||
await wrapper.vm.onSwitchDiscard()
|
||
await settleConfigReload(wrapper)
|
||
|
||
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
|
||
expect(wrapper.vm.maindata.b_name).toBe('示例模块')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('刷新:确认框选「保存并刷新」时保存后原地重载,不切换目标', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.maindata.b_name = '改名后'
|
||
wrapper.vm.requestReloadConfig()
|
||
await flushPromises()
|
||
await wrapper.vm.onSwitchSave()
|
||
await settleConfigReload(wrapper)
|
||
|
||
expect(wrapper.vm.switchConfirm).toBe(null)
|
||
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
|
||
expect(wrapper.vm.maindata.b_name).toBe('改名后')
|
||
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
|
||
})
|
||
|
||
it('刷新:无未保存改动时直接重载,不弹确认框', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
loadDataApi.mockClear()
|
||
wrapper.vm.requestReloadConfig()
|
||
await settleConfigReload(wrapper)
|
||
|
||
expect(wrapper.vm.switchConfirm).toBe(null)
|
||
expect(loadDataApi).toHaveBeenCalledWith('s_module', "b_id = N'demo'")
|
||
})
|
||
|
||
it('卡头「更多」删除模块走与树右键相同的入口', async () => {
|
||
const wrapper = await mountManagement()
|
||
await flushPromises()
|
||
wrapper.vm.moduleSelectedId = 'demo'
|
||
await flushPromises()
|
||
await flushPromises()
|
||
|
||
wrapper.vm.onMoreAction({ key: 'delete' })
|
||
await flushPromises()
|
||
|
||
expect(wrapper.vm.confirmVisible).toBe(true)
|
||
expect(wrapper.vm.confirmState?.title).toBe('删除模块')
|
||
})
|
||
})
|