9

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.

Fatih Doğmuş
  • 411
  • 1
  • 4
  • 10
  • 2
    This way you end up testing someone else's lib in your own tests while it may work improperly in test environment, be ready to mock it. It's unknown what's going on in your component so it's impossible to say exactly how it should be tested. See https://stackoverflow.com/help/how-to-ask and https://stackoverflow.com/help/mcve . Promises aren't 'timers' so they surely aren't flushed with runAllTimers. Try `flush-promises` lib to wait a bit more, in case you did everything else correctly this will be enough for a promise to take effect. Otherwise use RTL's `waitFor` for looser control flow. – Estus Flask Nov 17 '20 at 13:26

4 Answers4

19

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.

Fatih Doğmuş
  • 411
  • 1
  • 4
  • 10
10

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()
Tania Rascia
  • 1,563
  • 17
  • 35
-1

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: [...]
  });
});

Effanuel
  • 50
  • 1
  • 3
-1

Here, the solution was use getByText:

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