0

I'm trying to test that a component that gets its data from an API loads as expected using react-router-dom. It seems like the test needs to wait for the component's Promise to resolve, but I can't seem to find an approach that works.

Here's the really simple App I'm testing.

class App extends React.Component {
  render() {
    return (
      <div className="container">
        <Switch >
          <Route path='/object/:id'>
            <ObjectDetail />
          </Route>
          <Route path='/'>
            <Table />
          </Route>
        </Switch>
      </div>
    )
  }
}

export default App

And here's the objectDetail component (which uses a service to handle the API call):

class ObjectDetail extends React.Component {
  state = {
    data: {},
    dataResolved: false,
  }

  componentDidMount() {
    const id  = this.props.match.params.id;

    objectService.getObjectDetail(id).then((result) => {
      this.setState({
        data: result,
        dataResolved: true,
      });
    });

    document.title = 'Object Detail';
  }

  render() {
    const { data, dataResolved } = this.state;

    if(dataResolved) {
      return (
        <div>
          <h1>
            Object Detail
          </h1>
        
        // ... etc.
      );
    } else {
      return null;
    }
  }
}

export default withRouter(ObjectDetail)

The Table component renders a table with objects along with a details link, and the ObjectDetail component renders a heading, so I'm trying to test that the link navigation works as expected, like so:

test('navigates to details when you click the details link', async () => {
  render(
    <MemoryRouter>
      <App />
    </MemoryRouter>
  );
  await wait();

  const rows = within(screen.getByRole('rowgroup', {name: 'Table Body'})).getAllByRole('row');

  const link = within(rows[0]).getByRole('link')

  fireEvent.click(link)

  expect(screen.getByRole('heading', {name: 'Object Detail'})).toBeInTheDocument();
});

Everything works as I'd expect when I run the app and test manually as well as using Cypress for integration testing, but the test fails because it only renders this (falling back to else { return null; }):

<body>
  <div>
    <div
      class="container"
    />
  </div>
</body>

I've tried adding another await wait(); between the .click() and expect with the same result, and I tried what was proposed in this answer to a similar question without success.

I've tried some other approaches that seem to force the test to wait naively (like using setTimeout) and await waitForElement(() => screen.getByRole('heading')). Interestingly enough, those cause the test to error out with a Error: Request failed with status code 404, which is even more baffling.

How can I write a test that will render the routed component correctly?

Derek
  • 827
  • 11
  • 23
  • That you get an error only with waitForElement means that you didn't wait long enough for async action to be complete. The error says that the request fails and likely refers to waitForElement. Real requests shouldn't be performed in tests. You can start with mocking it. – Estus Flask Aug 07 '20 at 21:54
  • Mocking it is a good suggestion, thanks. I'd still be interested in a solution if there is one if only for my own education – Derek Aug 07 '20 at 21:59

1 Answers1

0

What worked for me is wrapping it like this (although it looks awful):

await act(() =>
  Promise.resolve(render(
    <MemoryRouter>
      <App />
    </MemoryRouter>
  ));
);
mxk
  • 1
  • 1