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

181 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { defineComponent, ref } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ElMessageBox } from 'element-plus'
import { useAuthStore } from '@/core/auth/auth'
import AppLayout from './AppLayout.vue'
const CachedPage = defineComponent({
setup() {
const count = ref(0)
return { count }
},
template: '<button class="cached-count" @click="count += 1">{{ count }}</button>',
})
vi.mock('element-plus', async (importOriginal) => {
const actual = await importOriginal<typeof import('element-plus')>()
return { ...actual, ElMessage: { warning: vi.fn(), error: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }
})
async function mountLayout(path = '/') {
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.user = { id: '1', name: '平台管理员', account: 'admin', roles: ['PLATFORM_ADMIN'] }
auth.permissions = ['platform:warehouse:list', 'platform:notification:task:list', 'platform:notification:template:list']
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '<p>page</p>' }, meta: { requiresAuth: true, workspaceTab: false, keepAlive: false } },
{ path: '/customers', name: 'customers', component: CachedPage, meta: { title: '客户管理', showPageHeading: false, requiresAuth: true } },
{ path: '/warehouses', name: 'warehouses', component: { template: '<p>warehouses</p>' }, meta: { title: '仓库管理', requiresAuth: true } },
{ path: '/login', component: { template: '<p>login</p>' } },
],
})
await router.push(path)
await router.isReady()
const wrapper = mount(AppLayout, {
global: { plugins: [pinia, router], stubs: { AdminSidebarMenu: { template: '<nav />' }, WorkspaceTabs: { template: '<div class="workspace-tabs-stub" />' }, 'el-dropdown': { template: '<div><slot /><slot name="dropdown" /></div>' }, 'el-dropdown-menu': { template: '<div><slot /></div>' }, 'el-dropdown-item': { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' }, 'el-button': { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' } } },
})
return { wrapper, router }
}
function mockViewport(isMobile: boolean) {
const mediaQuery = {
matches: isMobile,
media: '(max-width: 720px)',
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
} as unknown as MediaQueryList
vi.stubGlobal('matchMedia', vi.fn(() => mediaQuery))
}
describe('AppLayout', () => {
beforeEach(() => {
window.localStorage.clear()
vi.mocked(ElMessageBox.confirm).mockReset()
mockViewport(false)
})
afterEach(() => vi.unstubAllGlobals())
it('reports the desktop navigation expanded and collapsed states', async () => {
const { wrapper } = await mountLayout()
const menuButton = wrapper.get('.shell-menu-button')
expect(menuButton.attributes('aria-label')).toBe('收起导航')
expect(menuButton.attributes('aria-expanded')).toBe('true')
await menuButton.trigger('click')
expect(menuButton.attributes('aria-label')).toBe('展开导航')
expect(menuButton.attributes('aria-expanded')).toBe('false')
})
it('reports the mobile menu closed and open states', async () => {
mockViewport(true)
const { wrapper } = await mountLayout()
const menuButton = wrapper.get('.shell-menu-button')
expect(menuButton.attributes('aria-label')).toBe('打开菜单')
expect(menuButton.attributes('aria-expanded')).toBe('false')
await menuButton.trigger('click')
expect(menuButton.attributes('aria-label')).toBe('关闭菜单')
expect(menuButton.attributes('aria-expanded')).toBe('true')
})
it('closes the mobile menu when returning to the desktop breakpoint and removes the listener', async () => {
mockViewport(true)
const { wrapper } = await mountLayout()
const menuButton = wrapper.get('.shell-menu-button')
const mediaQuery = vi.mocked(window.matchMedia).mock.results[0]?.value as MediaQueryList
const changeHandler = vi.mocked(mediaQuery.addEventListener).mock.calls[0]?.[1] as ((event: MediaQueryListEvent) => void)
await menuButton.trigger('click')
expect(menuButton.attributes('aria-expanded')).toBe('true')
changeHandler({ matches: false } as MediaQueryListEvent)
await wrapper.vm.$nextTick()
expect(menuButton.attributes('aria-label')).toBe('收起导航')
expect(menuButton.attributes('aria-expanded')).toBe('true')
wrapper.unmount()
expect(mediaQuery.removeEventListener).toHaveBeenCalledWith('change', changeHandler)
})
it('shows the authenticated identity and the localhost full-access control', async () => {
const { wrapper } = await mountLayout()
expect(wrapper.get('.user-copy').text()).toBe('平台管理员(admin)')
expect(wrapper.get('.user-copy').attributes('aria-label')).toBe('登录身份:平台管理员(admin)')
const accessToggle = wrapper.get('[role="switch"]')
expect(accessToggle.text()).toContain('本地全权限')
expect(accessToggle.text()).toContain('已关闭')
expect(accessToggle.attributes('aria-checked')).toBe('false')
await accessToggle.trigger('click')
expect(accessToggle.text()).toContain('已开启')
expect(accessToggle.attributes('aria-checked')).toBe('true')
expect(wrapper.get('[aria-label="消息中心"]').attributes('disabled')).toBeUndefined()
expect(wrapper.text()).toContain('消息管理')
expect(wrapper.text()).toContain('模板管理')
expect(wrapper.text()).toContain('我的消息')
const notificationTrigger = wrapper.get('.notification-trigger')
expect(notificationTrigger.find('.notification-count').exists()).toBe(false)
})
it('avoids repeating identical display names and accounts while retaining the full accessible identity', async () => {
const { wrapper } = await mountLayout()
const auth = useAuthStore()
auth.user = { ...auth.user!, name: 'admin', account: 'admin' }
await wrapper.vm.$nextTick()
expect(wrapper.get('.user-copy').text()).toBe('admin')
expect(wrapper.get('.user-copy').attributes('aria-label')).toBe('登录身份:admin(admin)')
expect(wrapper.get('.user-copy').attributes('title')).toBe('登录身份:admin(admin)')
})
it('limits the shell theme to navigation and header instead of inheriting it into routed pages', async () => {
const { wrapper } = await mountLayout('/customers')
expect(wrapper.get('.app-sidebar').classes()).toContain('admin-workbench')
expect(wrapper.get('.app-header').classes()).toContain('admin-workbench')
expect(wrapper.get('.cached-count').element.closest('.admin-workbench')).toBeNull()
})
it('hides the shell heading when the route opts out', async () => {
const { wrapper } = await mountLayout('/customers')
expect(wrapper.find('.page-heading').exists()).toBe(false)
expect(wrapper.get('.user-copy').attributes('aria-label')).toBe('登录身份:平台管理员(admin)')
})
it('logs out after confirmation', async () => {
vi.mocked(ElMessageBox.confirm).mockResolvedValueOnce({} as never)
const { wrapper, router } = await mountLayout()
await wrapper.get('[aria-label="退出登录"]').trigger('click')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/login')
})
it('opens business routes as workspace tabs and keeps route component state while switching', async () => {
const { wrapper, router } = await mountLayout('/customers')
expect(wrapper.find('.workspace-tabs-stub').exists()).toBe(true)
await wrapper.get('.cached-count').trigger('click')
expect(wrapper.get('.cached-count').text()).toBe('1')
await router.push('/warehouses')
await flushPromises()
await router.push('/customers')
await flushPromises()
expect(wrapper.get('.cached-count').text()).toBe('1')
})
})