801 lines
30 KiB
JavaScript
801 lines
30 KiB
JavaScript
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)
|
||
})
|
||
})
|