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)
})
})