/** * 分 section 脏检查测试:守护 hasModuleChanges 的判定语义。 * 背景见 docs/module-management-tab-status-plan.md 阶段一: * - 加载时自动补全的默认列表行只写 drafts 不写 originals(useModuleConfig.loadModuleConfig), * 此时视为「脏」是既有行为,分 section 对比必须原样保留,不得“修复”; * - hasModuleChanges 从整树 JSON.stringify 对比改为 dirtySections 派生, * 本 spec 以判定结果等价为验收基准。 */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { ref } from 'vue' import { useModuleConfig } from '@/views/system/module-management/composables/useModuleConfig' vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(), describeApi: vi.fn(), })) vi.mock('@/components/ui', () => ({ Message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }, })) beforeEach(() => { vi.clearAllMocks() }) /** 完整的 8 段草稿结构(与 loadModuleConfig 构造的 original 同形) */ function baseConfig(over = {}) { const config = { module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data' }, fields: [{ b_id: '10', b_field: 'amount', b_name: '金额', b_canuse: 1 }], groups: [], listConfig: [], editConfig: [], queryConfig: [], autoCode: [], powers: [], } return Object.assign(config, over) } function setup({ drafts, originals }) { const draftsRef = ref(drafts) const originalsRef = ref(originals) const deps = { moduleSelectedId: ref('1'), moduleTreeData: ref([]), findModuleNode: () => null, selectedModule: ref({ b_id: '1', b_code: 'demo', b_name: '演示模块' }), busy: ref(false), configLoading: ref(false), configSyncing: ref(false), activeSection: ref('basic'), loadedModuleId: ref('1'), originals: originalsRef, drafts: draftsRef, skipModuleLoad: { value: false }, showConfirm: vi.fn(), switchConfirm: ref(null), i18nPanelRef: ref(null), } return { ...useModuleConfig(deps), originalsRef, draftsRef } } describe('useModuleConfig 脏检查', () => { it('未修改时 hasModuleChanges 为 false', () => { const originals = baseConfig() const { hasModuleChanges } = setup({ drafts: JSON.parse(JSON.stringify(originals)), originals }) expect(hasModuleChanges.value).toBe(false) }) it('无草稿时 hasModuleChanges 为 false', () => { const { hasModuleChanges } = setup({ drafts: null, originals: null }) expect(hasModuleChanges.value).toBe(false) }) it('任一 section 修改即 hasModuleChanges 为 true', () => { const originals = baseConfig() for (const key of ['module', 'fields', 'groups', 'listConfig', 'editConfig', 'queryConfig', 'autoCode', 'powers']) { const drafts = JSON.parse(JSON.stringify(originals)) if (key === 'module') drafts.module.b_name = '改名' else drafts[key] = [...drafts[key], { b_id: 'new' }] const { hasModuleChanges } = setup({ drafts, originals }) expect(hasModuleChanges.value, `修改 ${key} 应判定为脏`).toBe(true) } }) it('加载时补全的默认列表行(只写 drafts)视为脏——既有行为', () => { const originals = baseConfig() const drafts = JSON.parse(JSON.stringify(originals)) drafts.listConfig = [{ b_id: 'tmp1', b_field_id: '10', b_visible: 1, b_xh: 10 }] const { hasModuleChanges } = setup({ drafts, originals }) expect(hasModuleChanges.value).toBe(true) }) it('查询模块缺失的 section(undefined)两侧一致时不脏,单侧出现即脏', () => { const originals = { module: { b_id: '2', b_code: 'q', b_module_type: 'query' }, fields: [], groups: [], listConfig: [], editConfig: undefined, queryConfig: [], autoCode: undefined, powers: [], } const same = setup({ drafts: JSON.parse(JSON.stringify(originals)), originals }) expect(same.hasModuleChanges.value).toBe(false) const drafts = JSON.parse(JSON.stringify(originals)) drafts.editConfig = [] const diff = setup({ drafts, originals }) expect(diff.hasModuleChanges.value).toBe(true) }) it('dirtySections 精确指出被修改的 section', () => { const originals = baseConfig() const drafts = JSON.parse(JSON.stringify(originals)) drafts.module.b_name = '改名' drafts.queryConfig = [{ b_id: 'q1' }] const { dirtySections } = setup({ drafts, originals }) expect(dirtySections.value.has('module')).toBe(true) expect(dirtySections.value.has('queryConfig')).toBe(true) expect(dirtySections.value.has('fields')).toBe(false) expect(dirtySections.value.size).toBe(2) }) it('保存后整体替换 originals/drafts 为同一份深拷贝,脏状态清空', () => { const originals = baseConfig() const drafts = JSON.parse(JSON.stringify(originals)) drafts.module.b_name = '改名' const api = setup({ drafts, originals }) expect(api.hasModuleChanges.value).toBe(true) // 模拟 useModuleSave 保存后的整体 cloneData 换引用 const saved = JSON.parse(JSON.stringify(drafts)) api.originalsRef.value = saved api.draftsRef.value = JSON.parse(JSON.stringify(saved)) expect(api.hasModuleChanges.value).toBe(false) }) }) describe('useModuleConfig 缺配统计(表单设计徽标)', () => { function configWith(fields, editConfig) { return { module: { b_id: '1', b_module_type: 'data' }, fields, groups: [], listConfig: [], editConfig, queryConfig: [], autoCode: [], powers: [] } } it('启用字段未被 editConfig 引用即计入缺配', () => { const fields = [ { b_id: '10', b_field: 'a', b_canuse: 1 }, { b_id: '11', b_field: 'b', b_canuse: 1 }, ] const editConfig = [{ b_id: 'e1', b_field_id: '10' }] const { missingEditCount } = setup({ drafts: configWith(fields, editConfig), originals: configWith(fields, editConfig) }) expect(missingEditCount.value).toBe(1) }) it('停用字段不计入缺配', () => { const fields = [ { b_id: '10', b_field: 'a', b_canuse: 1 }, { b_id: '11', b_field: 'b', b_canuse: 0 }, ] const { missingEditCount } = setup({ drafts: configWith(fields, []), originals: configWith(fields, []) }) expect(missingEditCount.value).toBe(1) }) it('b_field_id 与 b_id 类型不一致(数字/字符串)仍按字符串比较', () => { const fields = [{ b_id: 10, b_field: 'a', b_canuse: 1 }] const editConfig = [{ b_id: 'e1', b_field_id: '10' }] const { missingEditCount } = setup({ drafts: configWith(fields, editConfig), originals: configWith(fields, editConfig) }) expect(missingEditCount.value).toBe(0) }) it('无字段时缺配为 0(新建模块空态不显示徽标)', () => { const { missingEditCount } = setup({ drafts: configWith([], []), originals: configWith([], []) }) expect(missingEditCount.value).toBe(0) }) })