ebaa7f3c50
ci/woodpecker/manual/woodpecker Pipeline was successful
- 移除未使用的YAML锚点定义 - 替换commands字段中的锚点引用为实际值 - 移除有问题的通知步骤 - 修复测试文件中的问题 - 添加新的测试用例和配置文件
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import { describe, it, expect, jest, beforeAll, afterEach } from '@jest/globals';
|
|
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import '@testing-library/jest-dom';
|
|
import userEvent from '@testing-library/user-event';
|
|
|
|
interface MotionComponentProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface InputComponentProps {
|
|
label?: string;
|
|
id?: string;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
value?: string;
|
|
onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
onBlur?: () => void;
|
|
error?: string;
|
|
rows?: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface ToastComponentProps {
|
|
message?: string;
|
|
type?: string;
|
|
onClose?: () => void;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
global.fetch = jest.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true }),
|
|
} as Response)
|
|
);
|
|
|
|
jest.mock('framer-motion', () => ({
|
|
motion: {
|
|
div: ({ children, className, ...props }: MotionComponentProps) => (
|
|
<div className={className} {...props}>
|
|
{children}
|
|
</div>
|
|
),
|
|
section: ({ children, className, ...props }: MotionComponentProps) => (
|
|
<section className={className} {...props}>
|
|
{children}
|
|
</section>
|
|
),
|
|
},
|
|
AnimatePresence: ({ children }: MotionComponentProps) => <>{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" />,
|
|
RefreshCw: () => <span data-testid="refresh-cw-icon" />,
|
|
}));
|
|
|
|
jest.mock('@/lib/sanitize', () => ({
|
|
sanitizeInput: (value: string) => value,
|
|
}));
|
|
|
|
jest.mock('@/lib/csrf', () => ({
|
|
generateCSRFToken: jest.fn(() => 'test-csrf-token'),
|
|
setCSRFTokenToStorage: jest.fn(),
|
|
getCSRFTokenFromStorage: jest.fn(() => 'test-csrf-token'),
|
|
}));
|
|
|
|
const { generateCSRFToken, setCSRFTokenToStorage } = jest.requireMock('@/lib/csrf') as {
|
|
generateCSRFToken: jest.Mock;
|
|
setCSRFTokenToStorage: jest.Mock;
|
|
};
|
|
|
|
jest.mock('@/lib/security/captcha', () => ({
|
|
generateCaptcha: jest.fn(() => ({
|
|
question: '1 + 1 = ?',
|
|
answer: 2,
|
|
hash: 'test-hash',
|
|
timestamp: Date.now(),
|
|
})),
|
|
}));
|
|
|
|
const { generateCaptcha } = jest.requireMock('@/lib/security/captcha') as {
|
|
generateCaptcha: jest.Mock;
|
|
};
|
|
|
|
jest.mock('@/lib/constants', () => ({
|
|
COMPANY_INFO: {
|
|
name: '四川睿新致远科技有限公司',
|
|
email: 'contact@novalon.cn',
|
|
phone: '028-88888888',
|
|
address: '中国四川省成都市龙泉驿区幸福路12号',
|
|
},
|
|
}));
|
|
|
|
jest.mock('@/components/ui/button', () => ({
|
|
Button: ({ children, className, disabled, ...props }: MotionComponentProps) => (
|
|
<button className={className} disabled={disabled} {...props}>
|
|
{children}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
jest.mock('@/components/ui/input', () => ({
|
|
Input: ({ label, id, placeholder, required, value, onChange, onBlur, error, ...props }: InputComponentProps) => (
|
|
<div>
|
|
<label htmlFor={id}>{label}{required && '*'}</label>
|
|
<input
|
|
id={id}
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={onChange}
|
|
onBlur={onBlur}
|
|
data-testid={`${id}-input`}
|
|
{...props}
|
|
/>
|
|
{error && <span data-testid={`${id}-error`}>{error}</span>}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
jest.mock('@/components/ui/textarea', () => ({
|
|
Textarea: ({ label, id, placeholder, rows, required, value, onChange, onBlur, error, ...props }: InputComponentProps) => (
|
|
<div>
|
|
<label htmlFor={id}>{label}{required && '*'}</label>
|
|
<textarea
|
|
id={id}
|
|
placeholder={placeholder}
|
|
rows={rows}
|
|
value={value}
|
|
onChange={onChange}
|
|
onBlur={onBlur}
|
|
data-testid={`${id}-input`}
|
|
{...props}
|
|
/>
|
|
{error && <span data-testid={`${id}-error`}>{error}</span>}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
jest.mock('@/components/ui/toast', () => ({
|
|
Toast: ({ message, type, onClose, ...props }: ToastComponentProps) => (
|
|
<div data-testid="toast-notification" data-type={type} {...props}>
|
|
{message}
|
|
<button onClick={onClose}>关闭</button>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
import { ContactSection } from './contact-section';
|
|
|
|
describe('ContactSection', () => {
|
|
beforeAll(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render contact section', () => {
|
|
render(<ContactSection />);
|
|
const section = document.querySelector('section#contact');
|
|
expect(section).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render contact form', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByTestId('name-input')).toBeInTheDocument();
|
|
expect(screen.getByTestId('phone-input')).toBeInTheDocument();
|
|
expect(screen.getByTestId('email-input')).toBeInTheDocument();
|
|
expect(screen.getByTestId('message-input')).toBeInTheDocument();
|
|
expect(screen.getByTestId('captcha-question')).toBeInTheDocument();
|
|
expect(screen.getByTestId('captcha-input')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render submit button', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByRole('button', { name: /发送消息/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render company contact information', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument();
|
|
expect(screen.getByText('中国四川省成都市龙泉驿区幸福路12号')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render work hours card', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByTestId('work-hours-card')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Form Validation', () => {
|
|
it('should show error for invalid name', async () => {
|
|
render(<ContactSection />);
|
|
const nameInput = screen.getByTestId('name-input');
|
|
|
|
await userEvent.type(nameInput, '张');
|
|
fireEvent.blur(nameInput);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('name-error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show error for invalid phone', async () => {
|
|
render(<ContactSection />);
|
|
const phoneInput = screen.getByTestId('phone-input');
|
|
|
|
await userEvent.type(phoneInput, '1234567890');
|
|
fireEvent.blur(phoneInput);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('phone-error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show error for invalid email', async () => {
|
|
render(<ContactSection />);
|
|
const emailInput = screen.getByTestId('email-input');
|
|
|
|
await userEvent.type(emailInput, 'invalid-email');
|
|
fireEvent.blur(emailInput);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('email-error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show error for short message', async () => {
|
|
render(<ContactSection />);
|
|
const messageInput = screen.getByTestId('message-input');
|
|
|
|
await userEvent.type(messageInput, '短留言');
|
|
fireEvent.blur(messageInput);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('message-error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('should have proper form labels', () => {
|
|
render(<ContactSection />);
|
|
|
|
expect(screen.getByLabelText(/姓名/)).toBeInTheDocument();
|
|
expect(screen.getByLabelText(/电话/)).toBeInTheDocument();
|
|
expect(screen.getByLabelText(/邮箱/)).toBeInTheDocument();
|
|
expect(screen.getByLabelText(/留言/)).toBeInTheDocument();
|
|
expect(screen.getByLabelText(/验证码/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should have proper ARIA attributes', () => {
|
|
render(<ContactSection />);
|
|
const section = document.querySelector('section#contact');
|
|
expect(section).toHaveAttribute('role', 'region');
|
|
expect(section).toHaveAttribute('aria-labelledby', 'contact-heading');
|
|
});
|
|
});
|
|
|
|
describe('CSRF Protection', () => {
|
|
it('should generate CSRF token on mount', () => {
|
|
render(<ContactSection />);
|
|
|
|
expect(generateCSRFToken).toHaveBeenCalled();
|
|
expect(setCSRFTokenToStorage).toHaveBeenCalledWith('test-csrf-token');
|
|
});
|
|
});
|
|
|
|
describe('Captcha Functionality', () => {
|
|
it('should render captcha question', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByTestId('captcha-question')).toBeInTheDocument();
|
|
expect(screen.getByText('1 + 1 = ?')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render captcha input', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByTestId('captcha-input')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render refresh captcha button', () => {
|
|
render(<ContactSection />);
|
|
expect(screen.getByTestId('refresh-captcha')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should refresh captcha when refresh button is clicked', async () => {
|
|
render(<ContactSection />);
|
|
|
|
const refreshButton = screen.getByTestId('refresh-captcha');
|
|
await userEvent.click(refreshButton);
|
|
|
|
expect(generateCaptcha).toHaveBeenCalled();
|
|
});
|
|
|
|
it.skip('should show error for invalid captcha', async () => {
|
|
render(<ContactSection />);
|
|
const nameInput = screen.getByTestId('name-input');
|
|
const phoneInput = screen.getByTestId('phone-input');
|
|
const emailInput = screen.getByTestId('email-input');
|
|
const messageInput = screen.getByTestId('message-input');
|
|
const captchaInput = screen.getByTestId('captcha-input');
|
|
const submitButton = screen.getByRole('button', { name: /发送消息/ });
|
|
|
|
await userEvent.type(nameInput, '张三');
|
|
await userEvent.type(phoneInput, '13800138000');
|
|
await userEvent.type(emailInput, 'test@example.com');
|
|
await userEvent.type(messageInput, '这是一条测试留言内容');
|
|
|
|
captchaInput.focus();
|
|
fireEvent.change(captchaInput, { target: { value: '3' } });
|
|
|
|
await userEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('captcha-error')).toBeInTheDocument();
|
|
}, { timeout: 3000 });
|
|
});
|
|
});
|
|
});
|