同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
278 lines
9.8 KiB
TypeScript
278 lines
9.8 KiB
TypeScript
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import '@testing-library/jest-dom';
|
|
|
|
const mockPush = jest.fn();
|
|
const mockReplace = jest.fn();
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
usePathname: jest.fn(() => '/'),
|
|
useSearchParams: jest.fn(() => new URLSearchParams()),
|
|
useRouter: jest.fn(() => ({
|
|
push: mockPush,
|
|
replace: mockReplace,
|
|
})),
|
|
}));
|
|
|
|
jest.mock('next/link', () => {
|
|
const MockLink = ({ children, href, onClick, ...props }: { children: React.ReactNode; href: string; onClick?: () => void; [key: string]: unknown }) => (
|
|
<a href={href} onClick={onClick} {...props}>
|
|
{children}
|
|
</a>
|
|
);
|
|
MockLink.displayName = 'MockLink';
|
|
return MockLink;
|
|
});
|
|
|
|
jest.mock('next/image', () => {
|
|
const MockImage = ({ src, alt, width, height, className, ...props }: { src: string; alt: string; width: number; height: number; className?: string; [key: string]: unknown }) => (
|
|
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
|
|
);
|
|
MockImage.displayName = 'MockImage';
|
|
return MockImage;
|
|
});
|
|
|
|
jest.mock('framer-motion', () => ({
|
|
motion: {
|
|
div: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
|
|
<div className={className} {...props}>
|
|
{children}
|
|
</div>
|
|
),
|
|
header: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
|
|
<header className={className} {...props}>
|
|
{children}
|
|
</header>
|
|
),
|
|
a: ({ children, className, href, ...props }: { children: React.ReactNode; className?: string; href?: string; [key: string]: unknown }) => (
|
|
<a className={className} href={href} {...props}>
|
|
{children}
|
|
</a>
|
|
),
|
|
nav: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
|
|
<nav className={className} {...props}>
|
|
{children}
|
|
</nav>
|
|
),
|
|
},
|
|
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
jest.mock('lucide-react', () => ({
|
|
Menu: () => <span data-testid="menu-icon" />,
|
|
X: () => <span data-testid="x-icon" />,
|
|
MessageCircle: () => <span data-testid="message-circle-icon" />,
|
|
}));
|
|
|
|
jest.mock('@/components/ui/button', () => {
|
|
const MockButton = ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
|
|
<button className={className} {...props}>
|
|
{children}
|
|
</button>
|
|
);
|
|
MockButton.displayName = 'MockButton';
|
|
return { Button: MockButton };
|
|
});
|
|
|
|
jest.mock('@/lib/site-config', () => ({
|
|
useSiteConfig: () => ({
|
|
name: '四川睿新致远科技有限公司',
|
|
shortName: '睿新致遠',
|
|
displayName: '睿新致远',
|
|
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' },
|
|
{ id: 'crm', title: 'CRM 客户管理', description: '线索·商机·合同·服务', href: '/products/crm' },
|
|
],
|
|
solutions: [
|
|
{ id: 'manufacturing', title: '制造业', description: '智能制造·MES·质量管控', href: '/solutions/manufacturing' },
|
|
],
|
|
},
|
|
}),
|
|
SiteConfigProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
jest.mock('@/lib/constants', () => ({
|
|
COMPANY_INFO: {
|
|
name: '四川睿新致远科技有限公司',
|
|
shortName: '睿新致遠',
|
|
displayName: '睿新致远',
|
|
},
|
|
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' },
|
|
{ id: 'crm', title: 'CRM 客户管理', description: '线索·商机·合同·服务', href: '/products/crm' },
|
|
],
|
|
solutions: [
|
|
{ id: 'manufacturing', title: '制造业', description: '智能制造·MES·质量管控', href: '/solutions/manufacturing' },
|
|
],
|
|
},
|
|
}));
|
|
|
|
jest.mock('@/hooks/use-focus-trap', () => ({
|
|
useFocusTrap: () => ({ current: null }),
|
|
}));
|
|
|
|
jest.mock('@/components/layout/mega-dropdown', () => ({
|
|
MegaDropdown: ({ label, groups }: { label: string; groups?: Array<{ id: string; title: string; description: string; href: string }> }) => (
|
|
<div data-testid="mega-dropdown">
|
|
<span>{label}</span>
|
|
{(groups ?? []).map(item => (
|
|
<a key={item.id} href={item.href}>{item.title}</a>
|
|
))}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
import { Header } from './header';
|
|
|
|
describe('Header', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render header component', () => {
|
|
render(<Header />);
|
|
expect(screen.getByRole('banner')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render logo', () => {
|
|
render(<Header />);
|
|
const logo = screen.getByAltText('四川睿新致远科技有限公司');
|
|
expect(logo).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render desktop navigation', () => {
|
|
render(<Header />);
|
|
expect(screen.getByTestId('desktop-navigation')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render navigation items', () => {
|
|
render(<Header />);
|
|
expect(screen.getByText('产品')).toBeInTheDocument();
|
|
expect(screen.getByText('解决方案')).toBeInTheDocument();
|
|
expect(screen.getByText('服务')).toBeInTheDocument();
|
|
expect(screen.getByText('关于我们')).toBeInTheDocument();
|
|
expect(screen.getByText('联系我们')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render consult button', () => {
|
|
render(<Header />);
|
|
expect(screen.getByTestId('consult-button')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render mobile menu button', () => {
|
|
render(<Header />);
|
|
expect(screen.getByTestId('mobile-menu-button')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Mobile Menu', () => {
|
|
it('should toggle mobile menu on button click', async () => {
|
|
render(<Header />);
|
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
|
|
|
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(menuButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-navigation')).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(menuButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show X icon when menu is open', async () => {
|
|
render(<Header />);
|
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
|
|
|
fireEvent.click(menuButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('x-icon')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show Menu icon when menu is closed', () => {
|
|
render(<Header />);
|
|
expect(screen.getByTestId('menu-icon')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('should have proper ARIA attributes on menu button', () => {
|
|
render(<Header />);
|
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
|
|
|
expect(menuButton).toHaveAttribute('aria-expanded', 'false');
|
|
expect(menuButton).toHaveAttribute('aria-controls', 'mobile-menu');
|
|
expect(menuButton).toHaveAttribute('aria-label', '打开菜单');
|
|
});
|
|
|
|
it('should update aria-expanded when menu is toggled', async () => {
|
|
render(<Header />);
|
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
|
|
|
fireEvent.click(menuButton);
|
|
|
|
await waitFor(() => {
|
|
expect(menuButton).toHaveAttribute('aria-expanded', 'true');
|
|
expect(menuButton).toHaveAttribute('aria-label', '关闭菜单');
|
|
});
|
|
});
|
|
|
|
it('should not double-toggle menu on Enter or Space keydown', () => {
|
|
render(<Header />);
|
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
|
|
|
// Enter/Space 由原生 button 的 click 处理,keydown 不应再次 toggle
|
|
fireEvent.keyDown(menuButton, { key: 'Enter' });
|
|
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
|
|
|
fireEvent.keyDown(menuButton, { key: ' ' });
|
|
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should have proper navigation role', () => {
|
|
render(<Header />);
|
|
const nav = screen.getByTestId('desktop-navigation');
|
|
expect(nav).toHaveAttribute('role', 'navigation');
|
|
});
|
|
});
|
|
|
|
describe('Navigation', () => {
|
|
it('should have correct href for navigation items', () => {
|
|
render(<Header />);
|
|
const contactLink = screen.getByText('联系我们').closest('a');
|
|
const serviceLink = screen.getByText('服务').closest('a');
|
|
|
|
expect(contactLink).toHaveAttribute('href', '/contact');
|
|
expect(serviceLink).toHaveAttribute('href', '/services');
|
|
});
|
|
});
|
|
});
|