20260915170028
This commit is contained in:
1 parent
d26afb5fec
commit
1f56cc3efb
89 files changed
+1135
-397
No files matched your search
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 模块绑定弹层(ModulePickerModal):模块树默认全部收起。
|
||||
*
|
||||
* 回归点:弹层曾用 default-expanded-keys 展开第一层根节点,一打开就铺开整棵树;
|
||||
* 现在不设该 prop,初始只渲染根节点,子节点由用户按需逐层点开。
|
||||
* 折叠态下子树不渲染(tree-node.vue 用 v-if 而非 v-show),因此可直接数 treeitem。
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import ModulePickerModal from '@/views/module/menu-management/ModulePickerModal.vue'
|
||||
|
||||
const TREE = [
|
||||
{
|
||||
b_id: 'base',
|
||||
b_module_type: 'module',
|
||||
b_name: '基础资料',
|
||||
children: [
|
||||
{
|
||||
b_id: 'base_otherdata',
|
||||
b_module_type: 'module',
|
||||
b_name: '其他数据',
|
||||
children: [{ b_id: 'b_port', b_module_type: 'data', b_name: '港口' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: 'system',
|
||||
b_module_type: 'module',
|
||||
b_name: '系统',
|
||||
children: [{ b_id: 'b_user', b_module_type: 'data', b_name: '用户' }],
|
||||
},
|
||||
]
|
||||
|
||||
const MODULE_TYPES = {
|
||||
base: 'module',
|
||||
base_otherdata: 'module',
|
||||
b_port: 'data',
|
||||
system: 'module',
|
||||
b_user: 'data',
|
||||
}
|
||||
|
||||
async function mountModal(props = {}) {
|
||||
const wrapper = mount(ModulePickerModal, {
|
||||
props: {
|
||||
open: true,
|
||||
treeData: TREE,
|
||||
checkedKeys: [],
|
||||
moduleTypes: MODULE_TYPES,
|
||||
...props,
|
||||
},
|
||||
// Modal 用 Teleport 挂到 body;打桩后内容留在组件树内,便于断言
|
||||
global: { stubs: { teleport: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
describe('模块绑定弹层:树默认收起', () => {
|
||||
it('初始只渲染根节点,没有任何节点处于展开态', async () => {
|
||||
const wrapper = await mountModal()
|
||||
|
||||
const items = wrapper.findAll('[role="treeitem"]')
|
||||
expect(items).toHaveLength(TREE.length)
|
||||
expect(items.map((item) => item.attributes('aria-expanded'))).toEqual([
|
||||
'false',
|
||||
'false',
|
||||
])
|
||||
})
|
||||
|
||||
it('折叠态下子节点不渲染', async () => {
|
||||
const wrapper = await mountModal()
|
||||
|
||||
expect(wrapper.text()).toContain('基础资料')
|
||||
expect(wrapper.text()).not.toContain('其他数据')
|
||||
expect(wrapper.text()).not.toContain('港口')
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* 往来单位分类树页面级纯函数。
|
||||
* 往来单位分类树的页面级纯函数(仍内联在 views/base/othercompany/index.vue)。
|
||||
*
|
||||
* 页面单文件自包含后(分类树逻辑内联在 views/base/othercompany/index.vue),
|
||||
* 纯函数不再单独导出,测试通过挂载页面组件访问 <script setup> 的内部绑定。
|
||||
*
|
||||
* 重点是 buildCategoryTree 不能污染原始行:buildTree 会就地往行对象上挂 children,
|
||||
* 若直接在 categories 的行上构建,重复构建会累积出重复子节点。
|
||||
* 建树、位置派生、排序号重排、整树重建等通用逻辑已收敛到 @/utils/tree,
|
||||
* 其测试在 tests/unit/utils.spec.js;本文件只覆盖留在页面里的两个业务判定,
|
||||
* 它们内联在页面中,因此通过挂载页面组件访问 <script setup> 的内部绑定。
|
||||
*/
|
||||
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
@@ -42,50 +40,6 @@ beforeEach(async () => {
|
||||
vm = wrapper.vm
|
||||
})
|
||||
|
||||
describe('othercompany 分类树工具:buildCategoryTree', () => {
|
||||
it('按 b_parent_id 建树并按 b_xh 排序', () => {
|
||||
const rows = [
|
||||
category({ b_id: 'b', b_xh: 20, b_name: 'B' }),
|
||||
category({ b_id: 'a', b_xh: 10, b_name: 'A' }),
|
||||
category({ b_id: 'a1', b_parent_id: 'a', b_depth: 1, b_path: '/a/a1/', b_name: 'A1' }),
|
||||
]
|
||||
|
||||
const tree = vm.buildCategoryTree(rows)
|
||||
|
||||
expect(tree.map((node) => node.b_id)).toEqual(['a', 'b'])
|
||||
expect(tree[0].children.map((node) => node.b_id)).toEqual(['a1'])
|
||||
})
|
||||
|
||||
it('不污染原始行,重复构建不会累积出重复子节点', () => {
|
||||
const rows = [
|
||||
category({ b_id: 'a' }),
|
||||
category({ b_id: 'a1', b_parent_id: 'a', b_depth: 1, b_path: '/a/a1/' }),
|
||||
]
|
||||
|
||||
vm.buildCategoryTree(rows)
|
||||
// buildTree 的 children 挂在浅拷贝上,原始行保持干净
|
||||
expect(rows[0].children).toBeUndefined()
|
||||
|
||||
const tree = vm.buildCategoryTree(rows)
|
||||
expect(tree[0].children).toHaveLength(1)
|
||||
expect(tree[0].children[0].b_id).toBe('a1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('othercompany 分类树工具:位置派生', () => {
|
||||
it('根节点的 b_depth 为 0、b_path 为 /编码/', () => {
|
||||
const row = { b_id: 'a' }
|
||||
vm.applyCategoryPosition(row, null)
|
||||
expect(row).toMatchObject({ b_depth: 0, b_path: '/a/' })
|
||||
})
|
||||
|
||||
it('子节点继承父级路径并递增层级', () => {
|
||||
const row = { b_id: 'a1' }
|
||||
vm.applyCategoryPosition(row, { b_depth: 2, b_path: '/a/b/' })
|
||||
expect(row).toMatchObject({ b_depth: 3, b_path: '/a/b/a1/' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('othercompany 分类树工具:排序号与子分类判定', () => {
|
||||
it('同级最大 b_xh 加 10,无同级时取 10', () => {
|
||||
const rows = [
|
||||
@@ -113,62 +67,6 @@ describe('othercompany 分类树工具:排序号与子分类判定', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('othercompany 分类树工具:重排与重建位置', () => {
|
||||
it('renumberSiblings 按 10 步长重排并只收集变化的行', () => {
|
||||
const a = { b_id: 'a', b_xh: 30 }
|
||||
const b = { b_id: 'b', b_xh: 10 }
|
||||
const updated = []
|
||||
|
||||
vm.renumberSiblings([a, b], updated)
|
||||
|
||||
expect([a.b_xh, b.b_xh]).toEqual([10, 20])
|
||||
expect(updated).toEqual([
|
||||
{ b_id: 'a', b_xh: 10 },
|
||||
{ b_id: 'b', b_xh: 20 },
|
||||
])
|
||||
})
|
||||
|
||||
it('rebuildTreePositions 整树收敛 b_depth / b_path,未变化的不重复收集', () => {
|
||||
const tree = [
|
||||
{
|
||||
b_id: 'a',
|
||||
b_depth: 0,
|
||||
b_path: '/a/',
|
||||
children: [{ b_id: 'a1', b_depth: 9, b_path: '/stale/', children: [] }],
|
||||
},
|
||||
]
|
||||
const updated = []
|
||||
|
||||
vm.rebuildTreePositions(tree, updated)
|
||||
|
||||
expect(tree[0].children[0]).toMatchObject({ b_depth: 1, b_path: '/a/a1/' })
|
||||
// 根节点本来就是 0 / '/a/',不该产生一条无意义的 update
|
||||
expect(updated).toEqual([{ b_id: 'a1', b_depth: 1, b_path: '/a/a1/' }])
|
||||
})
|
||||
|
||||
it('addRowUpdate 按 b_id 合并字段,避免同节点多条 update', () => {
|
||||
const updated = []
|
||||
vm.addRowUpdate(updated, { b_id: 'a', b_xh: 20 })
|
||||
vm.addRowUpdate(updated, { b_id: 'a', b_parent_id: 'p' })
|
||||
vm.addRowUpdate(updated, { b_id: 'b', b_xh: 10 })
|
||||
|
||||
expect(updated).toEqual([
|
||||
{ b_id: 'a', b_xh: 20, b_parent_id: 'p' },
|
||||
{ b_id: 'b', b_xh: 10 },
|
||||
])
|
||||
})
|
||||
|
||||
it('findNodePosition 返回所在父数组与索引', () => {
|
||||
const tree = [{ b_id: 'a', children: [{ b_id: 'a1' }, { b_id: 'a2' }] }]
|
||||
|
||||
expect(vm.findNodePosition(tree, 'a')).toMatchObject({ index: 0 })
|
||||
const found = vm.findNodePosition(tree, 'a2')
|
||||
expect(found.index).toBe(1)
|
||||
expect(found.list.map((node) => node.b_id)).toEqual(['a1', 'a2'])
|
||||
expect(vm.findNodePosition(tree, 'not-exist')).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('othercompany 分类树工具:虚拟根键', () => {
|
||||
it('与 FmsTree 的 rootKey 约定保持一致', () => {
|
||||
expect(vm.ALL_CATEGORY_KEY).toBe('__fms_tree_root__')
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { buildTree, sortTree } from '@/utils/tree'
|
||||
import {
|
||||
addRowUpdate,
|
||||
applyTreePosition,
|
||||
buildSortedTree,
|
||||
buildTree,
|
||||
decorateTreeNodes,
|
||||
findNodePosition,
|
||||
rebuildTreePositions,
|
||||
renumberSiblings,
|
||||
sortTree,
|
||||
} from '@/utils/tree'
|
||||
|
||||
describe('utils/tree buildTree', () => {
|
||||
it('空数组返回空树', () => {
|
||||
@@ -91,6 +101,146 @@ describe('utils/tree sortTree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('utils/tree 可编辑树:建树与节点装饰', () => {
|
||||
it('buildSortedTree 建树并排序,且不污染原始行(重复构建不累积子节点)', () => {
|
||||
const rows = [
|
||||
{ b_id: 'a', b_parent_id: null, b_xh: 20 },
|
||||
{ b_id: 'b', b_parent_id: null, b_xh: 10 },
|
||||
{ b_id: 'a1', b_parent_id: 'a', b_xh: 10 },
|
||||
]
|
||||
|
||||
const tree = buildSortedTree(rows)
|
||||
|
||||
expect(tree.map((node) => node.b_id)).toEqual(['b', 'a'])
|
||||
expect(tree[1].children.map((node) => node.b_id)).toEqual(['a1'])
|
||||
// children 挂在浅拷贝上,原始行保持干净
|
||||
expect(rows[0].children).toBeUndefined()
|
||||
expect(buildSortedTree(rows)[1].children).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('decorateTreeNodes 预载类型图标,并强制指定类型非叶子', () => {
|
||||
const Folder = () => null
|
||||
const tree = [
|
||||
{
|
||||
b_id: 'a',
|
||||
b_module_type: 'module',
|
||||
children: [{ b_id: 'a1', b_module_type: 'data', children: [] }],
|
||||
},
|
||||
]
|
||||
|
||||
decorateTreeNodes(tree, {
|
||||
typeField: 'b_module_type',
|
||||
nonLeafValues: ['module'],
|
||||
iconMap: { module: Folder },
|
||||
})
|
||||
|
||||
expect(tree[0].isLeaf).toBe(false)
|
||||
expect(tree[0].icon).toBe(Folder)
|
||||
// 非 module 类型不改 isLeaf,图标表里没有的类型不写 icon
|
||||
expect(tree[0].children[0].isLeaf).toBeUndefined()
|
||||
expect(tree[0].children[0].icon).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('utils/tree 可编辑树:位置派生与就地变更', () => {
|
||||
it('applyTreePosition 根节点的 b_depth 为 0、b_path 为 /编码/', () => {
|
||||
const row = { b_id: 'a' }
|
||||
applyTreePosition(row, null)
|
||||
expect(row).toMatchObject({ b_depth: 0, b_path: '/a/' })
|
||||
})
|
||||
|
||||
it('applyTreePosition 子节点继承父级路径并递增层级', () => {
|
||||
const row = { b_id: 'a1' }
|
||||
applyTreePosition(row, { b_depth: 2, b_path: '/a/b/' })
|
||||
expect(row).toMatchObject({ b_depth: 3, b_path: '/a/b/a1/' })
|
||||
})
|
||||
|
||||
it('findNodePosition 返回所在父数组与索引', () => {
|
||||
const tree = [{ b_id: 'a', children: [{ b_id: 'a1' }, { b_id: 'a2' }] }]
|
||||
|
||||
expect(findNodePosition(tree, 'a')).toMatchObject({ index: 0 })
|
||||
const found = findNodePosition(tree, 'a2')
|
||||
expect(found.index).toBe(1)
|
||||
expect(found.list.map((node) => node.b_id)).toEqual(['a1', 'a2'])
|
||||
expect(findNodePosition(tree, 'not-exist')).toBe(null)
|
||||
})
|
||||
|
||||
it('addRowUpdate 按 b_id 合并字段,避免同节点多条 update', () => {
|
||||
const updated = []
|
||||
addRowUpdate(updated, { b_id: 'a', b_xh: 20 })
|
||||
addRowUpdate(updated, { b_id: 'a', b_parent_id: 'p' })
|
||||
addRowUpdate(updated, { b_id: 'b', b_xh: 10 })
|
||||
|
||||
expect(updated).toEqual([
|
||||
{ b_id: 'a', b_xh: 20, b_parent_id: 'p' },
|
||||
{ b_id: 'b', b_xh: 10 },
|
||||
])
|
||||
})
|
||||
|
||||
it('renumberSiblings 按 10 步长重排并只收集变化的行', () => {
|
||||
const a = { b_id: 'a', b_xh: 30 }
|
||||
const b = { b_id: 'b', b_xh: 10 }
|
||||
const updated = []
|
||||
|
||||
renumberSiblings([a, b], updated)
|
||||
|
||||
expect([a.b_xh, b.b_xh]).toEqual([10, 20])
|
||||
expect(updated).toEqual([
|
||||
{ b_id: 'a', b_xh: 10 },
|
||||
{ b_id: 'b', b_xh: 20 },
|
||||
])
|
||||
})
|
||||
|
||||
it('rebuildTreePositions 整树收敛 b_depth / b_path,未变化的不重复收集', () => {
|
||||
const tree = [
|
||||
{
|
||||
b_id: 'a',
|
||||
b_depth: 0,
|
||||
b_path: '/a/',
|
||||
children: [{ b_id: 'a1', b_depth: 9, b_path: '/stale/', children: [] }],
|
||||
},
|
||||
]
|
||||
const updated = []
|
||||
|
||||
rebuildTreePositions(tree, updated)
|
||||
|
||||
expect(tree[0].children[0]).toMatchObject({ b_depth: 1, b_path: '/a/a1/' })
|
||||
// 根节点本来就是 0 / '/a/',不该产生一条无意义的 update
|
||||
expect(updated).toEqual([{ b_id: 'a1', b_depth: 1, b_path: '/a/a1/' }])
|
||||
})
|
||||
|
||||
it('rebuildTreePositions 根节点路径保留前导斜杠(回归:三份实现曾漂移)', () => {
|
||||
const tree = [
|
||||
{
|
||||
b_id: 'base',
|
||||
b_depth: 0,
|
||||
b_path: '/base/',
|
||||
children: [
|
||||
{ b_id: 'base_otherdata', b_depth: 1, b_path: '/base/base_otherdata/', children: [] },
|
||||
],
|
||||
},
|
||||
]
|
||||
const updated = []
|
||||
|
||||
rebuildTreePositions(tree, updated)
|
||||
|
||||
expect(tree[0].b_path).toBe('/base/')
|
||||
expect(tree[0].children[0].b_path).toBe('/base/base_otherdata/')
|
||||
// 与库中既有格式一致,整棵树都不该被改动
|
||||
expect(updated).toEqual([])
|
||||
})
|
||||
|
||||
it('rebuildTreePositions 修正过期路径时补全前导斜杠', () => {
|
||||
const tree = [{ b_id: 'base', b_depth: 3, b_path: 'stale/', children: [] }]
|
||||
const updated = []
|
||||
|
||||
rebuildTreePositions(tree, updated)
|
||||
|
||||
expect(tree[0]).toMatchObject({ b_depth: 0, b_path: '/base/' })
|
||||
expect(updated).toEqual([{ b_id: 'base', b_depth: 0, b_path: '/base/' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('utils/theme', () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.style.cssText = ''
|
||||
|
||||
Reference in new issue
Block a user