Как протестировать реагирующий модальный портал с библиотекой реагирующего тестирования?

В приложениях React обычно есть модальные окна или диалоги, которые появляются поверх основного содержимого. Однако тестирование этих модальных окон может оказаться сложной задачей, поскольку они обычно реализуются с помощью порталов, которые представляют собой элементы, отображаемые вне основной иерархии DOM. В этом блоге мы рассмотрим, как протестировать модальное окно, использующее порталы, с помощью библиотеки тестирования React.

Во-первых, давайте создадим простой модальный компонент, использующий порталы:

Modal.js

import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';

const Modal = ({ children, onClose }) => {
  const modalRoot = useRef(document.createElement('div'));

  useEffect(() => {
    const root = document.getElementById('modal-root');
    root.appendChild(modalRoot.current);

    return () => {
      root.removeChild(modalRoot.current);
    };
  }, []);

  return ReactDOM.createPortal(
    <div className="modal-overlay">
      <div className="modal">
        <button onClick={onClose}>Close</button>
        {children}
      </div>
    </div>,
    modalRoot.current
  );
};

export default Modal;

В этом компоненте мы создаем ссылку modalRoot, которая ссылается на новый элемент div. Мы используем хук useEffect, чтобы добавить этот элемент к элементу modal-root в основной иерархии DOM. Мы также удаляем элемент modalRoot, когда компонент размонтирован. Наконец, мы используем ReactDOM.createPortal для рендеринга модального содержимого внутри элемента modalRoot.

Теперь давайте напишем тест для этого компонента, используя библиотеку тестирования React:

Modal.test.js

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import Modal from './Modal';

describe('Modal', () => {
  it('renders modal content', () => {
    render(
      <Modal onClose={jest.fn()}>
        <h1>Modal Title</h1>
        <p>Modal Content</p>
      </Modal>
    );

    expect(screen.getByText('Modal Title')).toBeInTheDocument();
    expect(screen.getByText('Modal Content')).toBeInTheDocument();
  });

  it('calls onClose when close button is clicked', () => {
    const onClose = jest.fn();

    render(
      <Modal onClose={onClose}>
        <h1>Modal Title</h1>
        <p>Modal Content</p>
      </Modal>
    );

    fireEvent.click(screen.getByText('Close'));

    expect(onClose).toHaveBeenCalled();
  });
});

В первом тесте мы отображаем модальный компонент с некоторыми дочерними элементами и используем screen.getByText для проверки того, что дочерние элементы отображаются внутри модального окна. Во втором тесте мы визуализируем модальное окно с помощью фиктивной функции onClose и используем fireEvent.click для имитации нажатия кнопки закрытия. Затем мы проверяем, что функция onClose вызывается.

Обратите внимание, что нам не нужно беспокоиться о том, что модальное окно отображается вне основной иерархии DOM. Объект screen, предоставляемый библиотекой тестирования React, использует объект document, который включает в себя любые элементы, отображаемые через порталы.

Таким образом, тестировать модальные окна, использующие порталы, легко с помощью библиотеки тестирования React. Мы можем визуализировать модальный компонент так же, как и любой другой компонент, и использовать объект screen для взаимодействия с модальным и его содержимым.

Вот еще один пример повторно используемого модального компонента, использующего портал:

В этом примере компонент Portal определяется в собственном файле и экспортируется как компонент по умолчанию. Компонент Modal определяется в отдельном файле и импортирует компонент Portal.

Портал.js

import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';

const Portal = ({ children }) => {
  const portalNode = useRef(document.createElement('div'));

  useEffect(() => {
    const portalRoot = document.getElementById('portal-root');
    portalRoot.appendChild(portalNode.current);

    return () => {
      portalRoot.removeChild(portalNode.current);
    };
  }, []);

  return ReactDOM.createPortal(children, portalNode.current);
};

export default Portal;

Modal.js

import React from 'react';
import Portal from './Portal';

const Modal = ({ onClose, children }) => (
  <Portal>
    <div className="modal">
      <div className="modal-content">
        <button onClick={onClose}>Close</button>
        {children}
      </div>
    </div>
  </Portal>
);

export default Modal;

Тестирование компонента Portal может быть немного сложным, так как он в основном взаимодействует с DOM и не имеет никакого видимого вывода. Однако есть несколько вещей, которые мы можем проверить, чтобы убедиться, что компонент Portal работает правильно.

Один из подходов к тестированию компонента Portal заключается в использовании document.createElement для создания узла DOM, рендеринга компонента Portal с этим узлом в качестве его дочернего элемента, а затем создания утверждений о визуализированных выходных данных. Вот пример теста для компонента Portal:

Портал.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import Portal from './Portal';

describe('Portal', () => {
  it('renders children in a portal', () => {
    const container = document.createElement('div');
    const child = <h1>Portal Content</h1>;

    ReactDOM.render(<Portal>{child}</Portal>, container);

    expect(container.firstChild).toMatchSnapshot();
  });
});

В этом тесте мы создаем элемент div, используя document.createElement, и передаем его как дочерний элемент компоненту Portal. Затем мы используем ReactDOM.render для рендеринга компонента Portal и его дочерних элементов в контейнере.

Наконец, мы делаем утверждение, используя сопоставитель Jest toMatchSnapshot, который делает снимок отрендеренного вывода и сравнивает его с сохраненным снимком. Это позволяет нам легко увидеть, соответствует ли визуализированный вывод нашим ожиданиям.

Имейте в виду, что тестирование компонента Portal в первую очередь касается проверки того, что он правильно взаимодействует с DOM и отображает свои дочерние элементы в правильном месте. Вы также можете протестировать пограничные случаи, такие как рендеринг компонента Portal без каких-либо дочерних элементов или его рендеринг с несколькими дочерними элементами.

Modal.test.js

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import Modal from './Modal';

describe('Modal', () => {
  it('renders modal content', () => {
    render(
      <Modal onClose={jest.fn()}>
        <h1>Modal Title</h1>
        <p>Modal Content</p>
      </Modal>
    );

    expect(screen.getByText('Modal Title')).toBeInTheDocument();
    expect(screen.getByText('Modal Content')).toBeInTheDocument();
  });

  it('calls onClose when close button is clicked', () => {
    const onClose = jest.fn();

    render(
      <Modal onClose={onClose}>
        <h1>Modal Title</h1>
        <p>Modal Content</p>
      </Modal>
    );

    fireEvent.click(screen.getByText('Close'));

    expect(onClose).toHaveBeenCalled();
  });
});

Этот тест очень похож на предыдущие примеры, но теперь мы импортируем компонент Modal из отдельного файла и тестируем его как составной компонент, который использует компонент Portal для рендеринга своего содержимого.

В целом, разделение компонентов Portal и Modal на отдельные файлы может сделать ваш код более модульным и простым в обслуживании.

Заключение

Использование портала для рендеринга модального содержимого — отличный способ отделить модальное содержимое от основной иерархии DOM и улучшить доступность вашего приложения. Библиотека тестирования React предоставляет отличные инструменты для тестирования компонентов, использующих порталы, и вы даже можете повторно использовать компонент Portal для других компонентов.

Если у вас есть дополнительные вопросы, вам нужны дополнительные рекомендации или вы хотите связаться со мной, не стесняйтесь обращаться ко мне в моем профиле LinkedIn. Буду рад помочь вам чем смогу!