20260914223013

This commit is contained in:
oneao committed 2026-09-14 22:30:13 +08:00
1 parent 549c8bd2eb
commit 63514476d7
60 files changed
+6478 -2004

No files matched your search

@@ -0,0 +1,195 @@
/**
* 回归用例:菜单管理 - 绑定模块(s_menu_module)的加载与保存。
*
* s_menu 表没有模块列:绑定关系存 s_menu_module,行身份 = (b_menu_id, b_module_id),
* 只登记 data / virtual 模块(module 只是模块树上的分类,不参与权限)。
* 保存时与加载基线做行级 diff,只提交增删行;目录 / 外链菜单不绑定模块。
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import MenuManagement from '@/views/module/menu-management/index.vue'
import { mountPage } from '../helpers/app.js'
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn(),
saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })),
nextIdApi: vi.fn(() => Promise.resolve({ data: 9001, code: 0, success: true })),
describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })),
}))
vi.mock('@/components/ui/message/message-manager.js', () => {
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
return { Message: m, default: m }
})
import { loadDataApi, saveObjectApi } from '@/services/api'
const directoryMenu = {
b_id: 'base',
b_parent_id: null,
b_depth: 0,
b_path: '/base/',
b_name: '基础资料',
b_i18n: 'menu.base',
b_menu_type: 'directory',
b_route: null,
b_icon: null,
b_xh: 10,
b_canuse: 1,
}
const pageMenu = {
b_id: 'otherdata',
b_parent_id: 'base',
b_depth: 1,
b_path: '/base/otherdata/',
b_name: '其他数据',
b_i18n: 'menu.otherdata',
b_menu_type: 'page',
b_route: '/base/otherdata',
b_icon: null,
b_xh: 10,
b_canuse: 1,
}
/** 模块清单:base / base_otherdata 为分类节点,b_port / b_money 为数据模块 */
const modules = [
{
b_id: 'base',
b_parent_id: null,
b_depth: 0,
b_path: '/base/',
b_module_type: 'module',
b_name: '基础资料',
b_canuse: 1,
b_xh: 10,
},
{
b_id: 'base_otherdata',
b_parent_id: 'base',
b_depth: 1,
b_path: '/base/base_otherdata/',
b_module_type: 'module',
b_name: '其他数据',
b_canuse: 1,
b_xh: 30,
},
{
b_id: 'b_port',
b_parent_id: 'base_otherdata',
b_depth: 2,
b_path: '/base/base_otherdata/b_port/',
b_module_type: 'data',
b_name: '港口',
b_canuse: 1,
b_xh: 10,
},
{
b_id: 'b_money',
b_parent_id: 'base_otherdata',
b_depth: 2,
b_path: '/base/base_otherdata/b_money/',
b_module_type: 'data',
b_name: '币种',
b_canuse: 1,
b_xh: 20,
},
]
/** 库中 s_menu_module 的现有行(每个用例前重置) */
let boundRows = []
/** 从查询条件里取 N'xxx' 字面量(mock 按 id 过滤用;'1 = 1' 等条件返回 null) */
function literalOf(condition) {
return /N'([^']*)'/.exec(String(condition || ''))?.[1] ?? null
}
beforeEach(() => {
boundRows = [{ b_menu_id: 'otherdata', b_module_id: 'b_port', b_xh: 10, b_canuse: 1 }]
loadDataApi.mockReset()
saveObjectApi.mockClear()
loadDataApi.mockImplementation((view, condition) => {
if (view === 's_menu') {
const id = literalOf(condition)
const rows = [directoryMenu, pageMenu].filter((row) => !id || row.b_id === id)
return Promise.resolve({ data: rows })
}
if (view === 's_module') return Promise.resolve({ data: modules })
if (view === 's_menu_module') {
const id = literalOf(condition)
return Promise.resolve({
data: boundRows.filter((row) => !id || row.b_menu_id === id),
})
}
return Promise.resolve({ data: [] })
})
})
/** 取出最近一次保存请求里的指定表请求 */
function lastRequestOf(table) {
const requests = saveObjectApi.mock.calls.at(-1)[0]
return requests.find((request) => request.table === table)
}
describe('菜单管理:绑定模块(s_menu_module)', () => {
it('选中菜单时按 b_menu_id 加载绑定模块', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
expect(loadDataApi).toHaveBeenCalledWith(
's_menu_module',
"b_menu_id = N'otherdata'",
'b_xh ASC, b_module_id ASC',
)
expect(wrapper.vm.menuModules).toEqual(['b_port'])
})
it('新增绑定:只提交 s_menu_module 的 insert 行', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
wrapper.vm.menuModules = ['b_port', 'b_money']
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.key_field).toBe('b_menu_id,b_module_id')
expect(request.inserts).toEqual([
{ b_menu_id: 'otherdata', b_module_id: 'b_money', b_xh: 20, b_canuse: 1 },
])
expect(request.deletes).toBeUndefined()
})
it('移除绑定:只提交 s_menu_module 的 delete 主键行', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'otherdata'
await flushPromises()
wrapper.vm.menuModules = []
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.deletes).toEqual([{ b_menu_id: 'otherdata', b_module_id: 'b_port' }])
expect(request.inserts).toBeUndefined()
})
it('目录菜单不绑定模块:保存时清空并删除既有关系行', async () => {
boundRows = [{ b_menu_id: 'base', b_module_id: 'b_port', b_xh: 10, b_canuse: 1 }]
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
wrapper.vm.menuSelectedId = 'base'
await flushPromises()
expect(wrapper.vm.menuModules).toEqual(['b_port'])
await wrapper.vm.handleSave()
const request = lastRequestOf('s_menu_module')
expect(request.deletes).toEqual([{ b_menu_id: 'base', b_module_id: 'b_port' }])
})
})
@@ -10,9 +10,8 @@
* 3) 分类编码 b_id 是主键,修改时只提交名称与排序号,不能改编码。
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { flushPromises } from '@vue/test-utils'
import Othercompany from '@/views/base/othercompany/index.vue'
import CategoryTreePanel from '@/views/base/othercompany/CategoryTreePanel.vue'
import { mountPage } from '../helpers/app.js'
vi.mock('@/services/api', () => ({
@@ -28,8 +27,14 @@ vi.mock('@/components/ui/message/message-manager.js', () => {
return { Message: m, default: m }
})
// Modal.confirm 是命令式对话框:mock 掉后由用例手动触发 onOk,模拟用户点「确定」
vi.mock('@/components/ui/modal/modal-confirm.js', () => ({
confirm: vi.fn(),
}))
import { loadDataApi, pageDataApi, saveObjectApi } from '@/services/api'
import { Message } from '@/components/ui/message/message-manager.js'
import { confirm as modalConfirm } from '@/components/ui/modal/modal-confirm.js'
const ROOT1 = {
b_id: 'root1',
@@ -70,6 +75,7 @@ beforeEach(() => {
Message.success.mockClear()
Message.error.mockClear()
Message.warning.mockClear()
modalConfirm.mockReset()
loadDataApi.mockImplementation((view) => {
if (view === 'b_othercompany_bmfl')
return Promise.resolve({ data: CATEGORIES.map((row) => ({ ...row })) })
@@ -157,8 +163,8 @@ describe('往来单位:删除分类的两条守卫', () => {
expect(Message.warning).toHaveBeenCalledWith('该分类下还有子分类,请先删除子分类')
expect(pageDataApi).not.toHaveBeenCalled()
expect(modalConfirm).not.toHaveBeenCalled()
expect(saveObjectApi).not.toHaveBeenCalled()
expect(wrapper.vm.categoryDeleteTarget).toBe(null)
})
it('分类下还有往来单位时拦下,不发删除请求', async () => {
@@ -168,18 +174,20 @@ describe('往来单位:删除分类的两条守卫', () => {
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
expect(Message.warning).toHaveBeenCalledWith('该分类下还有往来单位数据,不能删除')
expect(modalConfirm).not.toHaveBeenCalled()
expect(saveObjectApi).not.toHaveBeenCalled()
expect(wrapper.vm.categoryDeleteTarget).toBe(null)
})
it('守卫通过后弹确认,确认才落库删除', async () => {
const wrapper = await mountOthercompany()
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
expect(wrapper.vm.categoryDeleteTarget?.b_id).toBe('child1')
expect(modalConfirm).toHaveBeenCalledTimes(1)
expect(saveObjectApi).not.toHaveBeenCalled()
await wrapper.vm.confirmCategoryDelete()
// 模拟用户点「删除」
await modalConfirm.mock.calls[0][0].onOk()
await flushPromises()
const request = lastRequestOf('b_othercompany_bmfl')
@@ -221,36 +229,30 @@ describe('往来单位:拖拽分类', () => {
})
})
describe('往来单位分类树面板:搜索', () => {
describe('往来单位分类树:搜索', () => {
it('按名称与编码过滤,并把命中的层级展开', async () => {
// 面板收到的是已经建好的树(页面的 categoryTree)。这里整树真实渲染:
// Input / Button 与保留字同名,shallowMount 打桩会触发 Vue 保留名告警。
const treeData = [{ ...ROOT1, children: [{ ...CHILD1 }] }, { ...ROOT2 }]
const wrapper = mount(CategoryTreePanel, { props: { treeData } })
const wrapper = await mountOthercompany()
expect(wrapper.vm.filteredTree).toHaveLength(2)
expect(wrapper.emitted('update:expandedKeys')).toBeUndefined()
// 初始:整树两棵根(root1 带子节点 child1、root2),含子节点的分类保持展开
expect(wrapper.vm.filteredCategoryTree.map((node) => node.b_id)).toEqual(['root1', 'root2'])
expect(wrapper.vm.expandedCategoryKeys).toEqual(['root1'])
// 命中子分类的编码:保留其祖先,保证命中节点可见
wrapper.vm.keyword = 'child1'
wrapper.vm.categoryKeyword = 'child1'
await flushPromises()
expect(wrapper.vm.filteredTree.map((node) => node.b_id)).toEqual(['root1'])
expect(wrapper.vm.filteredTree[0].children.map((node) => node.b_id)).toEqual(['child1'])
expect(wrapper.vm.filteredCategoryTree.map((node) => node.b_id)).toEqual(['root1'])
expect(wrapper.vm.filteredCategoryTree[0].children.map((node) => node.b_id)).toEqual(['child1'])
// 搜索中展开全部,保证命中节点可见
expect(wrapper.emitted('update:expandedKeys')?.at(-1)?.[0]).toEqual([
'root1',
'child1',
'root2',
])
expect(wrapper.vm.expandedCategoryKeys).toEqual(['root1', 'child1', 'root2'])
// 命中根分类的名称
wrapper.vm.keyword = '另一个'
wrapper.vm.categoryKeyword = '另一个'
await flushPromises()
expect(wrapper.vm.filteredTree.map((node) => node.b_id)).toEqual(['root2'])
expect(wrapper.vm.filteredCategoryTree.map((node) => node.b_id)).toEqual(['root2'])
// 清空搜索后收起
wrapper.vm.keyword = ''
wrapper.vm.categoryKeyword = ''
await flushPromises()
expect(wrapper.emitted('update:expandedKeys')?.at(-1)?.[0]).toEqual([])
expect(wrapper.vm.expandedCategoryKeys).toEqual([])
})
})