Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Testowanie Komponentów Asynchronicznych -- Sygnały z Głębokiego Kosmosu

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ć.

waitFor -- Czekanie na sygnał

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});

Konfiguracja waitFor

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);

findBy queries -- Skrót do waitFor

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();

Przykład z komponentem fetchującym dane

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}

Testowanie tego komponentu

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});

act() -- Synchronizacja z Reactem

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});

Kiedy potrzebny jest act()?

  • React Testing Library automatycznie opakowuje
    render
    i
    fireEvent
    w
    act()
  • Musisz użyć go ręcznie, gdy aktualizujesz stan poza testami RTL (np. fake timers, bezpośrednie wywołanie callbacków)

Testowanie debounced/throttled operacji

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});

waitForElementToBeRemoved

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});
Przejdź do CodeWorlds