Files
workspace/code/fms/fms-vue/tests/views/module-management.spec.js
T
2026-08-22 17:30:20 +08:00

801 lines
30 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.
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: '<div class="stub-basic" />',
methods: { validate() { return Promise.resolve(basicValidateResult) } },
},
ModuleFieldsPanel: { name: 'ModuleFieldsPanel', template: '<div class="stub-fields" />' },
ModuleListConfigPanel: { name: 'ModuleListConfigPanel', template: '<div class="stub-list" />' },
ModuleEditConfigPanel: { name: 'ModuleEditConfigPanel', template: '<div class="stub-edit" />' },
ModuleQueryConfigPanel: { name: 'ModuleQueryConfigPanel', template: '<div class="stub-query" />' },
ModuleAutoCodePanel: { name: 'ModuleAutoCodePanel', template: '<div class="stub-autocode" />' },
ModulePowersPanel: { name: 'ModulePowersPanel', template: '<div class="stub-powers" />' },
ModuleI18nPanel: { name: 'ModuleI18nPanel', template: '<div class="stub-i18n" />' },
MenuDetailPanel: { name: 'MenuDetailPanel', template: '<div class="stub-menu" />' },
// Modal 尊重 open / footer 条件:footer=false 时不渲染按钮区(与真实组件一致)
Modal: {
name: 'Modal',
props: ['open', 'title', 'okText', 'okType', 'confirmLoading', 'footer'],
template:
'<div v-if="open" class="stub-modal"><slot /><div v-if="footer !== false" class="stub-modal-footer"><slot name="footer" /></div></div>',
},
Transition: { template: '<slot />' },
}
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: '<div class="stub-form"><slot /></div>' },
FormItem: { name: 'FormItem', props: ['label', 'name', 'span'], template: '<div class="stub-form-item"><slot /></div>' },
Input: {
name: 'Input',
props: ['modelValue'],
emits: ['update:modelValue'],
template:
'<input class="stub-input" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
},
Select: { name: 'Select', props: ['modelValue'], template: '<div class="stub-select" />' },
Switch: { name: 'Switch', props: ['modelValue'], template: '<div class="stub-switch" />' },
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: '<div class="stub-menu-powers" />' },
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)
})
})