import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { MobileMenu } from './mobile-menu'; jest.mock('@/hooks/use-focus-trap', () => ({ useFocusTrap: () => ({ current: null }), })); jest.mock('@/lib/site-config', () => ({ useSiteConfig: () => ({ mainNav: [ { id: 'products', label: '产品', href: '/products', hasDropdown: true, dropdownKey: 'products' }, { id: 'solutions', label: '解决方案', href: '/solutions', hasDropdown: true, dropdownKey: 'solutions' }, { id: 'services', label: '服务', href: '/services' }, { id: 'about', label: '关于我们', href: '/about' }, { id: 'contact', label: '联系我们', href: '/contact' }, ], megaDropdown: { products: [ { id: 'erp', title: 'ERP 管理系统', description: '财务·采购·销售·库存·生产', href: '/products/erp' }, ], solutions: [ { id: 'manufacturing', title: '制造业', description: '智能制造·MES·质量管控', href: '/solutions/manufacturing' }, ], }, }), SiteConfigProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('@/lib/constants', () => ({ NAVIGATION_V2: [ { id: 'products', label: '产品', href: '/products', hasDropdown: true, dropdownKey: 'products' }, { id: 'solutions', label: '解决方案', href: '/solutions', hasDropdown: true, dropdownKey: 'solutions' }, { id: 'services', label: '服务', href: '/services' }, { id: 'about', label: '关于我们', href: '/about' }, { id: 'contact', label: '联系我们', href: '/contact' }, ], MEGA_DROPDOWN_DATA: { products: [ { id: 'erp', title: 'ERP 管理系统', description: '财务·采购·销售·库存·生产', href: '/products/erp' }, ], solutions: [ { id: 'manufacturing', title: '制造业', description: '智能制造·MES·质量管控', href: '/solutions/manufacturing' }, ], }, })); jest.mock('next/link', () => { const MockLink = ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => ( {children} ); MockLink.displayName = 'MockLink'; return MockLink; }); jest.mock('framer-motion', () => ({ AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, motion: { div: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
{children}
), }, })); jest.mock('lucide-react', () => ({ ChevronDown: () => , Menu: () => , X: () => , })); jest.mock('@/components/ui/static-link', () => ({ StaticLink: ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => ( {children} ), })); jest.mock('@/lib/utils', () => ({ cn: (...args: unknown[]) => args.filter(Boolean).join(' '), })); describe('MobileMenu', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('Rendering', () => { it('should render menu button', () => { render(); expect(screen.getByRole('button', { name: '打开菜单' })).toBeInTheDocument(); }); it('should render menu icon when closed', () => { render(); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); }); it('should not render menu panel when closed', () => { render(); expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); }); describe('Opening Menu', () => { it('should open menu when button clicked', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(screen.getByRole('navigation')).toBeInTheDocument(); }); it('should change button label when open', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(screen.getByRole('button', { name: '关闭菜单' })).toBeInTheDocument(); }); it('should render navigation items', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(screen.getByText('产品')).toBeInTheDocument(); expect(screen.getByText('服务')).toBeInTheDocument(); }); }); describe('Closing Menu', () => { it('should close menu when button clicked again', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); const closeButton = screen.getByRole('button', { name: '关闭菜单' }); fireEvent.click(closeButton); expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); it('should close menu when overlay clicked', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); const overlay = document.querySelector('.fixed.inset-0'); if (overlay) { fireEvent.click(overlay); } expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); }); describe('Keyboard Navigation', () => { it('should open menu with Enter key', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(screen.getByRole('navigation')).toBeInTheDocument(); }); it('should open menu with Space key', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(screen.getByRole('navigation')).toBeInTheDocument(); }); it('should close menu with Escape key', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); fireEvent.keyDown(button, { key: 'Escape' }); expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); }); describe('Accessibility', () => { it('should have aria-expanded attribute', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-expanded', 'false'); }); it('should update aria-expanded when open', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); expect(button).toHaveAttribute('aria-expanded', 'true'); }); it('should have aria-controls attribute', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-controls', 'mobile-menu-panel'); }); it('should have navigation role', () => { render(); const button = screen.getByRole('button', { name: '打开菜单' }); fireEvent.click(button); const nav = screen.getByRole('navigation'); expect(nav).toHaveAttribute('aria-label', '移动端导航'); }); }); describe('Styling', () => { it('should have responsive visibility', () => { const { container } = render(); const wrapper = container.firstChild as HTMLElement; expect(wrapper).toHaveClass('lg:hidden'); }); it('should apply custom className', () => { const { container } = render(); const wrapper = container.firstChild as HTMLElement; expect(wrapper).toHaveClass('custom-class'); }); }); });