Files
admin-web/src/app/components/AdminSidebarMenu.test.ts
T
2026-10-20 23:34:11 +08:00

222 lines
8.9 KiB
TypeScript

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: '<nav :data-active="defaultActive" :data-openeds="JSON.stringify(defaultOpeneds)" :data-collapsed="String(collapse)" @click="$emit(\'select\', \'/warehouses\')"><slot /></nav>',
}),
'el-sub-menu': {
props: ['index'],
template: '<section data-kind="group" :data-index="index"><header><slot name="title" /></header><slot /></section>',
},
'el-menu-item': {
props: ['index'],
template: '<a data-kind="item" :data-index="index"><slot name="title" /><slot /></a>',
},
}
describe('AdminSidebarMenu', () => {
function routerPlugin() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<p />' } },
{ path: '/warehouses', component: { template: '<p />' } },
],
})
}
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 = '<aside class="app-sidebar"><nav class="side-nav"></nav></aside>'
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<HTMLElement>('.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<HTMLElement>('.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')
})
})