How to test react-toastify with jest and react-testing-library

Viewed 9750

I have a screen with some form, and on submission, I send the request to back-end with axios. After successfully receiving the response, I show a toast with react-toastify. Pretty straight forward screen. However, when I try to test this behavior with an integration test using jest and react testing library, I can't seem to make the toast appear on DOM.

I have a utility renderer like that to render the component that I'm testing with toast container:

import {render} from "@testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";

export const renderWithToastify = (component) => (
  render(
    <div>
      {component}
      <ToastContainer/>
    </div>
  )
);

In the test itself, I fill the form with react-testing-library, pressing the submit button, and waiting for the toast to show up. I'm using mock service worker to mock the response. I confirmed that the response is returned OK, but for some reason, the toast refuses to show up. My current test is as follows:

expect(await screen.findByRole("alert")).toBeInTheDocument();

I'm looking for an element with role alert. But this seems to be not working. Also, I tried doing something like this:

...

beforeAll(() => {
  jest.useFakeTimers();
}

...

it("test", () => {
  ...

  act(() =>
    jest.runAllTimers();
  )
  expect(await screen.findByRole("alert")).toBeInTheDocument();
}

I'm kind of new to JS, and the problem is probably due to asynch nature of both axios and react-toastify, but I don't know how to test this behavior. I tried a lot of things, including mocking timers and running them, mocking timers and advancing them, not mocking them and waiting etc. I even tried to mock the call to toast, but I couldn't get it working properly. Plus this seems like an implementation detail, so I don't think I should be mocking that.

I think the problem is I show the toast after the axios promise is resolved, so timers gets confused somehow.

I tried to search many places, but failed to find an answer.

Thanks in advance.

4 Answers

Thank you @Estus Flask, but the problem was much much more stupid :) I had to render ToastContainer before my component, like this:

import {render} from "@testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";

export const renderWithToastify = (component) => {
  return (
    render(
      <div>
        <ToastContainer/>
        {component}
      </div>
    )
  );
};

Then, the test was very simple, I just had to await on the title of the toast:

expect(await screen.findByText("alert text")).toBeInTheDocument();

The findByRole doesn't seem to work for some reason, but I'm too tired to dig deeper :) I didn't have to use any fake timers or flush the promises. Apperently, RTL already does those when you use await and finBy* queries, only the order of rendering was wrong.

In order to use a mock when you don't have access to the DOM (like a Redux side effect) you can do:

import { toast } from 'react-toastify'

jest.mock('react-toastify', () => ({
  toast: {
    success: jest.fn(),
  },
}))
expect(toast.success).toHaveBeenCalled()

What I would do is mock the method from react-toastify to spy on that method to see what is gets called it, but not the actual component appearing on screen:

// setupTests.js
jest.mock('react-toastify', () => {
  const actual = jest.requireActual('react-toastify');
  Object.assign(actual, {toast: jest.fn()});
  return actual;
});

and then in the actual test:

// test.spec.js
import {toast} from 'react-toastify';

const toastCalls = []
const spy = toast.mockImplementation((...args) => {
     toastCalls.push(args)
  }
)

describe('...', () => {
  it('should ...', () => {
    // do something that calls the toast
    ...
    // then
    expect(toastCalls).toEqual(...)
   }
 }
)


Another recommendation would be to put this mockImplementation into a separate helper function which you can easily call for the tests you need it for. This is a bear bones approach:


function startMonitoring() {
  const monitor = {toast: [], log: [], api: [], navigation: []};

  toast.mockImplementation((...args) => {
    monitor.toast.push(args);
  });
  log.mockImplementation((...args) => {
    monitor.log.push(args);
  });
  api.mockImplementation((...args) => {
    monitor.api.push(args);
  });
  navigation.mockImplementation((...args) => {
    monitor.navigation.push(args);
  });
  return () => monitor;
}

it('should...', () => {
  const getSpyCalls = startMonitoring();
  // do something

  expect(getSpyCalls()).toEqual({
    toast: [...],
    log: [...],
    api: [...],
    navigation: [...]
  });
});

Here, the solution was use getByText:

await waitFor(() => {
  expect(screen.getByText(/Logged!/i)).toBeTruthy()
})
Related