Files
workspace/code/g3soft-erp/web/tests/utils/tree.spec.ts
T
2026-10-14 17:25:47 +08:00

695 lines
23 KiB
TypeScript
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.
/**
* utils/tree 的单元测试。
*
* 移植自 fms-vue 的 tests/unit/utils.spec.js(删掉其中的 utils/theme 段——本项目无关),
* 并按 TS strict 做了类型标注;另补 fms 未覆盖的六个函数的用例
* (findTreeNode / collectAllKeys / expandAncestors / filterTreeByText / insertTreeNode / removeTreeNode)。
*/
import { describe, it, expect } from 'vitest'
import {
addRowUpdate,
applyTreePosition,
buildSortedTree,
buildTree,
collectAllKeys,
collectDescendantIds,
computeTreeMove,
decorateTreeNodes,
expandAncestors,
filterTreeByText,
findNodePosition,
findTreeNode,
insertTreeNode,
rebuildTreePositions,
removeTreeNode,
renumberSiblings,
sortTree,
} from '@/utils/tree'
import type { TreeMoveResult, TreeRow } 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 as TreeRow[])[0].id).toBe('b')
})
it('副作用说明:向父节点对象注入 children 属性(当前实现行为)', () => {
const list: TreeRow[] = [
{ b_id: 1, b_parent_id: null },
{ b_id: 2, b_parent_id: 1 },
]
buildTree(list)
// 当前实现直接修改传入的节点对象(parent[childrenKey] = []),
// 调用方若复用原数组需注意此副作用;需要纯函数请用 buildSortedTree
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: TreeRow[] = [
{
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)
})
})
/** 两层示例树:a(含 a1,a1 含 a1x)、b */
function makeQueryTree(): TreeRow[] {
return [
{
b_id: 'a',
b_name: '甲',
b_code: 'alpha',
children: [
{
b_id: 'a1',
b_name: '甲一',
b_code: 'alpha-1',
children: [{ b_id: 'a1x', b_name: '子节点', b_code: 'alpha-1-x', children: [] }],
},
],
},
{ b_id: 'b', b_name: '乙', b_code: 'beta', children: [] },
]
}
describe('utils/tree 查询与过滤', () => {
it('findTreeNode 递归查找并返回树中的活节点', () => {
const tree = makeQueryTree()
const found = findTreeNode(tree, 'a1x')
expect(found?.b_name).toBe('子节点')
// 返回的是原节点引用,业务可以继续改它
expect(found).toBe(tree[0].children?.[0].children?.[0])
expect(findTreeNode(tree, 'ghost')).toBe(null)
})
it('findTreeNode 支持自定义 idKey / childrenKey', () => {
const tree: TreeRow[] = [{ code: 'p', kids: [{ code: 'c' }] }]
expect(findTreeNode(tree, 'c', 'code', 'kids')?.code).toBe('c')
})
it('collectAllKeys 收集全部层级 key 并统一转字符串', () => {
const tree: TreeRow[] = [
{ b_id: 1, children: [{ b_id: 2, children: [] }] },
{ b_id: 3, children: [] },
]
expect(collectAllKeys(tree)).toEqual(['1', '2', '3'])
expect(collectAllKeys([])).toEqual([])
})
it('expandAncestors 返回根到目标节点的祖先路径(不含自身)', () => {
const tree = makeQueryTree()
expect(expandAncestors(tree, 'a1x')).toEqual(['a', 'a1'])
// 根节点没有祖先
expect(expandAncestors(tree, 'a')).toEqual([])
expect(expandAncestors(tree, 'ghost')).toEqual([])
})
it('filterTreeByText 命中深层节点时保留祖先链路(剪掉兄弟)', () => {
const tree = makeQueryTree()
// a1x 的 b_code 独有;b_id 不参与匹配(默认只匹配 b_name / b_code)
const filtered = filterTreeByText(tree, 'alpha-1-x')
// 只保留 a → a1 → a1x 这条链路,兄弟节点被剪掉
expect(filtered).toHaveLength(1)
expect(filtered[0].b_id).toBe('a')
expect(filtered[0].children?.[0].b_id).toBe('a1')
expect(filtered[0].children?.[0].children?.[0].b_id).toBe('a1x')
})
it('filterTreeByText 命中父节点但子树未命中时,子树被剪空(命中不豁免后代过滤)', () => {
const tree = makeQueryTree()
// '甲一' 只命中 a1 的名字;a1x 未命中被剪掉,a1 自身保留
const filtered = filterTreeByText(tree, '甲一')
expect(filtered[0].children?.[0].b_id).toBe('a1')
expect(filtered[0].children?.[0].children).toEqual([])
})
it('filterTreeByText 匹配 b_code,空关键字返回原引用', () => {
const tree = makeQueryTree()
expect(filterTreeByText(tree, 'beta')[0].b_id).toBe('b')
expect(filterTreeByText(tree, ' ')).toBe(tree)
})
it('filterTreeByText 未命中返回空数组,且结果是浅拷贝视图', () => {
const tree = makeQueryTree()
expect(filterTreeByText(tree, '不存在的关键字')).toEqual([])
const filtered = filterTreeByText(tree, 'beta')
// 克隆节点、children 指向克隆後的数组:不是原始行对象
expect(filtered[0]).not.toBe(tree[1])
})
it('insertTreeNode 顶级插入(parentIdKey 为空)', () => {
const tree: TreeRow[] = [{ b_id: 'a', b_parent_id: null }]
insertTreeNode(tree, { b_id: 'b', b_parent_id: null })
expect(tree.map((n) => n.b_id)).toEqual(['a', 'b'])
})
it('insertTreeNode 挂到指定父节点下并按 sort 排序', () => {
const tree: TreeRow[] = [
{
b_id: 'a',
b_parent_id: null,
children: [{ b_id: 'a1', b_parent_id: 'a', b_xh: 10, children: [] }],
},
]
insertTreeNode(tree, { b_id: 'a2', b_parent_id: 'a', b_xh: 5 }, { sort: sortTree })
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a2', 'a1'])
})
it('removeTreeNode 移除根级与深层节点', () => {
const tree = makeQueryTree()
removeTreeNode(tree, 'b')
expect(tree.map((n) => n.b_id)).toEqual(['a'])
removeTreeNode(tree, 'a1x')
expect(tree[0].children?.[0].children).toHaveLength(0)
})
})
describe('utils/tree 可编辑树:建树与节点装饰', () => {
it('buildSortedTree 建树并排序,且不污染原始行(重复构建不累积子节点)', () => {
const rows: TreeRow[] = [
{ 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 tree: TreeRow[] = [
{
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: FolderIcon },
})
expect(tree[0].isLeaf).toBe(false)
expect(tree[0].icon).toBe(FolderIcon)
// 非 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: TreeRow[] = [{ 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: TreeRow[] = []
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: TreeRow[] = []
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: TreeRow[] = [
{
b_id: 'a',
b_depth: 0,
b_path: '/a/',
children: [{ b_id: 'a1', b_depth: 9, b_path: '/stale/', children: [] }],
},
]
const updated: TreeRow[] = []
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: TreeRow[] = [
{
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: TreeRow[] = []
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: TreeRow[] = [{ b_id: 'base', b_depth: 3, b_path: 'stale/', children: [] }]
const updated: TreeRow[] = []
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/' }])
})
})
/** decorateTreeNodes 用的占位图标组件 */
const FolderIcon = () => null
/** 三层扁平行:a(含 a1、a2);a1 含 a1x;b 独立 */
function makeDescendantRows(): TreeRow[] {
return [
{ b_id: 'a', b_parent_id: null },
{ b_id: 'a1', b_parent_id: 'a' },
{ b_id: 'a2', b_parent_id: 'a' },
{ b_id: 'a1x', b_parent_id: 'a1' },
{ b_id: 'b', b_parent_id: null },
]
}
const asSet = (list: string[]) => new Set(list)
/** 造一棵两级树:a(含 a1 / a2)、b */
function makeMoveTree(): TreeRow[] {
return [
{
b_id: 'a',
b_parent_id: null,
b_xh: 10,
b_depth: 0,
b_path: '/a/',
children: [
{ b_id: 'a1', b_parent_id: 'a', b_xh: 10, b_depth: 1, b_path: '/a/a1/', children: [] },
{ b_id: 'a2', b_parent_id: 'a', b_xh: 20, b_depth: 1, b_path: '/a/a2/', children: [] },
],
},
{ b_id: 'b', b_parent_id: null, b_xh: 20, b_depth: 0, b_path: '/b/', children: [] },
]
}
/** 在 updatedRows 里按 b_id 取那一行 */
function rowById(rows: TreeRow[], id: string): TreeRow | undefined {
return rows.find((row) => String(row.b_id) === id)
}
type MoveOk = Extract<TreeMoveResult, { ok: true }>
type MoveFail = Extract<TreeMoveResult, { ok: false }>
/** 断言拖拽成功并收窄联合类型(strict 下后续可直接取 updatedRows / moved) */
function mustOk(result: TreeMoveResult): MoveOk {
expect(result.ok).toBe(true)
if (!result.ok) throw new Error(`期望拖拽成功,实际被拦截:${result.reason}`)
return result
}
/** 断言拖拽被拦截并收窄联合类型 */
function mustFail(result: TreeMoveResult): MoveFail {
expect(result.ok).toBe(false)
if (result.ok) throw new Error('期望拖拽被拦截,实际成功')
return result
}
describe('utils/tree collectDescendantIds:按父级关联展开整棵子树', () => {
it('收集自身与全部层级的下级,不限深度', () => {
const ids = collectDescendantIds(makeDescendantRows(), 'a')
expect(asSet(ids)).toEqual(new Set(['a', 'a1', 'a2', 'a1x']))
expect(ids).toHaveLength(4)
})
it('叶子节点只返回自身', () => {
expect(collectDescendantIds(makeDescendantRows(), 'a1x')).toEqual(['a1x'])
expect(collectDescendantIds(makeDescendantRows(), 'b')).toEqual(['b'])
})
it('结果含自身(选中一个节点 = 选中整棵子树)', () => {
const ids = collectDescendantIds(makeDescendantRows(), 'a1')
expect(ids).toContain('a1')
expect(asSet(ids)).toEqual(new Set(['a1', 'a1x']))
})
it('ID 数值与字符串混用时不漏(统一按字符串比较)', () => {
const rows = [
{ b_id: 1, b_parent_id: null },
{ b_id: 2, b_parent_id: 1 },
{ b_id: 3, b_parent_id: '2' },
]
// 数值 1 起头,也要能顺着字符串 '2' 找到 3
expect(asSet(collectDescendantIds(rows, 1))).toEqual(new Set(['1', '2', '3']))
})
it('rootId 不在数据中时返回只含自身的集合,不抛错', () => {
// 语义安全:拼成 IN ('不存在') 自然查不到数据,调用方无需额外判空
expect(collectDescendantIds(makeDescendantRows(), 'ghost')).toEqual(['ghost'])
expect(collectDescendantIds([], 'ghost')).toEqual(['ghost'])
expect(collectDescendantIds(null, 'ghost')).toEqual(['ghost'])
})
it('b_parent_id 成环的脏数据不会死循环(visited 兜底)', () => {
const rows = [
{ b_id: 'x', b_parent_id: 'y' },
{ b_id: 'y', b_parent_id: 'x' },
]
expect(asSet(collectDescendantIds(rows, 'x'))).toEqual(new Set(['x', 'y']))
})
it('自引用的脏数据也不会死循环', () => {
const rows = [{ b_id: 'z', b_parent_id: 'z' }]
expect(collectDescendantIds(rows, 'z')).toEqual(['z'])
})
it('字段名可用 options 覆盖(非 b_id / b_parent_id 的数据)', () => {
const rows = [
{ code: 'p', parent: null },
{ code: 'c', parent: 'p' },
]
const ids = collectDescendantIds(rows, 'p', { idKey: 'code', parentKey: 'parent' })
expect(asSet(ids)).toEqual(new Set(['p', 'c']))
})
})
describe('utils/tree computeTreeMove:拖拽移动的守卫与落库行', () => {
const makeTree = makeMoveTree
it('拖到自己身上被拒绝,且不产生任何变更', () => {
const tree = makeTree()
const result = mustFail(
computeTreeMove(tree, { dragKey: 'a', dropKey: 'a', dropPosition: 'inside' }),
)
expect(result.reason).toBe('self')
expect('updatedRows' in result).toBe(false)
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a1', 'a2'])
})
it('拖拽或落点节点不存在时返回 notFound', () => {
const tree = makeTree()
expect(
mustFail(computeTreeMove(tree, { dragKey: 'ghost', dropKey: 'a', dropPosition: 'inside' }))
.reason,
).toBe('notFound')
expect(
mustFail(computeTreeMove(tree, { dragKey: 'a', dropKey: 'ghost', dropPosition: 'inside' }))
.reason,
).toBe('notFound')
})
it('把节点拖进自己的后代被判成环,回带调用方传入的提示文案', () => {
const tree = makeTree()
const result = mustFail(
computeTreeMove(tree, {
dragKey: 'a',
dropKey: 'a1',
dropPosition: 'inside',
messages: { cycle: '不能把模块移动到它自己的下级' },
}),
)
expect(result.reason).toBe('cycle')
expect(result.message).toBe('不能把模块移动到它自己的下级')
// 树未被破坏
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a1', 'a2'])
})
it('inside 落到非容器类型上被拒绝', () => {
const tree = makeTree()
const result = mustFail(
computeTreeMove(tree, {
dragKey: 'b',
dropKey: 'a1',
dropPosition: 'inside',
typeField: 'b_module_type',
nonLeafValues: ['module'],
messages: { notContainer: '只能把模块放入模块类型的节点下' },
}),
)
expect(result.reason).toBe('notContainer')
expect(result.message).toBe('只能把模块放入模块类型的节点下')
})
it('inside 落到未保存的草稿节点上被拒绝', () => {
const tree = makeTree()
const result = mustFail(
computeTreeMove(tree, {
dragKey: 'b',
dropKey: '-1',
dropPosition: 'inside',
isUnsaved: (id) => id === '-1',
messages: { unsavedTarget: '请先保存目标模块,再调整层级' },
}),
)
expect(result.reason).toBe('notFound') // -1 不在树里
// 真正放进树里再验一次
const tree2 = makeTree()
tree2[0].children?.push({ b_id: '-1', b_parent_id: 'a', b_xh: 30, children: [] })
const result2 = mustFail(
computeTreeMove(tree2, {
dragKey: 'b',
dropKey: '-1',
dropPosition: 'inside',
isUnsaved: (id) => id === '-1',
messages: { unsavedTarget: '请先保存目标模块,再调整层级' },
}),
)
expect(result2.reason).toBe('unsavedTarget')
expect(result2.message).toBe('请先保存目标模块,再调整层级')
})
it('inside:换父成功,重算 b_parent_id / b_depth / b_path,父级子集与原子级都重排 b_xh', () => {
const tree = makeTree()
const result = mustOk(
computeTreeMove(tree, { dragKey: 'b', dropKey: 'a1', dropPosition: 'inside' }),
)
expect(result.parentChanged).toBe(true)
// a1 收下 b 作为子节点
const a1 = tree[0].children?.find((n) => n.b_id === 'a1')
expect(a1?.children?.map((n) => n.b_id)).toEqual(['b'])
// a 下不再有 b
expect(tree.map((n) => n.b_id)).toEqual(['a'])
// 迁移后的位置字段随新父级收敛
expect(result.moved).toMatchObject({
b_id: 'b',
b_parent_id: 'a1',
b_depth: 2,
b_path: '/a/a1/b/',
})
// b_parent_id 变化会单独补一条 update
expect(rowById(result.updatedRows, 'b')?.b_parent_id).toBe('a1')
// b 落到 a1 下成为唯一子节点 → 位置 10;a 下只剩 a1(原 10)、a2(原 20),
// 重排后取值不变,因此不再产生 update
expect(rowById(result.updatedRows, 'b')?.b_xh).toBe(10)
expect(rowById(result.updatedRows, 'a2')).toBeUndefined()
})
it('inside:落点原本没有 children 字段时补出空数组并摘掉 isLeaf', () => {
const tree: TreeRow[] = [
{ b_id: 'a', b_parent_id: null, b_xh: 10, b_depth: 0, b_path: '/a/', children: [] },
{ b_id: 'b', b_parent_id: null, b_xh: 20, b_depth: 0, b_path: '/b/', isLeaf: true },
]
const result = mustOk(
computeTreeMove(tree, { dragKey: 'a', dropKey: 'b', dropPosition: 'inside' }),
)
expect(result.ok).toBe(true)
const b = tree.find((n) => n.b_id === 'b')
expect(b?.children?.map((n) => n.b_id)).toEqual(['a'])
expect(b?.isLeaf).toBe(false)
})
it('after:同级换序,父级不变时不再产生 b_parent_id 更新', () => {
const tree = makeTree()
const result = mustOk(
computeTreeMove(tree, { dragKey: 'a2', dropKey: 'a1', dropPosition: 'after' }),
)
expect(result.parentChanged).toBe(false)
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a1', 'a2'])
// 位置本就是 10 / 20,重排后无变化 → 该节点不产生 update
expect(rowById(result.updatedRows, 'a2')).toBeUndefined()
expect(result.updatedRows).toEqual([])
})
it('before:跨父拖到目标之前,落到目标同级并接管目标父级', () => {
const tree = makeTree()
const result = mustOk(
computeTreeMove(tree, { dragKey: 'b', dropKey: 'a2', dropPosition: 'before' }),
)
expect(result.parentChanged).toBe(true)
expect(tree.map((n) => n.b_id)).toEqual(['a'])
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a1', 'b', 'a2'])
expect(result.moved).toMatchObject({ b_parent_id: 'a', b_depth: 1, b_path: '/a/b/' })
expect(rowById(result.updatedRows, 'b')?.b_parent_id).toBe('a')
})
it('同级 after 且拖到自身之后:索引修正后位置不变', () => {
const tree = makeTree()
// a1 拖到 a2 之后 => a1 落到末尾
const result = mustOk(
computeTreeMove(tree, { dragKey: 'a1', dropKey: 'a2', dropPosition: 'after' }),
)
expect(result.ok).toBe(true)
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a2', 'a1'])
})
it('同级 before 且拖到自身之前:索引修正后位置不变', () => {
const tree = makeTree()
const result = mustOk(
computeTreeMove(tree, { dragKey: 'a2', dropKey: 'a1', dropPosition: 'before' }),
)
expect(tree[0].children?.map((n) => n.b_id)).toEqual(['a2', 'a1'])
expect(rowById(result.updatedRows, 'a2')?.b_xh).toBe(10)
})
it('无变化场景给出空 updatedRows 而不是报错', () => {
const tree = makeTree()
// 把 a1 拖到 a2 之前,本来就是该位置
const result = mustOk(
computeTreeMove(tree, { dragKey: 'a1', dropKey: 'a2', dropPosition: 'before' }),
)
expect(result.updatedRows).toEqual([])
})
})