feat: 增加测试覆盖率并优化代码质量
test: 添加单元测试和端到端测试 refactor: 重构登录页面和上传模块 ci: 更新测试覆盖率阈值至42% build: 添加测试相关依赖 docs: 更新测试文档 style: 修复代码格式问题
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
span: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
h1: ({ children, className, ...props }: any) => (
|
||||
<h1 className={className} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, className, ...props }: any) => (
|
||||
<h2 className={className} {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Lightbulb: () => <span data-testid="lightbulb-icon" />,
|
||||
Users: () => <span data-testid="users-icon" />,
|
||||
Target: () => <span data-testid="target-icon" />,
|
||||
Award: () => <span data-testid="award-icon" />,
|
||||
MapPin: () => <span data-testid="map-pin-icon" />,
|
||||
Mail: () => <span data-testid="mail-icon" />,
|
||||
Phone: () => <span data-testid="phone-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/card', () => ({
|
||||
Card: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CardContent: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/flip-clock', () => ({
|
||||
FlipClock: ({ years, months, days }: any) => (
|
||||
<div data-testid="flip-clock">
|
||||
{years}年 {months}月 {days}天
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
shortName: '睿新致遠',
|
||||
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
|
||||
address: '四川省成都市龙泉驿区',
|
||||
email: 'contact@ruixin.com',
|
||||
phone: '028-12345678',
|
||||
},
|
||||
STATS: [
|
||||
{ value: '10+', label: '企业客户' },
|
||||
{ value: '20+', label: '成功案例' },
|
||||
{ value: '30+', label: '项目交付' },
|
||||
{ value: '12+', label: '年行业经验' },
|
||||
],
|
||||
}));
|
||||
|
||||
import AboutPage from './page';
|
||||
|
||||
describe('AboutPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render about page', () => {
|
||||
const { container } = render(<AboutPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company introduction', () => {
|
||||
render(<AboutPage />);
|
||||
const intro = screen.getByText(/关于我们/i);
|
||||
expect(intro).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company history', () => {
|
||||
render(<AboutPage />);
|
||||
const history = screen.getByText(/发展历程/i);
|
||||
expect(history).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company culture', () => {
|
||||
render(<AboutPage />);
|
||||
const culture = screen.getByText(/核心价值观/i);
|
||||
expect(culture).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render team members', () => {
|
||||
render(<AboutPage />);
|
||||
const team = screen.getByText(/团队组建/i);
|
||||
expect(team).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact information', () => {
|
||||
render(<AboutPage />);
|
||||
const contact = screen.getByText(/联系我们/i);
|
||||
expect(contact).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render statistics', () => {
|
||||
render(<AboutPage />);
|
||||
const stats = screen.getByText(/企业客户/i);
|
||||
expect(stats).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<AboutPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { CaseDetailClient } from './client';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: jest.fn(),
|
||||
back: jest.fn(),
|
||||
forward: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: { children: React.ReactNode; href: string }) => {
|
||||
return <a href={href}>{children}</a>;
|
||||
};
|
||||
});
|
||||
|
||||
const mockCaseItem = {
|
||||
id: 'test-case',
|
||||
title: '测试案例标题',
|
||||
client: '测试客户',
|
||||
industry: '制造业',
|
||||
description: '这是一个测试案例的描述',
|
||||
results: [
|
||||
{ label: '业务处理效率', value: '提升50%' },
|
||||
{ label: '客户满意度', value: '提升30%' },
|
||||
],
|
||||
tags: ['AI', '大数据'],
|
||||
};
|
||||
|
||||
describe('CaseDetailClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render case detail page', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case title', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('测试案例标题');
|
||||
});
|
||||
|
||||
it('should render case client name', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const clients = screen.getAllByText('测试客户');
|
||||
expect(clients.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render case industry badge', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const industries = screen.getAllByText('制造业');
|
||||
expect(industries.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render case description', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const description = screen.getByText('这是一个测试案例的描述');
|
||||
expect(description).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case results', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const result1 = screen.getByText('提升50%');
|
||||
const result2 = screen.getByText('提升30%');
|
||||
expect(result1).toBeInTheDocument();
|
||||
expect(result2).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case tags', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const tags = screen.getAllByText('AI');
|
||||
expect(tags.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render contact button', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const contactButton = screen.getByRole('link', { name: /联系我们/i });
|
||||
expect(contactButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sections', () => {
|
||||
it('should render customer challenges section', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const section = screen.getByText('客户遇到的成长瓶颈');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render solution section', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const section = screen.getByText('我们如何智连未来');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render growth story section', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const section = screen.getByText('共同成长的故事');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render achievements section', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const section = screen.getByText('今天,他们走到了哪里');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render testimonial section', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const section = screen.getByText('客户证言精选');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have back button', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const backButton = screen.getByRole('button', { name: /返回/i });
|
||||
expect(backButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have main landmark', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<CaseDetailClient caseItem={mockCaseItem as any} />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
|
||||
const h2s = screen.getAllByRole('heading', { level: 2 });
|
||||
expect(h2s.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
ArrowLeft: () => <span data-testid="arrow-left-icon" />,
|
||||
Building2: () => <span data-testid="building-icon" />,
|
||||
Calendar: () => <span data-testid="calendar-icon" />,
|
||||
TrendingUp: () => <span data-testid="trending-up-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
CASES: [
|
||||
{
|
||||
id: 'case-1',
|
||||
client: '客户A',
|
||||
title: '数字化转型案例',
|
||||
industry: '制造业',
|
||||
description: '帮助客户实现数字化转型',
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
client: '客户B',
|
||||
title: 'ERP系统实施案例',
|
||||
industry: '零售业',
|
||||
description: 'ERP系统成功实施',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
import CasesPage from './page';
|
||||
|
||||
describe('CasesPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render cases page', () => {
|
||||
const { container } = render(<CasesPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page header', () => {
|
||||
render(<CasesPage />);
|
||||
const title = screen.getByText(/与谁同行/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back to home link', () => {
|
||||
render(<CasesPage />);
|
||||
const backLink = screen.getByText(/返回首页/i);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case cards', () => {
|
||||
render(<CasesPage />);
|
||||
const caseTitles = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(caseTitles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render case categories', () => {
|
||||
render(<CasesPage />);
|
||||
const categories = screen.getByText(/制造业/i);
|
||||
expect(categories).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render CTA section', () => {
|
||||
render(<CasesPage />);
|
||||
const cta = screen.getByText(/准备开始您的数字化转型之旅/i);
|
||||
expect(cta).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have case detail links', () => {
|
||||
render(<CasesPage />);
|
||||
const links = screen.getAllByRole('link');
|
||||
const caseLinks = links.filter(link => link.getAttribute('href')?.startsWith('/cases/'));
|
||||
expect(caseLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have contact links', () => {
|
||||
render(<CasesPage />);
|
||||
const contactLinks = screen.getAllByRole('link', { name: /联系我们|立即咨询/i });
|
||||
expect(contactLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<CasesPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Mail: () => <span data-testid="mail-icon" />,
|
||||
Phone: () => <span data-testid="phone-icon" />,
|
||||
MapPin: () => <span data-testid="map-pin-icon" />,
|
||||
Send: () => <span data-testid="send-icon" />,
|
||||
Loader2: () => <span data-testid="loader-icon" />,
|
||||
Clock: () => <span data-testid="clock-icon" />,
|
||||
HeadphonesIcon: () => <span data-testid="headphones-icon" />,
|
||||
CheckCircle2: () => <span data-testid="check-circle-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, disabled, ...props }: any) => (
|
||||
<button className={className} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/input', () => ({
|
||||
Input: ({ label, error, 'data-testid': testId, id, onChange, onBlur, ...props }: any) => (
|
||||
<div>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
<input
|
||||
id={id}
|
||||
data-testid={testId}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span data-testid={`${id}-error`} role="alert">{error}</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/textarea', () => ({
|
||||
Textarea: ({ label, error, 'data-testid': testId, id, onChange, onBlur, ...props }: any) => (
|
||||
<div>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
<textarea
|
||||
id={id}
|
||||
data-testid={testId}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span data-testid={`${id}-error`} role="alert">{error}</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/toast', () => ({
|
||||
Toast: ({ message, type, onClose }: any) => (
|
||||
<div data-testid="toast" data-type={type}>
|
||||
{message}
|
||||
<button onClick={onClose}>关闭</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/sanitize', () => ({
|
||||
sanitizeInput: (input: string) => input,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/csrf', () => ({
|
||||
generateCSRFToken: () => 'test-csrf-token',
|
||||
setCSRFTokenToStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
email: 'contact@ruixin.com',
|
||||
phone: '028-12345678',
|
||||
address: '四川省成都市龙泉驿区',
|
||||
},
|
||||
}));
|
||||
|
||||
import ContactPage from './page';
|
||||
|
||||
describe('ContactPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(global.fetch as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render contact page', () => {
|
||||
const { container } = render(<ContactPage />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page title', () => {
|
||||
render(<ContactPage />);
|
||||
const title = screen.getByText(/开启/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render name input', () => {
|
||||
render(<ContactPage />);
|
||||
const nameInput = screen.getByPlaceholderText(/请输入您的姓名/i);
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render email input', () => {
|
||||
render(<ContactPage />);
|
||||
const emailInput = screen.getByPlaceholderText(/请输入您的邮箱/i);
|
||||
expect(emailInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render phone input', () => {
|
||||
render(<ContactPage />);
|
||||
const phoneInput = screen.getByPlaceholderText(/请输入您的电话/i);
|
||||
expect(phoneInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render message textarea', () => {
|
||||
render(<ContactPage />);
|
||||
const messageTextarea = screen.getByPlaceholderText(/请输入您想咨询的内容/i);
|
||||
expect(messageTextarea).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render submit button', () => {
|
||||
render(<ContactPage />);
|
||||
const submitButton = screen.getByTestId('submit-button');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact information', () => {
|
||||
render(<ContactPage />);
|
||||
const contactInfo = screen.getByTestId('contact-info');
|
||||
expect(contactInfo).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('should show error for short name on blur', async () => {
|
||||
render(<ContactPage />);
|
||||
const nameInput = screen.getByPlaceholderText(/请输入您的姓名/i);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(nameInput, { target: { value: 'A' } });
|
||||
fireEvent.blur(nameInput);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for invalid phone on blur', async () => {
|
||||
render(<ContactPage />);
|
||||
const phoneInput = screen.getByPlaceholderText(/请输入您的电话/i);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(phoneInput, { target: { value: '12345' } });
|
||||
fireEvent.blur(phoneInput);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for invalid email on blur', async () => {
|
||||
render(<ContactPage />);
|
||||
const emailInput = screen.getByPlaceholderText(/请输入您的邮箱/i);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
|
||||
fireEvent.blur(emailInput);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('should submit form successfully', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
});
|
||||
|
||||
render(<ContactPage />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText(/请输入您的姓名/i);
|
||||
const phoneInput = screen.getByPlaceholderText(/请输入您的电话/i);
|
||||
const emailInput = screen.getByPlaceholderText(/请输入您的邮箱/i);
|
||||
const subjectInput = screen.getByPlaceholderText(/请输入消息主题/i);
|
||||
const messageTextarea = screen.getByPlaceholderText(/请输入您想咨询的内容/i);
|
||||
const submitButton = screen.getByTestId('submit-button');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(nameInput, { target: { value: '张三' } });
|
||||
fireEvent.change(phoneInput, { target: { value: '13800138000' } });
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||
fireEvent.change(subjectInput, { target: { value: '测试主题' } });
|
||||
fireEvent.change(messageTextarea, { target: { value: '这是一条测试留言内容' } });
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/contact', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have main landmark', () => {
|
||||
render(<ContactPage />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<ContactPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { NewsDetailClient } from './NewsDetailClient';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: jest.fn(),
|
||||
back: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: { children: React.ReactNode; href: string }) => {
|
||||
return <a href={href}>{children}</a>;
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
const mockNews = {
|
||||
id: 'test-news',
|
||||
title: '测试新闻标题',
|
||||
category: '公司新闻',
|
||||
date: '2024-01-01',
|
||||
excerpt: '这是测试新闻摘要',
|
||||
content: '这是测试新闻内容',
|
||||
};
|
||||
|
||||
describe('NewsDetailClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render news detail page', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const container = screen.getByText('测试新闻标题').closest('div');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news title', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('测试新闻标题');
|
||||
});
|
||||
|
||||
it('should render news category', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const categories = screen.getAllByText('公司新闻');
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render news date', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const date = screen.getByText('2024-01-01');
|
||||
expect(date).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news excerpt', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const excerpt = screen.getByText('这是测试新闻摘要');
|
||||
expect(excerpt).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news content', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const content = screen.getByText('这是测试新闻内容');
|
||||
expect(content).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have back button', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const backButtons = screen.getAllByRole('button', { name: /返回/i });
|
||||
expect(backButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<NewsDetailClient news={mockNews as any} />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Search: () => <span data-testid="search-icon" />,
|
||||
Calendar: () => <span data-testid="calendar-icon" />,
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
ArrowLeft: () => <span data-testid="arrow-left-icon" />,
|
||||
Filter: () => <span data-testid="filter-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, onClick, ...props }: any) => (
|
||||
<button className={className} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/card', () => ({
|
||||
Card: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CardContent: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/input', () => ({
|
||||
Input: ({ className, ...props }: any) => (
|
||||
<input className={className} {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
NEWS: [
|
||||
{
|
||||
id: 'news-1',
|
||||
title: '公司成立新闻',
|
||||
category: '公司新闻',
|
||||
date: '2026-01-15',
|
||||
excerpt: '公司正式成立,开启数字化转型之旅',
|
||||
},
|
||||
{
|
||||
id: 'news-2',
|
||||
title: '产品发布新闻',
|
||||
category: '产品发布',
|
||||
date: '2026-02-01',
|
||||
excerpt: '新产品正式发布',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
import NewsListPage from './page';
|
||||
|
||||
describe('NewsListPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render news page', () => {
|
||||
const { container } = render(<NewsListPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page header', () => {
|
||||
render(<NewsListPage />);
|
||||
const title = screen.getByText(/新闻动态/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back to home link', () => {
|
||||
render(<NewsListPage />);
|
||||
const backLink = screen.getByText(/返回首页/i);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news cards', () => {
|
||||
render(<NewsListPage />);
|
||||
const newsCards = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(newsCards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render category filter', () => {
|
||||
render(<NewsListPage />);
|
||||
const filterLabel = screen.getByText(/分类筛选/i);
|
||||
expect(filterLabel).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render search input', () => {
|
||||
render(<NewsListPage />);
|
||||
const searchInput = screen.getByPlaceholderText(/搜索新闻/i);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('should filter news by category', () => {
|
||||
render(<NewsListPage />);
|
||||
const companyNewsButton = screen.getByRole('button', { name: '公司新闻' });
|
||||
fireEvent.click(companyNewsButton);
|
||||
|
||||
const newsCards = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(newsCards.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter news by search query', () => {
|
||||
render(<NewsListPage />);
|
||||
const searchInput = screen.getByPlaceholderText(/搜索新闻/i);
|
||||
fireEvent.change(searchInput, { target: { value: '成立' } });
|
||||
|
||||
const newsCards = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(newsCards.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have news detail links', () => {
|
||||
render(<NewsListPage />);
|
||||
const links = screen.getAllByRole('link');
|
||||
const newsLinks = links.filter(link => link.getAttribute('href')?.startsWith('/news/'));
|
||||
expect(newsLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<NewsListPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useSearchParams: () => ({
|
||||
get: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
span: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
h1: ({ children, className, ...props }: any) => (
|
||||
<h1 className={className} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('next/dynamic', () => {
|
||||
const React = require('react');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: (importFn: any, options: any) => {
|
||||
const componentName = importFn.toString().match(/\/(\w+-section)/)?.[1] || 'dynamic-component';
|
||||
const idMap: Record<string, string> = {
|
||||
'services-section': 'services',
|
||||
'products-section': 'products',
|
||||
'cases-section': 'cases',
|
||||
'about-section': 'about',
|
||||
'news-section': 'news',
|
||||
};
|
||||
const id = idMap[componentName] || componentName;
|
||||
return React.forwardRef((props: any, ref: any) => (
|
||||
<section id={id} data-testid={componentName} {...props} />
|
||||
));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/components/sections/hero-section', () => ({
|
||||
HeroSection: () => (
|
||||
<section id="home" aria-labelledby="hero-heading">
|
||||
<h1 id="hero-heading">睿新致遠</h1>
|
||||
</section>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/loading-skeleton', () => ({
|
||||
SectionSkeleton: () => <div data-testid="section-skeleton">Loading...</div>,
|
||||
}));
|
||||
|
||||
import HomePage from './page';
|
||||
|
||||
describe('HomePage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render home page', () => {
|
||||
render(<HomePage />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render hero section', () => {
|
||||
render(<HomePage />);
|
||||
const heroSection = document.querySelector('#home');
|
||||
expect(heroSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render services section', () => {
|
||||
render(<HomePage />);
|
||||
const servicesSection = document.querySelector('#services');
|
||||
expect(servicesSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render products section', () => {
|
||||
render(<HomePage />);
|
||||
const productsSection = document.querySelector('#products');
|
||||
expect(productsSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render cases section', () => {
|
||||
render(<HomePage />);
|
||||
const casesSection = document.querySelector('#cases');
|
||||
expect(casesSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render about section', () => {
|
||||
render(<HomePage />);
|
||||
const aboutSection = document.querySelector('#about');
|
||||
expect(aboutSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news section', () => {
|
||||
render(<HomePage />);
|
||||
const newsSection = document.querySelector('#news');
|
||||
expect(newsSection).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have main landmark', () => {
|
||||
render(<HomePage />);
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<HomePage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ProductDetailPage from './page';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
notFound: jest.fn(),
|
||||
useRouter: jest.fn(() => ({
|
||||
push: jest.fn(),
|
||||
back: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: { children: React.ReactNode; href: string }) => {
|
||||
return <a href={href}>{children}</a>;
|
||||
};
|
||||
});
|
||||
|
||||
const mockProduct = {
|
||||
id: 'test-product',
|
||||
title: '测试产品',
|
||||
category: '企业软件',
|
||||
description: '这是测试产品描述',
|
||||
overview: '这是测试产品概述',
|
||||
features: ['功能1', '功能2'],
|
||||
benefits: ['优势1', '优势2'],
|
||||
process: ['步骤1', '步骤2'],
|
||||
specs: ['规格1', '规格2'],
|
||||
pricing: {
|
||||
base: '¥10,000/年',
|
||||
standard: '¥30,000/年',
|
||||
enterprise: '定制',
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
PRODUCTS: [
|
||||
{
|
||||
id: 'test-product',
|
||||
title: '测试产品',
|
||||
category: '企业软件',
|
||||
description: '这是测试产品描述',
|
||||
overview: '这是测试产品概述',
|
||||
features: ['功能1', '功能2'],
|
||||
benefits: ['优势1', '优势2'],
|
||||
process: ['步骤1', '步骤2'],
|
||||
specs: ['规格1', '规格2'],
|
||||
pricing: {
|
||||
base: '¥10,000/年',
|
||||
standard: '¥30,000/年',
|
||||
enterprise: '定制',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('ProductDetailPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render product detail page', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const container = screen.getByText('测试产品').closest('div');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product title', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('测试产品');
|
||||
});
|
||||
|
||||
it('should render product category', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const category = screen.getByText('企业软件');
|
||||
expect(category).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product description', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const description = screen.getByText('这是测试产品描述');
|
||||
expect(description).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product overview section', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const overview = screen.getByText('产品概述');
|
||||
expect(overview).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product features section', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const features = screen.getByText('核心功能');
|
||||
expect(features).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product benefits section', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const benefits = screen.getByText('产品优势');
|
||||
expect(benefits).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pricing section', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const pricing = screen.getByText('价格方案');
|
||||
expect(pricing).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have contact link', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const contactLinks = screen.getAllByRole('link', { name: /联系我们/i });
|
||||
expect(contactLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', async () => {
|
||||
const page = await ProductDetailPage({ params: Promise.resolve({ id: 'test-product' }) });
|
||||
render(page);
|
||||
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
|
||||
const h2s = screen.getAllByRole('heading', { level: 2 });
|
||||
expect(h2s.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
ArrowLeft: () => <span data-testid="arrow-left-icon" />,
|
||||
Check: () => <span data-testid="check-icon" />,
|
||||
TrendingUp: () => <span data-testid="trending-up-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/card', () => ({
|
||||
Card: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CardContent: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CardHeader: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CardTitle: ({ children, className, ...props }: any) => (
|
||||
<h3 className={className} {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
CardDescription: ({ children, className, ...props }: any) => (
|
||||
<p className={className} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
PRODUCTS: [
|
||||
{
|
||||
id: 'erp',
|
||||
title: 'ERP企业资源计划',
|
||||
category: '企业管理',
|
||||
description: '一站式企业资源管理解决方案',
|
||||
features: ['财务管理', '供应链管理', '生产管理', '人力资源'],
|
||||
benefits: ['提高运营效率', '降低管理成本'],
|
||||
},
|
||||
{
|
||||
id: 'crm',
|
||||
title: 'CRM客户关系管理',
|
||||
category: '客户管理',
|
||||
description: '智能化客户关系管理平台',
|
||||
features: ['客户管理', '销售管理', '营销自动化', '数据分析'],
|
||||
benefits: ['提升客户满意度', '增加销售收入'],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
import ProductsPage from './page';
|
||||
|
||||
describe('ProductsPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render products page', () => {
|
||||
const { container } = render(<ProductsPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page header', () => {
|
||||
render(<ProductsPage />);
|
||||
const title = screen.getByText(/产品服务/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back to home link', () => {
|
||||
render(<ProductsPage />);
|
||||
const backLink = screen.getByText(/返回首页/i);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product cards', () => {
|
||||
render(<ProductsPage />);
|
||||
const productTitles = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(productTitles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render product categories', () => {
|
||||
render(<ProductsPage />);
|
||||
const categories = screen.getByText(/企业管理/i);
|
||||
expect(categories).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render CTA section', () => {
|
||||
render(<ProductsPage />);
|
||||
const cta = screen.getByText(/需要定制化解决方案/i);
|
||||
expect(cta).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have product detail links', () => {
|
||||
render(<ProductsPage />);
|
||||
const links = screen.getAllByRole('link');
|
||||
const productLinks = links.filter(link => link.getAttribute('href')?.startsWith('/products/'));
|
||||
expect(productLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have contact link', () => {
|
||||
render(<ProductsPage />);
|
||||
const contactLink = screen.getByRole('link', { name: /联系我们/i });
|
||||
expect(contactLink).toHaveAttribute('href', '/contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<ProductsPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
ArrowLeft: () => <span data-testid="arrow-left-icon" />,
|
||||
Code: () => <span data-testid="code-icon" />,
|
||||
Cloud: () => <span data-testid="cloud-icon" />,
|
||||
BarChart3: () => <span data-testid="bar-chart-icon" />,
|
||||
Shield: () => <span data-testid="shield-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/loading-skeleton', () => ({
|
||||
ServiceCardSkeleton: () => <div data-testid="service-card-skeleton">Loading...</div>,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
SERVICES: [
|
||||
{
|
||||
id: 'software-dev',
|
||||
title: '软件开发',
|
||||
icon: 'Code',
|
||||
description: '定制化软件开发服务',
|
||||
features: ['需求分析', '架构设计', '开发测试', '运维支持'],
|
||||
},
|
||||
{
|
||||
id: 'cloud-service',
|
||||
title: '云服务',
|
||||
icon: 'Cloud',
|
||||
description: '企业云服务解决方案',
|
||||
features: ['云迁移', '云原生', '云安全', '云运维'],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
import ServicesPage from './page';
|
||||
|
||||
describe('ServicesPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render services page', () => {
|
||||
const { container } = render(<ServicesPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page header', () => {
|
||||
render(<ServicesPage />);
|
||||
const title = screen.getByText(/核心业务/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back to home link', () => {
|
||||
render(<ServicesPage />);
|
||||
const backLink = screen.getByText(/返回首页/i);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading skeletons initially', () => {
|
||||
render(<ServicesPage />);
|
||||
const skeletons = screen.getAllByTestId('service-card-skeleton');
|
||||
expect(skeletons.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should render CTA section', () => {
|
||||
render(<ServicesPage />);
|
||||
const cta = screen.getByText(/准备开始您的数字化转型之旅/i);
|
||||
expect(cta).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have contact link', () => {
|
||||
render(<ServicesPage />);
|
||||
const contactLink = screen.getByRole('link', { name: /立即咨询/i });
|
||||
expect(contactLink).toHaveAttribute('href', '/contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<ServicesPage />);
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user