Files
huangzd1997 26edb9e6ed task-6(壳层/路由): 定义后端菜单树到侧边栏节点映射规则并落地
新增 src/layout/menu-mapper.ts:toSidebarEntries 处理分组/顶级路由叶/畸形节点,
groupKeysForActive 按当前路由展开激活分组;AdminLayout 菜单区改用映射结果,
删除旧 visibleChildren/canSee 死逻辑;8 用例覆盖空树/单叶/缺 route 过滤等。
2026-09-05 14:43:52 +08:00

100 lines
4.6 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import { groupKeysForActive, toSidebarEntries } from '../src/layout/menu-mapper.ts'
import type { AdminMenuNode } from '../src/types/admin.ts'
function node(partial: Partial<AdminMenuNode> & { key: string; name: string }): AdminMenuNode {
return { key: partial.key, name: partial.name, route: partial.route, children: partial.children, sort: partial.sort }
}
const TREE: AdminMenuNode[] = [
node({ key: 'account', name: '账号权限', children: [
node({ key: 'admin_users', name: '用户管理', route: '/account/users' }),
node({ key: 'admin_columns', name: '菜单管理', route: '/account/menus' }),
] }),
]
test('test_task_006_menu_sidebar_mapping_normal_primary_path', () => {
// 正常主路径:带可路由子节点的后端菜单渲染为分组,顺序保留。
const entries = toSidebarEntries(TREE)
assert.equal(entries.length, 1)
assert.equal(entries[0].kind, 'group')
if (entries[0].kind === 'group') {
assert.equal(entries[0].name, '账号权限')
assert.deepEqual(entries[0].children.map((c) => c.route), ['/account/users', '/account/menus'])
}
})
test('test_task_006_menu_sidebar_mapping_normal_variant_input', () => {
// 正常变体:另一组菜单(含顶级路由叶 + 分组混合)正确混合输出。
const mix: AdminMenuNode[] = [
node({ key: 'g', name: '分组', children: [node({ key: 'c', name: '子', route: '/x/c' })] }),
node({ key: 'leaf', name: '顶级页', route: '/top' }),
]
const entries = toSidebarEntries(mix)
assert.equal(entries.length, 2)
assert.equal(entries[0].kind, 'group')
assert.equal(entries[1].kind, 'item')
if (entries[1].kind === 'item') assert.equal(entries[1].route, '/top')
})
test('test_task_006_menu_sidebar_mapping_normal_repeated_operation_is_idempotent', () => {
// 正常重复:映射结果稳定,不修改输入源树。
const snapshot = JSON.stringify(TREE)
const first = JSON.stringify(toSidebarEntries(TREE))
const second = JSON.stringify(toSidebarEntries(TREE))
assert.equal(first, second)
assert.equal(JSON.stringify(TREE), snapshot, '映射不得污染源菜单树')
})
test('test_task_006_menu_sidebar_mapping_boundary_empty_input', () => {
// 边界空值:空菜单树得到空侧边栏列表,不崩溃。
assert.deepEqual(toSidebarEntries([]), [])
assert.deepEqual(toSidebarEntries(undefined as unknown as AdminMenuNode[]), [])
})
test('test_task_006_menu_sidebar_mapping_boundary_single_item', () => {
// 边界单元素:单个顶级路由页不因没有子节点而被丢弃。
const entries = toSidebarEntries([node({ key: 's', name: '单页', route: '/solo' })])
assert.equal(entries.length, 1)
assert.equal(entries[0].kind, 'item')
if (entries[0].kind === 'item') assert.equal(entries[0].route, '/solo')
})
test('test_task_006_menu_sidebar_mapping_boundary_limit_or_missing_field', () => {
// 边界上限/缺字段:子项缺 route 被过滤;分组无任何可路由子项时不渲染占位分组。
const noRoutableChildren: AdminMenuNode[] = [
node({ key: 'g', name: '空分组', children: [node({ key: 'c', name: '无路由' })] }),
]
assert.deepEqual(toSidebarEntries(noRoutableChildren), [])
const withMissing = toSidebarEntries([node({ key: 'g', name: '有', children: [
node({ key: 'ok', name: '可点', route: '/a' }),
node({ key: 'no', name: '不可点' }),
] })])
if (withMissing[0].kind === 'group') {
assert.deepEqual(withMissing[0].children.map((c) => c.key), ['ok'])
} else {
assert.fail('应输出分组')
}
})
test('test_task_006_menu_sidebar_mapping_invalid_input_rejected', () => {
// 异常输入:route/children 缺省的畸形节点被跳过而非抛异常。
const malformed = [
{ key: 'x', name: '畸形' } as unknown as AdminMenuNode,
{ key: 'y', name: '无key子项', children: [{ name: '裸' }] } as unknown as AdminMenuNode,
]
assert.deepEqual(toSidebarEntries(malformed), [], '畸形节点不应产生侧边栏项')
})
test('test_task_006_menu_sidebar_mapping_dependency_failure_returns_actionable_message', () => {
// 依赖失败:壳层消费映射模块;激活分组展开按当前路由命中。
const layout = readSource('src/layout/AdminLayout.vue')
assert.match(layout, /toSidebarEntries/)
assert.match(layout, /groupKeysForActive/)
const active = toSidebarEntries(TREE)
assert.deepEqual(groupKeysForActive(active, '/account/menus'), ['account'])
assert.deepEqual(groupKeysForActive(active, '/account/nope'), [], '未命中路由不展开任何分组')
})