import { flushPromises, mount } from '@vue/test-utils' import { defineComponent, markRaw } from 'vue' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { createMemoryHistory, createRouter } from 'vue-router' import { ElMenu, ElMenuItem, ElSubMenu } from 'element-plus' import { describe, expect, it, vi } from 'vitest' import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/core/navigation/adminNavigation' import AdminSidebarMenu from './AdminSidebarMenu.vue' const visibleNavigation = filterAdminNavigation(adminNavigation, new Set([ 'platform:customer:list', 'platform:warehouse:list', 'platform:firstleg:inbound:list', 'platform:location:list', 'platform:pallet:list', 'firstleg:fba-pricing:rate:view', 'platform:express:list', 'platform:counterparty:list', 'platform:download:list', 'platform:user:list', 'platform:role:list', 'platform:permission:list', ])) const stubs = { 'el-menu': defineComponent({ props: ['defaultActive', 'defaultOpeneds', 'collapse'], emits: ['select'], methods: { updateActiveIndex() {} }, template: '', }), 'el-sub-menu': { props: ['index'], template: '
', }, 'el-menu-item': { props: ['index'], template: '', }, } describe('AdminSidebarMenu', () => { function routerPlugin() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: { template: '

' } }, { path: '/warehouses', component: { template: '

' } }, ], }) } it('renders the visible phase-one modules and forwards the active route', () => { const wrapper = mount(AdminSidebarMenu, { props: { items: visibleNavigation, activePath: '/system/users', collapsed: false }, global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users') expect(wrapper.get('nav').attributes('data-openeds')).toBe('[]') const groupTitles = wrapper.findAll('[data-kind="group"] header').map((node) => node.text()) const paths = wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index')) expect(groupTitles).toContain('客户管理') expect(groupTitles).not.toContain('供应商中心') expect(groupTitles).toContain('通用') expect(groupTitles).toContain('系统管理') expect(groupTitles).not.toContain('预约管理') expect(groupTitles).not.toContain('财务管理') expect(paths).not.toContain('/finance/customer-accounts') expect(paths).toContain('/dashboard') expect(paths).toContain('/orders') expect(paths).not.toContain('/master-data/express-companies') expect(paths).not.toContain('/orders/first-leg') expect(paths).not.toContain('/first-leg-pricing/customer-rules') expect(paths).toContain('/downloads') expect(paths).not.toContain('/prototype/reports/overview') expect(paths).toContain('/system/permissions') }) it('forwards desktop collapse state to Element Plus', () => { const wrapper = mount(AdminSidebarMenu, { props: { items: visibleNavigation, activePath: '/', collapsed: true }, global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true') }) it('fits collapsed icons inside the scrolling rail and keeps hover submenus outside it', async () => { const host = document.createElement('div') host.className = 'app-shell is-collapsed' host.innerHTML = '

' document.body.append(host) const style = document.createElement('style') style.textContent = readFileSync(resolve(process.cwd(), 'src/styles/shell.css'), 'utf8') document.head.append(style) const rail = host.querySelector('.side-nav')! const router = routerPlugin() await router.push('/') const icon = markRaw(adminNavigation[0].icon) const items: AdminNavigationNode[] = [ { key: 'home', title: '首页', icon, path: '/' }, { key: 'resources', title: '资源管理', icon, children: [{ key: 'warehouses', title: '仓库管理', icon, path: '/warehouses' }], }, ] const wrapper = mount(AdminSidebarMenu, { attachTo: rail, props: { items, activePath: '/', collapsed: true }, global: { plugins: [router], components: { ElMenu, ElMenuItem, ElSubMenu }, stubs: { transition: false } }, }) try { await flushPromises() const menu = wrapper.get('.side-nav-menu.el-menu--collapse') const group = wrapper.get('.side-nav-menu > .el-sub-menu') const leafTrigger = wrapper.get('.el-menu-tooltip__trigger') expect(getComputedStyle(host.querySelector('.app-sidebar')!).width).toBe('76px') expect(getComputedStyle(rail).overflowX).toBe('hidden') expect(getComputedStyle(rail).overflowY).toBe('auto') expect(getComputedStyle(menu.element).width).toBe('100%') for (const trigger of [leafTrigger, group.get('.el-sub-menu__title')]) { expect(getComputedStyle(trigger.element).paddingLeft).toBe('0px') expect(getComputedStyle(trigger.element).paddingRight).toBe('0px') expect(getComputedStyle(trigger.element).justifyContent).toBe('center') expect(getComputedStyle(trigger.get('.menu-icon').element).marginRight).toBe('0px') } await group.trigger('mouseenter') await vi.waitFor(() => expect(group.attributes('aria-expanded')).toBe('true')) const popup = document.body.querySelector('.shell-nav-popper .el-menu--popup') expect(popup).not.toBeNull() expect(popup?.closest('.side-nav')).toBeNull() expect(popup?.textContent).toContain('仓库管理') const destination = popup!.querySelector('.el-menu-item')! destination.focus() expect(document.activeElement).toBe(destination) expect(destination.getAttribute('role')).toBe('menuitem') destination.click() await flushPromises() expect(router.currentRoute.value.path).toBe('/warehouses') await wrapper.setProps({ collapsed: false }) host.classList.remove('is-collapsed') expect(getComputedStyle(host.querySelector('.app-sidebar')!).width).toBe('240px') expect(wrapper.find('.el-menu--collapse').exists()).toBe(false) } finally { wrapper.unmount() style.remove() host.remove() } }) it('renders only entries included in the filtered navigation input', () => { const items = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list'])) const wrapper = mount(AdminSidebarMenu, { props: { items, activePath: '/', collapsed: false }, global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.text()).toContain('菜单权限') expect(wrapper.text()).toContain('首页数据看板') expect(wrapper.text()).not.toContain('客户列表') }) it('recursively renders three navigation levels', () => { const items: AdminNavigationNode[] = [{ key: 'operations', title: '运营管理', icon: adminNavigation[0].icon, children: [{ key: 'shipment', title: '运输管理', icon: adminNavigation[0].icon, children: [{ key: 'tracking', title: '轨迹管理', icon: adminNavigation[0].icon, path: '/tracking', permission: 'tracking:list', }], }], }] const wrapper = mount(AdminSidebarMenu, { props: { items, activePath: '/tracking', collapsed: false }, global: { stubs, plugins: [routerPlugin()] }, }) expect(wrapper.findAll('[data-kind="group"]')).toHaveLength(2) expect(wrapper.get('[data-kind="item"]').attributes('data-index')).toBe('/tracking') }) it('recreates the menu when asynchronously loaded groups change', async () => { const wrapper = mount(AdminSidebarMenu, { props: { items: [adminNavigation[0]], activePath: '/dashboard', collapsed: false }, global: { stubs, plugins: [routerPlugin()] }, }) const initialMenu = wrapper.get('nav').element await wrapper.setProps({ items: visibleNavigation }) expect(wrapper.get('nav').element).not.toBe(initialMenu) expect(wrapper.text()).toContain('系统管理') }) it('navigates explicitly when a leaf menu item is selected', async () => { const router = routerPlugin() await router.push('/') const wrapper = mount(AdminSidebarMenu, { props: { items: visibleNavigation, activePath: '/', collapsed: false }, global: { stubs, plugins: [router] }, }) await wrapper.get('nav').trigger('click') await flushPromises() expect(router.currentRoute.value.path).toBe('/warehouses') }) })