Files
workspace/code/fms/fms-vue/tests/views/othercompany.spec.js
T
2026-09-17 16:58:40 +08:00

277 lines
9.4 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.
/**
* 回归用例:往来单位分类树(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([])
})
})