Files
workspace/code/fms/fms-vue/tests/unit/utils.spec.js
T
2026-09-15 17:00:29 +08:00

270 lines
8.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.
import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
addRowUpdate,
applyTreePosition,
buildSortedTree,
buildTree,
decorateTreeNodes,
findNodePosition,
rebuildTreePositions,
renumberSiblings,
sortTree,
} from '@/utils/tree'
describe('utils/tree buildTree', () => {
it('空数组返回空树', () => {
expect(buildTree([])).toEqual([])
})
it('无父节点的节点成为根', () => {
const list = [
{ b_id: 1, b_parent_id: null },
{ b_id: 2, b_parent_id: null },
]
const tree = buildTree(list)
expect(tree).toHaveLength(2)
expect(tree.map((n) => n.b_id)).toEqual([1, 2])
})
it('按 parentKey 挂载子节点', () => {
const list = [
{ b_id: 1, b_parent_id: null },
{ b_id: 2, b_parent_id: 1 },
{ b_id: 3, b_parent_id: 2 },
]
const tree = buildTree(list)
expect(tree).toHaveLength(1)
expect(tree[0].b_id).toBe(1)
expect(tree[0].children).toHaveLength(1)
expect(tree[0].children[0].b_id).toBe(2)
expect(tree[0].children[0].children[0].b_id).toBe(3)
})
it('父节点不存在的子节点提升为根(孤儿节点)', () => {
const list = [
{ b_id: 2, b_parent_id: 999 },
{ b_id: 3, b_parent_id: null },
]
const tree = buildTree(list)
expect(tree).toHaveLength(2)
})
it('支持自定义 idKey / parentKey / childrenKey', () => {
const list = [
{ id: 'a', pid: null },
{ id: 'b', pid: 'a' },
]
const tree = buildTree(list, 'id', 'pid', 'kids')
expect(tree[0].kids[0].id).toBe('b')
})
it('副作用说明:向父节点对象注入 children 属性(当前实现行为)', () => {
const list = [
{ b_id: 1, b_parent_id: null },
{ b_id: 2, b_parent_id: 1 },
]
buildTree(list)
// 当前实现直接修改传入的节点对象(parent[childrenKey] = []),
// 调用方若复用原数组需注意此副作用;若要求纯函数需后续改造
expect(Array.isArray(list[0].children)).toBe(true)
expect(list[0].children).toHaveLength(1)
})
})
describe('utils/tree sortTree', () => {
it('按 b_xh 升序排序', () => {
const tree = [{ b_xh: 3 }, { b_xh: 1 }, { b_xh: 2 }]
sortTree(tree)
expect(tree.map((n) => n.b_xh)).toEqual([1, 2, 3])
})
it('b_xh 相同按 b_id 排序', () => {
const tree = [{ b_xh: 1, b_id: 'b' }, { b_xh: 1, b_id: 'a' }]
sortTree(tree)
expect(tree.map((n) => n.b_id)).toEqual(['a', 'b'])
})
it('递归排序子节点', () => {
const tree = [
{
b_xh: 1,
children: [{ b_xh: 2 }, { b_xh: 1 }],
},
]
sortTree(tree)
expect(tree[0].children.map((n) => n.b_xh)).toEqual([1, 2])
})
it('返回原数组引用', () => {
const tree = [{ b_xh: 2 }, { b_xh: 1 }]
expect(sortTree(tree)).toBe(tree)
})
})
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 = ''
document.documentElement.classList.remove('dark')
})
it('applyPrimaryColor 写入 --fms-primary CSS 变量', async () => {
const { applyPrimaryColor } = await import('@/theme/index')
applyPrimaryColor('#ff0000')
expect(document.documentElement.style.getPropertyValue('--fms-primary')).toBe('#ff0000')
})
it('applyPrimaryColor 空值不写入', async () => {
const { applyPrimaryColor } = await import('@/theme/index')
applyPrimaryColor('')
expect(document.documentElement.style.getPropertyValue('--fms-primary')).toBe('')
})
it('applyDarkMode true 添加 dark class,false 移除', async () => {
const { applyDarkMode } = await import('@/theme/index')
applyDarkMode(true)
expect(document.documentElement.classList.contains('dark')).toBe(true)
applyDarkMode(false)
expect(document.documentElement.classList.contains('dark')).toBe(false)
})
})