React-testing-library - test if an html element does not exist

Viewed 389

This is my live code

In my current code I have a div with id #___portal

<div id="___portal"></div>

I have a component where I create a portal adding the content to the #___portal (in my real code, this div will always be created)

import React, { useState, useEffect } from "react";
import { createPortal } from "react-dom";

const App = ({ children }) => {
  const [portalEl, setPortalEl] = useState();

  useEffect(() => {
    //in my real code #___portal exists
    setPortalEl(document.getElementById("___portal"));
    console.log(portalEl);
  }, [portalEl]);

  if (!portalEl) return null;

  return createPortal(
    <>
      <div id="modal" role="dialog">
        {children}
      </div>
    </>,
    portalEl
  );
};

export default App;

I am trying to do a test where I want to test that a text is contained in the component. My test fails and I suspect that it is because the content of my component is not being added to the #___portal and that is why my component is not rendering, how can I do to simulate this in my test?

import App from "./App";
import React from "react";
import { render } from "react-testing-library";
describe("<App />", () => {
  it("Should render text", async () => {
    let testId = "___portal";
    let newDiv = document.createElement("div");
    newDiv.setAttribute("id", testId);
    document.body.appendChild(newDiv);

    const text = "hello world";
    const { getByText } = render(
      <App>
        <div>{text}</div>
      </App>
    );
    expect(getByText(text)).toBeInTheDocument();
  });
});

//output of test
<body>
 <div
  id="___portal"
  />
 <div />
</body>
2 Answers

When testing react, ANYTHING that happens as the result of a useEffect needs to use the async API. Usually, this is as simple as using the findBy* methods instead of the getBy* methods. These return a Promise, so be sure to await it:

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

Every time your component is rendered for first time portalEl is undefined and component returns null. Since you are getting portal root element in useEffect you can use async/await helper methods that work with queries to wait for changes in the DOM. But, according to your implementation I think that what you need to do is a small refactor to App component:

You don't need to wait for useEffect to get portalEl because you know it exists beforehand. This way you save an extra render by not calling setPortalEl

/** checking for object window in case this code runs on server side */
const portalEl = (typeof window !== 'undefined') ? document.getElementById("___portal"): null;

const App = ({ children }) => {

  if (!portalEl) return null;

  return createPortal(
    <>
      <div id="modal" role="dialog">
        {children}
      </div>
    </>,
    portalEl
  );
};

or assign its value directly:

  const [portalEl] = useState(document.getElementById("___portal"));
Related