1

I'm testing an component who has 2 useEffect inside an component. Each fetch asynchronously some data.

  // Pour récupérer les types de Flux
  useEffect(() => {
    // AbortController permet d'avorter le fetch si le composant se unmount avant l'arrivé de la réponse
    const abortController = new AbortController();

    getData(abortController.signal, `${BASE_URL}/sesame-recuperer-type-flux`)
      .then((info) => {
        setTypeFlux(info.typeFlux);
      })
      .catch((error) => {
        if (error.name === "AbortError") return; // si la query a été avorté, ne faits rien
        throw error;
      });

    return () => {
      abortController.abort(); // arrete la requete en avortant grace au abortController sur l'unmount
    };
  }, []);

I always get this type of error:

 PASS  tests/Table.test.tsx
 FAIL  tests/Filtres.test.tsx (5.168 s)
  ● Console

    console.error
      Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
          at fn (D:\devgit\sesame\front-mfe-gestion-flux\src\components\Filtres.tsx:13:3)

      29 |     getData(abortController.signal, `${BASE_URL}/sesame-recuperer-organismes`)
      30 |       .then((info) => {
    > 31 |         setOrganismes(info.organismes);
         |         ^
      32 |       })
      33 |       .catch((error) => {
      34 |         if (error.name === "AbortError") return; // si la query a été avorté, ne faits rien

      at printWarning (../node_modules/react-dom/cjs/react-dom.development.js:67:30)
 PASS  tests/Table.test.tsx (12.061 s)
 PASS  tests/Filtres.test.tsx (13.435 s)
  ● Console

    console.error
      Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
          at fn (D:\devgit\sesame\front-mfe-gestion-flux\src\components\Filtres.tsx:13:3)

      29 |     getData(abortController.signal, `${BASE_URL}/sesame-recuperer-organismes`)
      30 |       .then((info) => {
    > 31 |         setOrganismes(info.organismes);
         |         ^
      32 |       })
      33 |       .catch((error) => {
      34 |         if (error.name === "AbortError") return; // si la query a été avorté, ne faits rien

      at printWarning (../node_modules/react-dom/cjs/react-dom.development.js:67:30)
      at error (../node_modules/react-dom/cjs/react-dom.development.js:43:5)
      at warnAboutUpdateOnUnmountedFiberInDEV (../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
      at scheduleUpdateOnFiber (../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
      at setOrganismes (../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
      at ../src/components/Filtres.tsx:31:9

I think it's something doing with act() and setState() because with AbortController I cancel the request if the component unmount.

I already tried to wrapped things with act() but it has no effect.

This is my test :

let mockedSetRsqlFiltres = jest.fn();

let mockedSetOpenModal = jest.fn();

let mockedCreerRSQL = jest.fn();


describe("affiche les éléments sans problème", () => {
    beforeEach(()=>{
        render(<Filtres formatColonnes={formatColonnes} setRsqlFiltres={mockedSetRsqlFiltres} setOpenModal={mockedSetOpenModal} creerRSQL={mockedCreerRSQL}/>);
    })
  it("contient un filtre au début", async () => {
    let filtres = await screen.findAllByTestId("filtre");
    expect(filtres.length).toBe(1);
  });

  it("ajoute un filtre quand on clique sur le bouton ajouter", async () => {
    let button = await screen.findByRole('button',{name: "Ajouter filtre"});
    fireEvent.click(button);
    let filtres = await screen.findAllByTestId("filtre");
    expect(filtres.length).toBe(2)
  });
});

How can I fix this ?

Thanks for your help !

Lfrlulu
  • 11
  • 2

1 Answers1

1

The issue is that getData gets resolved after the test is completed. So your test flow is like this:

render(<Filtres formatColonnes={formatColonnes} setRsqlFiltres={mockedSetRsqlFiltres} setOpenModal={mockedSetOpenModal} creerRSQL={mockedCreerRSQL}/>);

/// This initiates the getData

let filtres = await screen.findAllByTestId("filtre");
expect(filtres.length).toBe(1);

/// Component is dismounted

/// After that getData promise is resolved and  setOrganismes(info.organismes) is called which changes the component state
// But the component is not mounted any more. And so you are getting the error

I have some questions:

What is getData doing ? Can you mock it so that you resolve it to see the effect?

Some notes: You seem to have http call aborting for clean up, but the best practise is also not to update state if component is dismounted

useEfect(() => {
  let mounted = true;
  const abortController = new AbortController();

  getData().then(() => { if (mounted) setTheState() })

  return () => {
    mounted = false;
    abortController.abort();
  }
});

It seems like you are doing an integration or even e2e test. In order to test your component correct you have to mock all of its dependencies. It would be best if you use dependency injection for those. If you do not inject the dependencies all changes to any of the dependencies may lead to test failure of the UI component event if it is not its fault and its responsibility.

Svetoslav Petkov
  • 1,117
  • 5
  • 13