277 lines
9.4 KiB
JavaScript
277 lines
9.4 KiB
JavaScript
/**
|
||
* 回归用例:往来单位分类树(b_othercompany_category)的新增 / 修改 / 删除 / 拖拽排序。
|
||
*
|
||
* 1) b_path 为 not null 且无默认值(b_depth / b_xh / b_canuse 才有默认值),新增分类时
|
||
* 必须按父级派生 b_depth / b_path 一起写入,否则插入直接失败;
|
||
* 且两个派生字段与 b_parent_id 必须同批落库,否则会留下「路径按新父级、父级列还是旧值」。
|
||
*
|
||
* 2) 删除分类有两条守卫:还有子分类、分类下还挂着往来单位。两条都必须拦在发请求之前。
|
||
*
|
||
* 3) 分类编码 b_id 是主键,修改时只提交名称与排序号,不能改编码。
|
||
*/
|
||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||
import { flushPromises } from '@vue/test-utils'
|
||
import Othercompany from '@/views/base/othercompany/index.vue'
|
||
import { mountPage } from '../helpers/app.js'
|
||
|
||
vi.mock('@/services/api', () => ({
|
||
loadDataApi: vi.fn(),
|
||
pageDataApi: 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 }
|
||
})
|
||
|
||
// 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',
|
||
b_parent_id: null,
|
||
b_depth: 0,
|
||
b_path: '/root1/',
|
||
b_name: '一级分类',
|
||
b_xh: 10,
|
||
b_canuse: 1,
|
||
}
|
||
const ROOT2 = {
|
||
b_id: 'root2',
|
||
b_parent_id: null,
|
||
b_depth: 0,
|
||
b_path: '/root2/',
|
||
b_name: '另一个一级分类',
|
||
b_xh: 20,
|
||
b_canuse: 1,
|
||
}
|
||
const CHILD1 = {
|
||
b_id: 'child1',
|
||
b_parent_id: 'root1',
|
||
b_depth: 1,
|
||
b_path: '/root1/child1/',
|
||
b_name: '二级分类',
|
||
b_xh: 10,
|
||
b_canuse: 1,
|
||
}
|
||
|
||
const CATEGORIES = [ROOT1, ROOT2, CHILD1]
|
||
|
||
beforeEach(() => {
|
||
localStorage.clear()
|
||
sessionStorage.clear()
|
||
loadDataApi.mockReset()
|
||
pageDataApi.mockReset()
|
||
saveObjectApi.mockClear()
|
||
Message.success.mockClear()
|
||
Message.error.mockClear()
|
||
Message.warning.mockClear()
|
||
modalConfirm.mockReset()
|
||
loadDataApi.mockImplementation((view) => {
|
||
if (view === 'b_othercompany_category')
|
||
return Promise.resolve({ data: CATEGORIES.map((row) => ({ ...row })) })
|
||
return Promise.resolve({ data: [] })
|
||
})
|
||
pageDataApi.mockResolvedValue({ data: { rows: [], total: 0 } })
|
||
})
|
||
|
||
/** 取出最近一次保存请求里的指定表请求 */
|
||
function lastRequestOf(table) {
|
||
const requests = saveObjectApi.mock.calls.at(-1)?.[0] || []
|
||
return requests.find((request) => request.table === table)
|
||
}
|
||
|
||
/** 汇总最近一次保存请求里全部 updates 行 */
|
||
function lastUpdates() {
|
||
const requests = saveObjectApi.mock.calls.at(-1)?.[0] || []
|
||
return requests.flatMap((request) => request.updates || [])
|
||
}
|
||
|
||
async function mountOthercompany() {
|
||
const { wrapper } = await mountPage(Othercompany)
|
||
await flushPromises()
|
||
return wrapper
|
||
}
|
||
|
||
describe('往来单位:新增分类写派生树字段', () => {
|
||
it('新增顶级分类写入 b_parent_id=null 与根路径', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
wrapper.vm.onCategoryAdd({ parentId: null })
|
||
await wrapper.vm.onCategorySubmit({ b_id: 'newcat', b_name: '新分类', b_xh: 30 })
|
||
await flushPromises()
|
||
|
||
const request = lastRequestOf('b_othercompany_category')
|
||
expect(request.key_field).toBe('b_id')
|
||
expect(request.inserts).toHaveLength(1)
|
||
expect(request.inserts[0]).toMatchObject({
|
||
b_id: 'newcat',
|
||
b_parent_id: null,
|
||
b_depth: 0,
|
||
b_path: '/newcat/',
|
||
b_name: '新分类',
|
||
b_xh: 30,
|
||
b_canuse: 1,
|
||
})
|
||
})
|
||
|
||
it('新增子分类按父级路径拼出 b_path 并递增 b_depth', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
wrapper.vm.onCategoryAdd({ parentId: 'root1' })
|
||
await wrapper.vm.onCategorySubmit({ b_id: 'child2', b_name: '三级分类', b_xh: 20 })
|
||
await flushPromises()
|
||
|
||
expect(lastRequestOf('b_othercompany_category').inserts[0]).toMatchObject({
|
||
b_id: 'child2',
|
||
b_parent_id: 'root1',
|
||
b_depth: 1,
|
||
b_path: '/root1/child2/',
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('往来单位:修改分类', () => {
|
||
it('只提交名称与排序号,分类编码保持为主键值', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
wrapper.vm.onCategoryEdit({ b_id: 'child1' })
|
||
await wrapper.vm.onCategorySubmit({ b_id: 'child1', b_name: '改过的名字', b_xh: 99 })
|
||
await flushPromises()
|
||
|
||
const request = lastRequestOf('b_othercompany_category')
|
||
expect(request.inserts).toBeUndefined()
|
||
expect(request.updates).toHaveLength(1)
|
||
expect(request.updates[0]).toEqual({ b_id: 'child1', b_name: '改过的名字', b_xh: 99 })
|
||
})
|
||
})
|
||
|
||
describe('往来单位:删除分类的两条守卫', () => {
|
||
it('还有子分类时直接拦下,不发任何请求', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
await wrapper.vm.onCategoryDelete({ b_id: 'root1' })
|
||
|
||
expect(Message.warning).toHaveBeenCalledWith('该分类下还有子分类,请先删除子分类')
|
||
expect(pageDataApi).not.toHaveBeenCalled()
|
||
expect(modalConfirm).not.toHaveBeenCalled()
|
||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('守卫查询带 order_by:/data/page 缺排序字段后端直接报「order_by 不能为空」', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
|
||
|
||
expect(pageDataApi).toHaveBeenCalledTimes(1)
|
||
expect(pageDataApi.mock.calls[0][0]).toMatchObject({
|
||
viewName: 'v_b_othercompany',
|
||
orderBy: 'b_id ASC',
|
||
searchCondition: "[b_cate_id] = 'child1'",
|
||
searchColumns: ['b_id'],
|
||
pageNo: 1,
|
||
pageSize: 1,
|
||
})
|
||
// 带上排序字段后守卫能正常通过,不再走错误分支
|
||
expect(Message.error).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('分类下还有往来单位时拦下,不发删除请求', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
pageDataApi.mockResolvedValueOnce({ data: { rows: [{ b_id: 'OC001' }], total: 1 } })
|
||
|
||
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
|
||
|
||
expect(Message.warning).toHaveBeenCalledWith('该分类下还有往来单位数据,不能删除')
|
||
expect(modalConfirm).not.toHaveBeenCalled()
|
||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('守卫通过后弹确认,确认才落库删除', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
|
||
|
||
expect(modalConfirm).toHaveBeenCalledTimes(1)
|
||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||
|
||
// 模拟用户点「删除」
|
||
await modalConfirm.mock.calls[0][0].onOk()
|
||
await flushPromises()
|
||
|
||
const request = lastRequestOf('b_othercompany_category')
|
||
expect(request.deletes).toEqual([{ b_id: 'child1' }])
|
||
})
|
||
})
|
||
|
||
describe('往来单位:拖拽分类', () => {
|
||
it('跨级拖入时同批落库 b_parent_id 与重建后的 b_depth / b_path', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
await wrapper.vm.onCategoryDrop({
|
||
dragKey: 'child1',
|
||
dropKey: 'root2',
|
||
dropPosition: 'inside',
|
||
})
|
||
await flushPromises()
|
||
|
||
const moved = lastUpdates().find((row) => row.b_id === 'child1')
|
||
expect(moved).toMatchObject({
|
||
b_id: 'child1',
|
||
b_parent_id: 'root2',
|
||
b_depth: 1,
|
||
b_path: '/root2/child1/',
|
||
})
|
||
})
|
||
|
||
it('不能把分类拖到它自己的下级', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
await wrapper.vm.onCategoryDrop({
|
||
dragKey: 'root1',
|
||
dropKey: 'child1',
|
||
dropPosition: 'inside',
|
||
})
|
||
|
||
expect(Message.warning).toHaveBeenCalledWith('不能把分类移动到它自己的下级')
|
||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||
})
|
||
})
|
||
|
||
describe('往来单位分类树:搜索', () => {
|
||
it('按名称与编码过滤,并把命中的层级展开', async () => {
|
||
const wrapper = await mountOthercompany()
|
||
|
||
// 初始:整树两棵根(root1 带子节点 child1、root2),含子节点的分类保持展开
|
||
expect(wrapper.vm.filteredCategoryTree.map((node) => node.b_id)).toEqual(['root1', 'root2'])
|
||
expect(wrapper.vm.expandedCategoryKeys).toEqual(['root1'])
|
||
|
||
// 命中子分类的编码:保留其祖先,保证命中节点可见
|
||
wrapper.vm.categoryKeyword = 'child1'
|
||
await flushPromises()
|
||
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.vm.expandedCategoryKeys).toEqual(['root1', 'child1', 'root2'])
|
||
|
||
// 命中根分类的名称
|
||
wrapper.vm.categoryKeyword = '另一个'
|
||
await flushPromises()
|
||
expect(wrapper.vm.filteredCategoryTree.map((node) => node.b_id)).toEqual(['root2'])
|
||
|
||
// 清空搜索后收起
|
||
wrapper.vm.categoryKeyword = ''
|
||
await flushPromises()
|
||
expect(wrapper.vm.expandedCategoryKeys).toEqual([])
|
||
})
|
||
})
|