Wiele operacji w aplikacjach React działa asynchronicznie -- pobieranie danych z API, ładowanie zasobów, opóźnione aktualizację. To jak odbieranie sygnałów z głębokiego kosmosu -- odpowiedź nie przychodzi natychmiast, trzeba poczekać.
Funkcja
waitFor powtarza sprawdzenie asercji aż do momentu, gdy zakończy się sukcesem lub minie timeout:1import { render, screen, waitFor } from '@testing-library/react';
2
3test('loads mission data from API', async () => {
4 render(<MissionDetails missionId="apollo-13" />);
5
6 // Natychmiast widać spinner
7 expect(screen.getByText('Loading...')).toBeInTheDocument();
8
9 // Czekamy na dane
10 await waitFor(() => {
11 expect(screen.getByText('Apollo 13')).toBeInTheDocument();
12 });
13
14 // Spinner powinien zniknąć
15 expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
16});1await waitFor(
2 () => {
3 expect(screen.getByText('Data loaded')).toBeInTheDocument();
4 },
5 {
6 timeout: 3000, // Maksymalny czas oczekiwania (domyślnie 1000ms)
7 interval: 100, // Jak często sprawdzać (domyślnie 50ms)
8 }
9);Zapytania
findBy to skrót łączący waitFor z getBy:1// Te dwa zapisy są równoważne:
2
3// Sposób 1: waitFor + getBy
4await waitFor(() => {
5 expect(screen.getByText('Mission loaded')).toBeInTheDocument();
6});
7
8// Sposób 2: findBy (zalecany!)
9const element = await screen.findByText('Mission loaded');
10expect(element).toBeInTheDocument();1function CrewList({ shipId }) {
2 const [crew, setCrew] = useState([]);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 fetch(`/api/ships/${shipId}/crew`)
8 .then(res => res.json())
9 .then(data => {
10 setCrew(data);
11 setLoading(false);
12 })
13 .catch(err => {
14 setError(err.message);
15 setLoading(false);
16 });
17 }, [shipId]);
18
19 if (loading) return <p>Loading crew...</p>;
20 if (error) return <p>Error: {error}</p>;
21
22 return (
23 <ul>
24 {crew.map(member => (
25 <li key={member.id}>{member.name}</li>
26 ))}
27 </ul>
28 );
29}1// Mockujemy fetch
2beforeEach(() => {
3 global.fetch = jest.fn();
4});
5
6test('displays crew members after loading', async () => {
7 fetch.mockResolvedValueOnce({
8 json: () => Promise.resolve([
9 { id: 1, name: 'Captain Nova' },
10 { id: 2, name: 'Engineer Bolt' },
11 ])
12 });
13
14 render(<CrewList shipId="enterprise" />);
15
16 // Stan ładowania
17 expect(screen.getByText('Loading crew...')).toBeInTheDocument();
18
19 // Czekamy na dane
20 expect(await screen.findByText('Captain Nova')).toBeInTheDocument();
21 expect(screen.getByText('Engineer Bolt')).toBeInTheDocument();
22 expect(screen.queryByText('Loading crew...')).not.toBeInTheDocument();
23});
24
25test('displays error when fetch fails', async () => {
26 fetch.mockRejectedValueOnce(new Error('Network error'));
27
28 render(<CrewList shipId="enterprise" />);
29
30 expect(await screen.findByText('Error: Network error')).toBeInTheDocument();
31});Funkcja
act() informuje React, że wykonujemy operację, która aktualizuje stan i wymaga przerenderowania:1import { act } from 'react';
2
3test('updates timer display', () => {
4 jest.useFakeTimers();
5 render(<CountdownTimer seconds={10} />);
6
7 expect(screen.getByText('10')).toBeInTheDocument();
8
9 // Przyspieszamy czas o 3 sekundy
10 act(() => {
11 jest.advanceTimersByTime(3000);
12 });
13
14 expect(screen.getByText('7')).toBeInTheDocument();
15
16 jest.useRealTimers();
17});render i fireEvent w act()1test('debounced search filters results', async () => {
2 jest.useFakeTimers();
3 const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
4
5 render(<SearchBar onSearch={jest.fn()} debounceMs={300} />);
6
7 await user.type(screen.getByRole('textbox'), 'Mars');
8
9 // Przed upływem debounce -- brak wyników
10 expect(screen.queryByText('Searching...')).not.toBeInTheDocument();
11
12 // Po upływie debounce
13 act(() => {
14 jest.advanceTimersByTime(300);
15 });
16
17 await waitFor(() => {
18 expect(screen.getByText('Searching...')).toBeInTheDocument();
19 });
20
21 jest.useRealTimers();
22});Czekanie na zniknięcie elementu (np. spinnera):
1test('hides loading spinner after data loads', async () => {
2 render(<DataPanel />);
3
4 // Spinner jest widoczny
5 const spinner = screen.getByText('Loading...');
6
7 // Czekamy aż zniknie
8 await waitForElementToBeRemoved(spinner);
9
10 // Dane powinny być widoczne
11 expect(screen.getByText('Data ready')).toBeInTheDocument();
12});