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 & { 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'), [], '未命中路由不展开任何分组') })