Mục tiêu: Đảm bảo component hoạt động đúng theo góc nhìn của người dùng.
1. Các Query cơ bản (Priority)
Khi chọn phần tử DOM, hãy theo thứ tự ưu tiên này (để đảm bảo Accessibility):
getByRole:getByRole('button', { name: /submit/i })(Tốt nhất).getByLabelText:getByLabelText(/password/i)(Tốt cho Form).getByPlaceholderText:getByPlaceholderText(/email/i).getByTestId:getByTestId('custom-element')(Chỉ dùng khi bí quá).
2. Case Study: Login Form
Form có: Input Email, Password, Nút Submit. Validate email rỗng.
tsx:
// Login.tsx
export default function Login({ onSubmit }) {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!email) {
setError("Email is required");
return;
}
onSubmit({ email });
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{error && <div role="alert">{error}</div>}
<button type="submit">Login</button>
</form>
);
}3. Viết Test (Login.test.tsx)
tsx:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; // 👈 Giả lập hành động user chuẩn hơn fireEvent
import Login from './Login';
import { vi } from 'vitest';
describe('Login Component', () => {
it('should show error when submitting empty email', async () => {
// 1. Arrange
const handleLogin = vi.fn();
render(<Login onSubmit={handleLogin} />);
// 2. Act (User bấm nút mà không nhập gì)
const submitBtn = screen.getByRole('button', { name: /login/i });
await userEvent.click(submitBtn);
// 3. Assert (Mong đợi thấy lỗi, hàm submit KHÔNG được gọi)
expect(await screen.findByRole('alert')).toHaveTextContent(/required/i);
expect(handleLogin).not.toHaveBeenCalled();
});
it('should call onSubmit with email when valid', async () => {
const handleLogin = vi.fn();
render(<Login onSubmit={handleLogin} />);
// Act (User nhập liệu)
const emailInput = screen.getByLabelText(/email/i);
await userEvent.type(emailInput, 'test@example.com');
const submitBtn = screen.getByRole('button', { name: /login/i });
await userEvent.click(submitBtn);
// Assert
expect(handleLogin).toHaveBeenCalledWith({ email: 'test@example.com' });
});
});4. Key Takeaways
- Dùng
userEvent(async) thay vìfireEvent. Nó giả lập đầy đủ sự kiện (focus, keydown, keyup, change) giống trình duyệt thật hơn. - Dùng
findByRole(async) nếu bạn mong chờ element xuất hiện sau một khoảng thời gian (VD: sau khi API trả về). - Không bao giờ test state
emailbên trong component. Chỉ test cái user thấy (value trong input).