Search code examples
reactjsreact-testing-library

React test library does not work with Modal Portal


I have written a component for Modal Window, put it in createPortal. I wanted to write a test for that, but a got an error: console.error Error: Uncaught [Error: Target container is not a DOM element.] For some reasons test did not see my component. I do not have any idea why it work this way. If somebody knows, please write, thank you in advance! My test looks so:

test('modal', () => {
  const handleClose = jest.fn();

  const {getByText} = render(<Modal onClose={handleClose} isOpen title={'title'} />);
  expect(getByText('title')).toBeTruthy();
});

My component looks so:

interface IModalProps {
  isOpen: boolean;
  title?: string;
  className?: string;
  onClose: () => void;
  content?: React.ReactChildren | React.ReactChild
}

const Modal = ({isOpen, title, content, onClose, className}: IModalProps) => {

  const keydownHandler = ({keyCode}:KeyboardEvent) => {
    if (keyCode === 27) {
      onClose();
    }
  };

  React.useEffect(() => {
    document.addEventListener('keydown', keydownHandler);
    return () => document.removeEventListener('keydown', keydownHandler);
  });

  const modalInPortal = () => (
    <div
      className={classNames(
        'modal-overlay',
        isOpen && 'active')}
      onClick={onClose}
    >
      <div
        className={classNames(
          className,
          'modal' )}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="modal-block-content">
          <span
            className="close-btn"
            onClick={onClose}
          >
        &times;
          </span>
          <h2 className="modal-title">{title}</h2>
          {content && <div className="modal-body">{content}</div>}
          <div className="modal-footer">
            <Button
              color={'light'}
              style={{color:'blue'}}
              size="lg"
              onClick={onClose}
            >
              Cancel
            </Button>
            <Button size="lg">Save</Button>
          </div>
        </div>
      </div>
    </div>
  );

  return createPortal( modalInPortal(), document.getElementById('portal'));
};


Solution

  • Most likely, in production, you have a HTML element in your index.html that has an id of portal.

    Jest doesn't have that same HTML, so when it tries to portal, it can't find it and blows up.

    Usually to fix this, you need to add a setupFilesAfterEnv to your jest config that points to a new file that builds up that HTML outside of React's remit.

    Inside this file, you'd have:

    const portalEl = document.createElement('div')
    portalEl.setAttribute('id', 'portal')
    document.body.appendChild(portalEl)
    

    Now the test environment is like your real one for all tests (this file is globally applied).

    When asserting on the contents you will also probably need to use screen since the actual modal is loaded outside the container.