We use cookies to enhance your experience on the site
CodeWorlds

Testing Asynchronous Components -- Signals from Deep Space

Many operations in React applications work asynchronously -- fetching data from an API, loading resources, delayed updates. It's like receiving signals from deep space -- the response doesn't come immediately, you have to wait.

waitFor -- Waiting for the Signal

The

waitFor
function repeats an assertion check until it succeeds or the timeout expires:

1import { render, screen, waitFor } from '@testing-library/react';
2
3test('loads mission data from API', async () => {
4  render(<MissionDetails missionId="apollo-13" />);
5
6  // Spinner is visible immediately
7  expect(screen.getByText('Loading...')).toBeInTheDocument();
8
9  // Wait for data
10  await waitFor(() => {
11    expect(screen.getByText('Apollo 13')).toBeInTheDocument();
12  });
13
14  // Spinner should be gone
15  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
16});

waitFor Configuration

1await waitFor(
2  () => {
3    expect(screen.getByText('Data loaded')).toBeInTheDocument();
4  },
5  {
6    timeout: 3000,   // Maximum wait time (default 1000ms)
7    interval: 100,    // How often to check (default 50ms)
8  }
9);

findBy Queries -- Shortcut for waitFor

findBy
queries are a shortcut combining
waitFor
with
getBy
:

1// These two are equivalent:
2
3// Method 1: waitFor + getBy
4await waitFor(() => {
5  expect(screen.getByText('Mission loaded')).toBeInTheDocument();
6});
7
8// Method 2: findBy (recommended!)
9const element = await screen.findByText('Mission loaded');
10expect(element).toBeInTheDocument();

Example with a Data-Fetching Component

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}

Testing This Component

1// Mock 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  // Loading state
17  expect(screen.getByText('Loading crew...')).toBeInTheDocument();
18
19  // Wait for data
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() -- Synchronization with React

The

act()
function tells React that we're performing an operation that updates state and requires re-rendering:

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  // Fast-forward time by 3 seconds
10  act(() => {
11    jest.advanceTimersByTime(3000);
12  });
13
14  expect(screen.getByText('7')).toBeInTheDocument();
15
16  jest.useRealTimers();
17});

When is act() Needed?

  • React Testing Library automatically wraps
    render
    and
    fireEvent
    in
    act()
  • You need to use it manually when updating state outside RTL tests (e.g., fake timers, direct callback calls)

Testing Debounced/Throttled Operations

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  // Before debounce expires -- no results
10  expect(screen.queryByText('Searching...')).not.toBeInTheDocument();
11
12  // After debounce expires
13  act(() => {
14    jest.advanceTimersByTime(300);
15  });
16
17  await waitFor(() => {
18    expect(screen.getByText('Searching...')).toBeInTheDocument();
19  });
20
21  jest.useRealTimers();
22});

waitForElementToBeRemoved

Waiting for an element to disappear (e.g., a spinner):

1test('hides loading spinner after data loads', async () => {
2  render(<DataPanel />);
3
4  // Spinner is visible
5  const spinner = screen.getByText('Loading...');
6
7  // Wait for it to disappear
8  await waitForElementToBeRemoved(spinner);
9
10  // Data should be visible
11  expect(screen.getByText('Data ready')).toBeInTheDocument();
12});
Go to CodeWorlds