import { describe, expect, it } from 'vitest' import { computeAccordionToggle } from '@/layouts/useSidebarMenu' import type { SidebarMenuNode } from '@/types/menu' /** * 侧栏「手风琴模式」展开逻辑的回归用例。 * * 背景(真实故障):点击二级分组(系统管理 → 日志)时,不是展开「日志」, * 而是整个「系统管理」收起。根因是 toggleGroup 用了整表替换 * `openKeys = index === -1 ? [key] : []`,把祖先 key 一起删了; * 而子分组的容器挂在父级的 `v-show="isGroupOpen(父key)"` 上, * 祖先一丢,子分组连带整块 DOM 都被隐藏。 */ /** 造一个叶子节点。放模块作用域:纯构造,不必每次调用重建。 */ function leaf(key: string, title: string): SidebarMenuNode { return { key, title, path: `/${key}` } } /** 复刻 s_menu 真实结构:system → log → (log_login / log_audit) */ function makeTree(): SidebarMenuNode[] { return [ { key: 'approval', title: '审批中心', path: '/approval', children: [leaf('approval_todo', '我的待办'), leaf('approval_cc', '抄送我的')], }, { key: 'system', title: '系统管理', path: '/system', children: [ leaf('user', '用户管理'), { key: 'log', title: '日志', path: '/system/log', children: [leaf('log_login', '登录日志'), leaf('log_audit', '操作日志')], }, ], }, ] } describe('computeAccordionToggle(手风琴展开)', () => { it('展开二级分组时保留其祖先,父级不被收起(本次故障的回归点)', () => { const tree = makeTree() // 「系统管理」已展开,此时点击其子分组「日志」 const next = computeAccordionToggle(tree, ['system'], 'log') expect(next).toContain('log') // 关键断言:祖先必须还在,否则「日志」的容器会被 v-show 隐藏 expect(next).toContain('system') }) it('展开二级分组时只收起同级的兄弟分组', () => { // system 下放两个同级分组:打开 log 应关掉 report,但不能关掉 system const withSibling: SidebarMenuNode[] = [ { key: 'system', title: '系统管理', path: '/system', children: [ { key: 'log', title: '日志', path: '/system/log', children: [] }, { key: 'report', title: '报表', path: '/system/report', children: [] }, ], }, ] const next = computeAccordionToggle(withSibling, ['system', 'report'], 'log') expect(next).toContain('system') expect(next).toContain('log') expect(next).not.toContain('report') }) it('展开顶级分组时收起其它顶级分组', () => { const tree = makeTree() const next = computeAccordionToggle(tree, ['approval'], 'system') expect(next).toContain('system') expect(next).not.toContain('approval') }) it('再次点击已展开的分组会收起它自身(连同其子孙),但不动祖先', () => { const tree = makeTree() const next = computeAccordionToggle(tree, ['system', 'log'], 'log') expect(next).not.toContain('log') // 收起子分组不该连带收起父级 expect(next).toContain('system') }) it('展开顶级分组不会误删其它顶级分组下手动展开的深层项之外的祖先', () => { const tree = makeTree() // 系统管理 与 日志 都开着,此时点开「审批中心」 const next = computeAccordionToggle(tree, ['system', 'log'], 'approval') expect(next).toContain('approval') expect(next).not.toContain('system') // system 被收起时,其子孙也应一并清掉,避免下次展开时残留 expect(next).not.toContain('log') }) })