767931202d
- 重构 Header 导航、Footer 页脚、MobileMenu 移动端菜单 - 新增 MobileTabBar 移动端底部导航栏 - 更新 MegaDropdown 大型下拉导航 - 重构 SEO 结构化数据组件 - 更新数据层常量 (products, services, solutions, navigation 等) - 新增 useCountUp 数字递增 Hook - 修复 .gitignore 中 src/lib 被错误忽略的问题
231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
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 }) => (
|
|
<a href={href} {...props}>{children}</a>
|
|
);
|
|
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 }) => (
|
|
<div className={className} {...props}>{children}</div>
|
|
),
|
|
},
|
|
}));
|
|
|
|
jest.mock('lucide-react', () => ({
|
|
ChevronDown: () => <span data-testid="chevron-down" />,
|
|
Menu: () => <span data-testid="menu-icon" />,
|
|
X: () => <span data-testid="x-icon" />,
|
|
}));
|
|
|
|
jest.mock('@/components/ui/static-link', () => ({
|
|
StaticLink: ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => (
|
|
<a href={href} {...props}>{children}</a>
|
|
),
|
|
}));
|
|
|
|
jest.mock('@/lib/utils', () => ({
|
|
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
|
}));
|
|
|
|
describe('MobileMenu', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render menu button', () => {
|
|
render(<MobileMenu />);
|
|
expect(screen.getByRole('button', { name: '打开菜单' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render menu icon when closed', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button');
|
|
expect(button).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render menu panel when closed', () => {
|
|
render(<MobileMenu />);
|
|
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Opening Menu', () => {
|
|
it('should open menu when button clicked', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button', { name: '打开菜单' });
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should change button label when open', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button', { name: '打开菜单' });
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('button', { name: '关闭菜单' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render navigation items', () => {
|
|
render(<MobileMenu />);
|
|
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(<MobileMenu />);
|
|
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(<MobileMenu />);
|
|
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(<MobileMenu />);
|
|
const button = screen.getByRole('button', { name: '打开菜单' });
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should open menu with Space key', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button', { name: '打开菜单' });
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should close menu with Escape key', () => {
|
|
render(<MobileMenu />);
|
|
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(<MobileMenu />);
|
|
const button = screen.getByRole('button');
|
|
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
});
|
|
|
|
it('should update aria-expanded when open', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button', { name: '打开菜单' });
|
|
fireEvent.click(button);
|
|
|
|
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
});
|
|
|
|
it('should have aria-controls attribute', () => {
|
|
render(<MobileMenu />);
|
|
const button = screen.getByRole('button');
|
|
expect(button).toHaveAttribute('aria-controls', 'mobile-menu-panel');
|
|
});
|
|
|
|
it('should have navigation role', () => {
|
|
render(<MobileMenu />);
|
|
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(<MobileMenu />);
|
|
const wrapper = container.firstChild as HTMLElement;
|
|
expect(wrapper).toHaveClass('lg:hidden');
|
|
});
|
|
|
|
it('should apply custom className', () => {
|
|
const { container } = render(<MobileMenu className="custom-class" />);
|
|
const wrapper = container.firstChild as HTMLElement;
|
|
expect(wrapper).toHaveClass('custom-class');
|
|
});
|
|
});
|
|
});
|