16. Формы и взаимодействие

Тестирование форм
Заголовок раздела «Тестирование форм»import { useState } from 'react';
interface LoginFormProps { onSubmit: (data: { email: string; password: string }) => void;}
export function LoginForm({ onSubmit }: LoginFormProps) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [errors, setErrors] = useState<Record<string, string>>({});
const validate = () => { const newErrors: Record<string, string> = {}; if (!email) newErrors.email = 'Email is required'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) newErrors.email = 'Invalid email format'; if (!password) newErrors.password = 'Password is required'; else if (password.length < 8) newErrors.password = 'Password must be at least 8 characters'; return newErrors; };
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const validationErrors = validate(); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } onSubmit({ email, password }); };
return ( <form onSubmit={handleSubmit} noValidate> <div> <label htmlFor="email">Email</label> <input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} aria-describedby={errors.email ? 'email-error' : undefined} aria-invalid={!!errors.email} /> {errors.email && ( <span id="email-error" role="alert">{errors.email}</span> )} </div>
<div> <label htmlFor="password">Password</label> <input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} aria-invalid={!!errors.password} /> {errors.password && ( <span role="alert">{errors.password}</span> )} </div>
<button type="submit">Login</button> </form> );}import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { LoginForm } from './LoginForm';
describe('LoginForm', () => { const user = userEvent.setup(); const mockSubmit = jest.fn();
beforeEach(() => { mockSubmit.mockClear(); render(<LoginForm onSubmit={mockSubmit} />); });
test('renders form fields', () => { expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); });
test('submits with valid data', async () => { await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /login/i }));
expect(mockSubmit).toHaveBeenCalledWith({ password: 'password123', }); });
test('shows error for empty email', async () => { await user.click(screen.getByRole('button', { name: /login/i }));
expect(await screen.findByRole('alert', { name: /email is required/i })).toBeInTheDocument(); expect(mockSubmit).not.toHaveBeenCalled(); });
test('shows error for invalid email', async () => { await user.type(screen.getByLabelText(/email/i), 'notanemail'); await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/invalid email format/i)).toBeInTheDocument(); });
test('shows error for short password', async () => { await user.type(screen.getByLabelText(/password/i), '123'); await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/at least 8 characters/i)).toBeInTheDocument(); expect(mockSubmit).not.toHaveBeenCalled(); });
test('clears errors on valid input', async () => { // Показать ошибку await user.click(screen.getByRole('button', { name: /login/i })); expect(screen.getByText(/email is required/i)).toBeInTheDocument();
// Исправить await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.queryByRole('alert')).not.toBeInTheDocument(); });});Тестирование Select и Checkbox
Заголовок раздела «Тестирование Select и Checkbox»export function PreferencesForm({ onSave }) { return ( <form> <label> <input type="checkbox" name="notifications" /> Email notifications </label>
<label> Theme <select name="theme"> <option value="light">Light</option> <option value="dark">Dark</option> <option value="auto">Auto</option> </select> </label>
<button type="submit">Save preferences</button> </form> );}test('saves preferences', async () => { const user = userEvent.setup(); const onSave = jest.fn(); render(<PreferencesForm onSave={onSave} />);
// Включить уведомления await user.click(screen.getByRole('checkbox', { name: /email notifications/i })); expect(screen.getByRole('checkbox')).toBeChecked();
// Выбрать тему await user.selectOptions( screen.getByRole('combobox', { name: /theme/i }), 'dark' ); expect(screen.getByRole('combobox')).toHaveValue('dark');
await user.click(screen.getByRole('button', { name: /save/i })); expect(onSave).toHaveBeenCalled();});Тестирование React Hook Form
Заголовок раздела «Тестирование React Hook Form»import { useForm } from 'react-hook-form';
export function ContactForm({ onSubmit }) { const { register, handleSubmit, formState: { errors } } = useForm(); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name', { required: 'Name is required' })} placeholder="Your name" /> {errors.name && <span>{errors.name.message}</span>} <button type="submit">Send</button> </form> );}test('validates required fields', async () => { const user = userEvent.setup(); render(<ContactForm onSubmit={jest.fn()} />);
await user.click(screen.getByRole('button', { name: /send/i }));
expect(await screen.findByText('Name is required')).toBeInTheDocument();});Практические задания
Заголовок раздела «Практические задания»- Протестируй форму поиска с автодополнением
- Протестируй форму с несколькими шагами (Step 1 → Step 2 → Review)
- Протестируй форму с react-hook-form и Zod валидацией
- Тестируй форму с точки зрения пользователя: ввод → отправка → результат
- getByLabelText — лучший способ найти поля формы
- Тестируй валидацию: пустые поля, неверные форматы, граничные случаи
- Не тестируй реализацию формы — тестируй поведение