import { describe, it, expect, vi, beforeEach } from 'vitest' import { nextTick, ref } from 'vue' import { mount, flushPromises } from '@vue/test-utils' import { createPinia } from 'pinia' import MenuDetailPanel from '@/views/system/module-management/components/MenuDetailPanel.vue' import ModuleBasicPanel from '@/views/system/module-management/components/ModuleBasicPanel.vue' // —— 单一接缝:mock 业务 API 层,确定性驱动页面状态 —— // 三个模块形成父子结构:1 系统(分类,根)→ 2 订单(数据模块,子)/ 3 查询A(查询模块,子) const moduleRows = [ { b_id: '1', b_code: 'sys', b_name: '系统', b_module_type: 'category', b_parent_id: null, b_canuse: 1, b_xh: 1 }, { b_id: '2', b_code: 'order', b_name: '订单', b_module_type: 'data', b_parent_id: '1', b_canuse: 1, b_xh: 1 }, { b_id: '3', b_code: 'qry', b_name: '查询A', b_module_type: 'query', b_parent_id: '1', b_canuse: 1, b_xh: 2 }, ] const menuRows = [ { b_id: '10', b_code: 'menu-sys', b_name: '系统菜单', b_menu_type: 'directory', b_parent_id: null, b_canuse: 1, b_xh: 1 }, { b_id: '11', b_code: 'menu-user', b_name: '用户菜单', b_menu_type: 'page', b_parent_id: '10', b_canuse: 1, b_xh: 1 }, { b_id: '12', b_code: 'menu-standalone', b_name: '独立菜单', b_menu_type: 'page', b_parent_id: null, b_canuse: 1, b_xh: 2 }, ] vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(async (table) => { if (table === 's_module') return { data: [...moduleRows] } if (table === 's_menu') return { data: [...menuRows] } return { data: [] } }), saveObjectApi: vi.fn(async () => ({})), nextIdApi: vi.fn(async () => ({ data: '999' })), describeApi: vi.fn(async () => ({ data: [] })), })) vi.mock('@/components/ui/message/message-manager', () => { const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() } return { Message: m, default: m } }) import { loadDataApi, saveObjectApi, nextIdApi } from '@/services/api' import Message from '@/components/ui/message/message-manager' import ModuleManagement from '@/views/system/module-management/index.vue' // 子面板统一打桩(页面测试只验证页面自身的骨架/交互) // ModuleBasicPanel 的 validate 结果可配置,用于测试「保存校验失败」分支 let basicValidateResult = [] const panelStubs = { ModuleBasicPanel: { name: 'ModuleBasicPanel', template: '
', methods: { validate() { return Promise.resolve(basicValidateResult) } }, }, ModuleFieldsPanel: { name: 'ModuleFieldsPanel', template: '
' }, ModuleListConfigPanel: { name: 'ModuleListConfigPanel', template: '
' }, ModuleEditConfigPanel: { name: 'ModuleEditConfigPanel', template: '
' }, ModuleQueryConfigPanel: { name: 'ModuleQueryConfigPanel', template: '
' }, ModuleAutoCodePanel: { name: 'ModuleAutoCodePanel', template: '
' }, ModulePowersPanel: { name: 'ModulePowersPanel', template: '
' }, ModuleI18nPanel: { name: 'ModuleI18nPanel', template: '
' }, MenuDetailPanel: { name: 'MenuDetailPanel', template: '
' }, // Modal 尊重 open / footer 条件:footer=false 时不渲染按钮区(与真实组件一致) Modal: { name: 'Modal', props: ['open', 'title', 'okText', 'okType', 'confirmLoading', 'footer'], template: '
', }, Transition: { template: '' }, } async function mountPage() { const wrapper = mount(ModuleManagement, { global: { plugins: [createPinia()], stubs: panelStubs }, }) activeWrapper = wrapper await flushPromises() await nextTick() return wrapper } // 模块树面板(v-show 下两个树面板都在 DOM,需限定第一个) function moduleTreeOf(wrapper) { return wrapper.findAll('.mm-tree-panel')[0] } // 点击树节点标题选中模块;默认收起时需先展开根节点 async function selectModule(wrapper, name) { const tree = moduleTreeOf(wrapper) const switcher = tree.find('.tree-node-switcher:not(.is-placeholder)') if (switcher.exists()) { await switcher.trigger('click') await nextTick() } const node = tree.findAll('.tree-node-content').find((el) => el.text().includes(name)) expect(node).toBeTruthy() await node.trigger('click') await flushPromises() await nextTick() } // 记录当前挂载实例,afterEach 统一卸载(Teleport 内容随组件卸载自动清理) let activeWrapper = null describe('模块管理页面(重构后)', () => { beforeEach(() => { localStorage.clear() sessionStorage.clear() basicValidateResult = [] vi.clearAllMocks() }) afterEach(() => { activeWrapper?.unmount() activeWrapper = null }) it('渲染树面板:标题 + 新建下拉 + 刷新 + 搜索', async () => { const wrapper = await mountPage() const tree = moduleTreeOf(wrapper) expect(tree.find('.mm-tree-header__title').text()).toBe('模块') expect(tree.find('.mm-tree-header__icon-btn').exists()).toBe(true) expect(tree.find('input[data-slot="input"]').exists()).toBe(true) }) it('树默认收起:只显示根节点,不展开子节点', async () => { const wrapper = await mountPage() const tree = moduleTreeOf(wrapper) const contents = tree.findAll('.tree-node-content') // 模块树默认收起,只显示根节点「系统」(子节点「订单」不可见) expect(contents.length).toBe(1) expect(contents[0].text()).toContain('系统') // 子节点 订单 未展开不可见 expect(tree.text()).not.toContain('订单') }) it('树节点显示类型 tag', async () => { const wrapper = await mountPage() const tags = moduleTreeOf(wrapper).findAll('.mm-tree-node__tag') expect(tags.length).toBeGreaterThan(0) expect(tags[0].text()).toBe('分类') }) it('展开后子节点可见,选中数据模块显示分组导航且唯一激活', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') // 分组导航渲染:概览 / 数据结构 / 展示配置 / 业务能力 / 高级设置 const groupTitles = wrapper.findAll('.mm-config-nav__group-title').map((t) => t.text()) expect(groupTitles).toEqual(['概览', '数据结构', '展示配置', '业务能力', '高级设置']) // 仅一个激活项(基本信息) const active = wrapper.findAll('.mm-config-nav__item.is-active') expect(active).toHaveLength(1) expect(active[0].text()).toContain('基本信息') }) it('点击导航项切换内容面板', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') const fieldsItem = wrapper .findAll('.mm-config-nav__item') .find((el) => el.text().includes('字段定义')) expect(fieldsItem).toBeTruthy() await fieldsItem.trigger('click') await nextTick() const active = wrapper.findAll('.mm-config-nav__item.is-active') expect(active).toHaveLength(1) expect(active[0].text()).toContain('字段定义') // 对应内容面板显示 expect(wrapper.find('.stub-fields').exists()).toBe(true) }) it('header 显示:重载 / 保存按钮 + 面包屑', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') expect(wrapper.text()).toContain('重载') expect(wrapper.text()).toContain('保存') // 面包屑:上级路径 系统 / 订单 expect(wrapper.text()).toContain('系统') }) it('Ctrl+S 触发保存:调用 saveObjectApi', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') saveObjectApi.mockClear() window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true })) await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() }) it('点击「重载」重新加载当前模块数据', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') loadDataApi.mockClear() const reloadBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('重载')) await reloadBtn.trigger('click') await flushPromises() await nextTick() // 重新查询当前模块及其子表 expect(loadDataApi).toHaveBeenCalledWith('s_module', expect.any(String)) }) it('顶部新建下拉:新增数据模块(调用 nextIdApi 并生成草稿)', async () => { const wrapper = await mountPage() const newBtn = moduleTreeOf(wrapper).find('.mm-tree-header__icon-btn') await newBtn.trigger('click') await nextTick() const addItem = [...document.querySelectorAll('.dropdown-item')].find((el) => el.textContent.includes('新增数据模块'), ) expect(addItem).toBeTruthy() addItem.click() await flushPromises() await nextTick() expect(nextIdApi).toHaveBeenCalled() // 新建草稿标记「未保存」 expect(wrapper.text()).toContain('未保存') }) it('右键模块节点:删除确认弹窗为 danger 按钮', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') // 右键「订单」节点(无子节点,可删除)打开上下文菜单(Teleport 到 body) const row = moduleTreeOf(wrapper) .findAll('.tree-node') .find((el) => el.text().includes('订单')) await row.trigger('contextmenu') await nextTick() const deleteItem = [...document.querySelectorAll('.tree-context-menu__item')].find((el) => el.textContent.includes('删除模块'), ) expect(deleteItem).toBeTruthy() deleteItem.click() await flushPromises() await nextTick() const modal = wrapper.find('.stub-modal') expect(modal.exists()).toBe(true) expect(modal.text()).toContain('删除模块') // okType=danger → 确认按钮为 danger 样式 expect(modal.find('.button-danger').exists()).toBe(true) }) it('菜单区右键:四种新建类型(目录/页面/数据页面/外链)', async () => { const wrapper = await mountPage() const menuTab = wrapper.findAll('.tabs-tab').find((t) => t.text().includes('菜单配置')) await menuTab.trigger('click') await flushPromises() await nextTick() const menuTree = wrapper.findAll('.mm-tree-panel')[1] const row = menuTree.find('.tree-node') await row.trigger('contextmenu') await nextTick() const labels = [...document.querySelectorAll('.tree-context-menu__item')].map((el) => el.textContent.trim(), ) expect(labels).toContain('新增目录') expect(labels).toContain('新增页面') expect(labels).toContain('新增数据页面') expect(labels).toContain('新增外链') }) it('有未保存修改时切换节点弹出保护(保存并切换/放弃修改/取消)', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') // 通过基本信息面板 stub 发出修改,使 hasModuleChanges 为 true const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { ...moduleRows[1], b_name: '订单(已修改)' }) await nextTick() // 点击模块树根节点触发切换 const rootNode = moduleTreeOf(wrapper) .findAll('.tree-node-content') .find((el) => el.text().includes('系统')) await rootNode.trigger('click') await nextTick() // 弹出三按钮保护 const modal = wrapper.find('.stub-modal') expect(modal.exists()).toBe(true) expect(modal.text()).toContain('有未保存的修改') expect(modal.text()).toContain('保存并切换') expect(modal.text()).toContain('放弃修改') expect(modal.text()).toContain('取消') }) it('顶部切换到菜单配置后:菜单树渲染', async () => { const wrapper = await mountPage() const menuTab = wrapper .findAll('.tabs-tab') .find((t) => t.text().includes('菜单配置')) await menuTab.trigger('click') await flushPromises() await nextTick() // 激活 tab 变为菜单配置 const activeTabs = wrapper.findAll('.tabs-tab-active') expect(activeTabs[0].text()).toContain('菜单配置') // 菜单树面板标题与数据 const menuTree = wrapper.findAll('.mm-tree-panel')[1] expect(menuTree.find('.mm-tree-header__title').text()).toBe('菜单') expect(menuTree.text()).toContain('系统菜单') }) it('搜索过滤:输入关键词后树仅保留命中路径', async () => { const wrapper = await mountPage() const tree = moduleTreeOf(wrapper) const searchInput = tree.find('input[data-slot="input"]') await searchInput.setValue('订单') await nextTick() // 命中路径(系统 → 订单)可见;非命中的「查询A」被过滤 expect(tree.text()).toContain('订单') expect(tree.text()).not.toContain('查询A') }) it('点击刷新按钮重新加载模块树', async () => { const wrapper = await mountPage() loadDataApi.mockClear() const refreshBtn = moduleTreeOf(wrapper).findAll('.mm-tree-header__icon-btn')[1] await refreshBtn.trigger('click') await flushPromises() await nextTick() expect(loadDataApi).toHaveBeenCalledWith('s_module', '1 = 1', 'b_xh ASC, b_id ASC') }) it('模块右键:四种新建类型(分类/数据/查询)', async () => { const wrapper = await mountPage() const row = moduleTreeOf(wrapper).find('.tree-node') await row.trigger('contextmenu') await nextTick() const labels = [...document.querySelectorAll('.tree-context-menu__item')].map((el) => el.textContent.trim(), ) expect(labels).toContain('新增分类') expect(labels).toContain('新增数据模块') expect(labels).toContain('新增查询模块') }) it('分类节点:分组导航仅显示概览(无数据结构/展示配置/业务能力/多语言)', async () => { const wrapper = await mountPage() await selectModule(wrapper, '系统') const titles = wrapper.findAll('.mm-config-nav__group-title').map((t) => t.text()) expect(titles).toEqual(['概览']) }) it('查询模块:分组导航无「编辑配置」「自动编码」', async () => { const wrapper = await mountPage() await selectModule(wrapper, '查询A') const labels = wrapper.findAll('.mm-config-nav__label').map((t) => t.text()) expect(labels).toContain('字段定义') expect(labels).toContain('查询配置') expect(labels).not.toContain('编辑配置') expect(labels).not.toContain('自动编码') }) it('点击「保存」按钮保存模块(saveObjectApi 调用)', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') saveObjectApi.mockClear() const saveBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('保存')) await saveBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() }) it('保存校验失败:不调用 saveObjectApi', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') basicValidateResult = [{ name: 'b_code', errors: ['必填'] }] saveObjectApi.mockClear() const saveBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('保存')) await saveBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).not.toHaveBeenCalled() }) it('新建数据模块后保存:inserts 写入 s_module,未保存标记清除', async () => { const wrapper = await mountPage() // 顶部下拉新建数据模块 const newBtn = moduleTreeOf(wrapper).find('.mm-tree-header__icon-btn') await newBtn.trigger('click') await nextTick() const addItem = [...document.querySelectorAll('.dropdown-item')].find((el) => el.textContent.includes('新增数据模块'), ) addItem.click() await flushPromises() await nextTick() // 新建草稿:显示「未保存」 expect(wrapper.text()).toContain('未保存') // 通过基本信息 stub 补齐必填字段 const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { b_id: '999', b_code: 'newmod', b_name: '新模块', b_module_type: 'data', b_parent_id: '1', b_canuse: 1, b_xh: 10, }) await nextTick() saveObjectApi.mockClear() const saveBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('保存')) await saveBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() // inserts 含 s_module const reqs = saveObjectApi.mock.calls[0][0] const moduleReq = reqs.find((r) => r.table === 's_module') expect(moduleReq).toBeTruthy() expect(moduleReq.inserts).toBeTruthy() // 保存成功后未保存标记清除 expect(wrapper.text()).not.toContain('未保存') }) it('重载:有未保存修改时弹出确认', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { ...moduleRows[1], b_name: '订单(改)' }) await nextTick() const reloadBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('重载')) await reloadBtn.trigger('click') await nextTick() const modal = wrapper.find('.stub-modal') expect(modal.exists()).toBe(true) // Modal stub 不渲染 title,断言正文与按钮 expect(modal.text()).toContain('确定放弃修改并重新加载数据') expect(modal.text()).toContain('重载') }) it('切换保护:取消 → 保持当前选中', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { ...moduleRows[1], b_name: '订单(改)' }) await nextTick() await moduleTreeOf(wrapper) .findAll('.tree-node-content') .find((el) => el.text().includes('系统')) .trigger('click') await nextTick() const modal = wrapper.find('.stub-modal') const cancelBtn = modal.findAll('button').find((b) => b.text().includes('取消')) await cancelBtn.trigger('click') await nextTick() // 未切换:header 仍是「订单」 expect(wrapper.find('.mm-header__name').text()).toContain('订单') }) it('切换保护:放弃修改 → 切换到目标节点', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { ...moduleRows[1], b_name: '订单(改)' }) await nextTick() await moduleTreeOf(wrapper) .findAll('.tree-node-content') .find((el) => el.text().includes('系统')) .trigger('click') await nextTick() const modal = wrapper.find('.stub-modal') const discardBtn = modal.findAll('button').find((b) => b.text().includes('放弃修改')) await discardBtn.trigger('click') await flushPromises() await nextTick() // 已切换到「系统」 expect(wrapper.find('.mm-header__name').text()).toContain('系统') }) it('切换保护:保存并切换 → 先保存再切换', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') const basicStub = wrapper.findComponent({ name: 'ModuleBasicPanel' }) basicStub.vm.$emit('update', { ...moduleRows[1], b_name: '订单(改)' }) await nextTick() saveObjectApi.mockClear() await moduleTreeOf(wrapper) .findAll('.tree-node-content') .find((el) => el.text().includes('系统')) .trigger('click') await nextTick() const modal = wrapper.find('.stub-modal') const saveBtn = modal.findAll('button').find((b) => b.text().includes('保存并切换')) await saveBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() // 已切换到「系统」 expect(wrapper.find('.mm-header__name').text()).toContain('系统') }) it('删除确认后执行:调用 saveObjectApi(含子表删除)', async () => { const wrapper = await mountPage() await selectModule(wrapper, '订单') saveObjectApi.mockClear() const row = moduleTreeOf(wrapper) .findAll('.tree-node') .find((el) => el.text().includes('订单')) await row.trigger('contextmenu') await nextTick() const deleteItem = [...document.querySelectorAll('.tree-context-menu__item')].find((el) => el.textContent.includes('删除模块'), ) deleteItem.click() await flushPromises() await nextTick() const modal = wrapper.find('.stub-modal') const okBtn = modal.findAll('button').find((b) => b.text().includes('删除')) await okBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() // 删除请求含 s_module 的 deletes const reqs = saveObjectApi.mock.calls[0][0] expect(reqs.some((r) => r.table === 's_module' && r.deletes?.length)).toBe(true) }) it('删除含已保存子模块的目录:拦截并提示,不弹确认', async () => { const wrapper = await mountPage() const row = moduleTreeOf(wrapper).find('.tree-node') // 系统(含已保存子模块) await row.trigger('contextmenu') await nextTick() const deleteItem = [...document.querySelectorAll('.tree-context-menu__item')].find((el) => el.textContent.includes('删除模块'), ) deleteItem.click() await flushPromises() await nextTick() expect(wrapper.find('.stub-modal').exists()).toBe(false) expect(Message.warning).toHaveBeenCalled() }) it('删除未保存草稿:不弹确认直接移除节点', async () => { const wrapper = await mountPage() const tree = moduleTreeOf(wrapper) // 先展开树(新建会自动展开祖先,计数以展开后为基准) const switcher = tree.find('.tree-node-switcher:not(.is-placeholder)') if (switcher.exists()) { await switcher.trigger('click') await nextTick() } const before = tree.findAll('.tree-node').length // 系统 + 订单 + 查询A // 顶部新建数据模块(草稿) const newBtn = tree.find('.mm-tree-header__icon-btn') await newBtn.trigger('click') await nextTick() const addItem = [...document.querySelectorAll('.dropdown-item')].find((el) => el.textContent.includes('新增数据模块'), ) addItem.click() await flushPromises() await nextTick() const afterNew = tree.findAll('.tree-node').length expect(afterNew).toBe(before + 1) // 右键新草稿节点(最后一个)删除 const rows = tree.findAll('.tree-node') await rows[rows.length - 1].trigger('contextmenu') await nextTick() const deleteItem = [...document.querySelectorAll('.tree-context-menu__item')].find((el) => el.textContent.includes('删除模块'), ) deleteItem.click() await flushPromises() await nextTick() // 无确认弹窗,节点已移除 expect(wrapper.find('.stub-modal').exists()).toBe(false) expect(tree.findAll('.tree-node').length).toBe(before) }) it('菜单:选中菜单后保存(saveMenu 调用 saveObjectApi)', async () => { const wrapper = await mountPage() const menuTab = wrapper.findAll('.tabs-tab').find((t) => t.text().includes('菜单配置')) await menuTab.trigger('click') await flushPromises() await nextTick() const menuTree = wrapper.findAll('.mm-tree-panel')[1] const rootContent = menuTree.find('.tree-node-content') await rootContent.trigger('click') await flushPromises() await nextTick() saveObjectApi.mockClear() const saveBtn = wrapper .findAll('.mm-header__actions button') .find((b) => b.text().includes('保存')) await saveBtn.trigger('click') await flushPromises() await nextTick() expect(saveObjectApi).toHaveBeenCalled() }) it('菜单:有未保存修改时切换节点弹出保护', async () => { const wrapper = await mountPage() const menuTab = wrapper.findAll('.tabs-tab').find((t) => t.text().includes('菜单配置')) await menuTab.trigger('click') await flushPromises() await nextTick() const menuTree = wrapper.findAll('.mm-tree-panel')[1] await menuTree.find('.tree-node-content').trigger('click') // 选中系统菜单 await flushPromises() await nextTick() // stub MenuDetailPanel 发出修改 const menuStub = wrapper.findComponent({ name: 'MenuDetailPanel' }) menuStub.vm.$emit('update', { ...menuRows[0], b_name: '系统菜单(改)' }) await nextTick() // 点击另一个根菜单「独立菜单」 const standalone = menuTree .findAll('.tree-node-content') .find((el) => el.text().includes('独立菜单')) await standalone.trigger('click') await nextTick() const modal = wrapper.find('.stub-modal') expect(modal.exists()).toBe(true) expect(modal.text()).toContain('有未保存的修改') }) }) describe('ModuleBasicPanel(基本信息面板)', () => { const stubComponents = { Form: { name: 'Form', props: ['columns', 'layout'], template: '
' }, FormItem: { name: 'FormItem', props: ['label', 'name', 'span'], template: '
' }, Input: { name: 'Input', props: ['modelValue'], emits: ['update:modelValue'], template: '', }, Select: { name: 'Select', props: ['modelValue'], template: '
' }, Switch: { name: 'Switch', props: ['modelValue'], template: '
' }, I18nQuickSetModal: true, } const dataModule = { b_id: '1', b_code: 'order', b_name: '订单', b_module_type: 'data', b_parent_id: null, b_canuse: 1, b_xh: 10, b_viewtable: 'v_order', b_keyfield: 'b_id', } const mountBasic = (data) => mount(ModuleBasicPanel, { props: { data, parentName: '', loading: false }, global: { stubs: stubComponents }, }) it('渲染三个区块标题:标识与归属 / 数据来源 / 行为设置', async () => { const wrapper = mountBasic({ ...dataModule }) await nextTick() const titles = wrapper.findAll('.mm-basic__section-title').map((t) => t.text()) expect(titles).toEqual(['标识与归属', '数据来源', '行为设置', '描述']) }) it('分类节点不显示「数据来源」区块', async () => { const wrapper = mountBasic({ ...dataModule, b_module_type: 'category' }) await nextTick() const titles = wrapper.findAll('.mm-basic__section-title').map((t) => t.text()) expect(titles).toEqual(['标识与归属', '行为设置', '描述']) }) it('表单为三列布局(Form columns=3)', async () => { const wrapper = mountBasic({ ...dataModule }) await nextTick() expect(wrapper.findComponent({ name: 'Form' }).props('columns')).toBe(3) }) it('修改字段后 emit update(携带新值)', async () => { const wrapper = mountBasic({ ...dataModule }) await nextTick() const nameInput = wrapper.findAll('.stub-input')[1] // 第二个输入框 = 模块名称 await nameInput.setValue('新订单名') const update = wrapper.emitted('update') expect(update).toBeTruthy() expect(update[0][0].b_name).toBe('新订单名') }) }) describe('MenuDetailPanel', () => { const mountMenu = (menu) => mount(MenuDetailPanel, { props: { menu, parentName: '', moduleOptions: [], powers: [], loading: false }, global: { stubs: { IconSelect: true, ModulePowersPanel: { name: 'ModulePowersPanel', template: '
' }, ModuleI18nPanel: true, I18nQuickSetModal: true, }, }, }) it('validate / saveI18n / markI18nSaved 同时暴露(defineExpose 未被覆盖)', async () => { const wrapper = mountMenu(ref({ b_id: '1', b_code: '', b_name: '', b_menu_type: 'directory', b_canuse: 1, }).value) await nextTick() expect(typeof wrapper.vm.validate).toBe('function') expect(typeof wrapper.vm.saveI18n).toBe('function') expect(typeof wrapper.vm.markI18nSaved).toBe('function') }) it('validate 对空必填返回错误字段,且校验时切回基本信息分区', async () => { const wrapper = mountMenu(ref({ b_id: '1', b_code: '', b_name: '', b_menu_type: 'page', b_route: '', b_canuse: 1, }).value) await nextTick() const errors = await wrapper.vm.validate() expect(errors.length).toBeGreaterThan(0) const names = errors.map((e) => e.name) expect(names).toContain('b_code') expect(names).toContain('b_name') }) it('目录类型不显示「权限配置」导航项', async () => { const wrapper = mountMenu(ref({ b_id: '1', b_code: 'm', b_name: '目录', b_menu_type: 'directory', b_canuse: 1, }).value) await nextTick() expect(wrapper.text()).toContain('基本信息') expect(wrapper.text()).not.toContain('权限配置') }) it('页面类型显示「权限配置」导航项与内容面板', async () => { const wrapper = mountMenu(ref({ b_id: '1', b_code: 'p', b_name: '页面', b_menu_type: 'page', b_route: '/x', b_canuse: 1, }).value) await nextTick() expect(wrapper.text()).toContain('权限配置') // 点击导航切换到权限面板 const item = wrapper.findAll('.mm-config-nav__item').find((el) => el.text().includes('权限配置')) await item.trigger('click') await nextTick() expect(wrapper.find('.stub-menu-powers').exists()).toBe(true) }) })