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) }) })