diff --git a/admin-frontend-vue/tests/task-38.test.ts b/admin-frontend-vue/tests/task-38.test.ts new file mode 100644 index 00000000..e6fc3c9e --- /dev/null +++ b/admin-frontend-vue/tests/task-38.test.ts @@ -0,0 +1,67 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseMenuTree } from '../src/api/session-model.ts' +import { toSidebarEntries } from '../src/layout/menu-mapper.ts' + +test('test_task_038_menu_sort_and_empty_route_normal_primary_path', () => { + // 正常主路径:前端保后端顺序,不按 sort 二次重排。 + const payload = { data: { items: [ + { key: 'b', name: '乙', route: '/b', sort: 1 }, + { key: 'a', name: '甲', route: '/a', sort: 0 }, + ] } } + const tree = parseMenuTree(payload) + assert.equal(tree[0].key, 'b', '先出现者排前,不按 sort 值重排') +}) + +test('test_task_038_menu_sort_and_empty_route_normal_variant_input', () => { + // 正常变体:分组(无 route) 下带 route 的子页正常进分组。 + const tree = parseMenuTree({ items: [{ key: 'g', name: '组', route: undefined, children: [{ key: 'p', name: '页', route: '/p' }] }] }) + const entries = toSidebarEntries(tree) + assert.equal(entries[0].kind, 'group') +}) + +test('test_task_038_menu_sort_and_empty_route_normal_repeated_operation_is_idempotent', () => { + // 正常重复:排序稳定、解析不修改输入。 + const payload = { items: [{ key: 'a', name: 'A', route: '/a', sort: 2 }] } + assert.deepEqual(parseMenuTree(payload), parseMenuTree(payload)) + assert.equal((payload.items as Array<{ sort: number }>)[0].sort, 2) +}) + +test('test_task_038_menu_sort_and_empty_route_boundary_empty_input', () => { + // 边界空值:无路由的子节点被侧边栏过滤(点不进去不展示)。 + const entries = toSidebarEntries([{ key: 'g', name: '组', children: [{ key: 'p', name: '页' }] }]) + assert.equal(entries.length, 0, '组内无任何可路由子项则整组不展示') +}) + +test('test_task_038_menu_sort_and_empty_route_boundary_single_item', () => { + // 边界单元素:顶级无子但有 route 的叶节点作为单页项展示。 + const entries = toSidebarEntries([{ key: 's', name: '单页', route: '/solo' }]) + assert.equal(entries.length, 1) + assert.equal(entries[0].kind, 'item') +}) + +test('test_task_038_menu_sort_and_empty_route_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:既有可路由子项又有不可路由子项时只保留可路由子项。 + const entries = toSidebarEntries([{ key: 'g', name: '组', children: [ + { key: 'ok', name: '可进', route: '/ok' }, + { key: 'bad', name: '占位' }, + ] }]) + assert.equal(entries[0].kind, 'group') + if (entries[0].kind === 'group') assert.equal(entries[0].children.length, 1) +}) + +test('test_task_038_menu_sort_and_empty_route_invalid_input_rejected', () => { + // 异常输入:整树失败信封抛错而非空树;空路由不是合法的页面进入理由。 + assert.throws(() => parseMenuTree({ success: false, message: '菜单失败' }), /菜单失败/) + const tree = parseMenuTree({ items: [{ key: 'p', name: '页' }] }) + assert.equal(toSidebarEntries(tree).length, 0, '无 route 的顶级节点不进侧边栏') +}) + +test('test_task_038_menu_sort_and_empty_route_dependency_failure_returns_actionable_message', () => { + // 依赖失败:store/路由保存菜单后不二次排序(按后端下发顺序渲染)。 + const store = readSource('src/stores/admin-session.ts') + assert.equal(/\bsort\(\)/.test(store), false, 'store 不对菜单树调用 sort()') + const helpers = readSource('src/router/helpers.ts') + assert.equal(/\bsort\(\)/.test(helpers), false, 'helpers 不对菜单树调用 sort()') +})