🎃

Chakra UIのモーダルのスナップショットを取る方法

2022/01/26に公開

Chakra UI のモーダルのスナップショットがうまく取れない

Jest でスナップショットテストを書いていたら、Chakra UI のモーダル内のコンテンツをうまく取ることができませんでした。

example

import renderer from "react-test-renderer";
import * as React from "react";

import {
  Button,
  Modal,
  ModalBody,
  ModalCloseButton,
  ModalContent,
  ModalFooter,
  ModalHeader,
  ModalOverlay,
  useDisclosure,
} from "@chakra-ui/react";

const ChakraUiModal: React.VFC = () => {
  const { onClose } = useDisclosure();

  return (
    <>
      <Modal isOpen={true} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader>Modal Title</ModalHeader>
          <ModalCloseButton />
          <ModalBody>Content</ModalBody>

          <ModalFooter>
            <Button onClick={onClose}>Close</Button>
            <Button>Secondary Action</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </>
  );
};

it("renders correctly", () => {
  const tree = renderer.create(<ChakraUiModal />).toJSON();
  expect(tree).toMatchSnapshot();
});

例えば上記の通りスナップショットを取ろうとすると結果は下記の形になります。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `<span />`;

モーダル内のスナップショットが取れず span タグになっているのがわかります。

解決方法

色々調べた結果、下記の記事を参考にしました。

https://stackoverflow.com/questions/48373732/jest-snapshots-not-working-with-some-semantic-ui-react-components/49290892#49290892

こちらはSemantic-UI-Reactに関する記事ですが、
何をしているかというとjest.mockを使ってModalコンポーネントをラップしているPortalコンポーネントを、Modal以下をレンダリングするコンポーネントとしてモックしています。

Chakra UIModalの実装を見ていると同様にPortalでラップしていることがわかります。
https://github.com/chakra-ui/chakra-ui/blob/main/packages/modal/src/modal.tsx#L179

Portalについては下記に説明が載っていますが、任意のコンポーネントや要素をdocument.bodyの末尾に移動し、そこに React ツリーをレンダリングするために使用されるものです。
popovers や dropdowns、modals のためのものと書いてあります。
https://chakra-ui.com/docs/components/portal

Portalの実装も一応見てみると、Portalがレンダリングされて span タグのスナップショットが返ってきたことがわかります。
https://github.com/chakra-ui/chakra-ui/blob/main/packages/portal/src/portal.tsx#L85

なので、Chakra UIでもPortalコンポーネントを、Modal以下をレンダリングするコンポーネントとしてモックする方法が使うことが可能です。

下記のコードを追加すると、

import { ReactNode } from "react";

type Props = {
  children: ReactNode;
};

jest.mock("@chakra-ui/portal", () => ({
  Portal: jest.fn(({ children }: Props) => children),
}));

結果が下記のようになり、スナップショットが無事取れました。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
Array [
  <div
    className="chakra-modal__overlay css-13d1tkw"
    style={
      Object {
        "opacity": 0,
      }
    }
  />,
  <div
    data-focus-guard={true}
    style={
      Object {
        "height": "0px",
        "left": "1px",
        "overflow": "hidden",
        "padding": 0,
        "position": "fixed",
        "top": "1px",
        "width": "1px",
      }
    }
    tabIndex={0}
  />,
  <div
    data-focus-guard={true}
    style={
      Object {
        "height": "0px",
        "left": "1px",
        "overflow": "hidden",
        "padding": 0,
        "position": "fixed",
        "top": "1px",
        "width": "1px",
      }
    }
    tabIndex={1}
  />,
  <div
    data-focus-lock-disabled={false}
    onBlur={[Function]}
    onFocus={[Function]}
  >
    <div
      className="chakra-modal__content-container css-k2m3mz"
      onClick={[Function]}
      onKeyDown={[Function]}
      onMouseDown={[Function]}
      onScrollCapture={[Function]}
      onTouchMoveCapture={[Function]}
      onWheelCapture={[Function]}
      tabIndex={-1}
    >
      <section
        aria-describedby="chakra-modal--body-1715723249-2"
        aria-labelledby="chakra-modal--header-1715723249-2"
        aria-modal={true}
        className="chakra-modal__content css-6yiygt"
        id="chakra-modal-1715723249-2"
        onClick={[Function]}
        role="dialog"
        style={
          Object {
            "opacity": 0,
            "transform": "scale(0.95) translateZ(0)",
          }
        }
        tabIndex={-1}
      >
        <header
          className="chakra-modal__header css-72fd9l"
          id="chakra-modal--header-1715723249-2"
        >
          Modal Title
        </header>
        <button
          aria-label="Close"
          className="chakra-modal__close-btn css-6sdaxy"
          onClick={[Function]}
          type="button"
        >
          <svg
            aria-hidden={true}
            className="chakra-icon css-onkibi"
            focusable="false"
            viewBox="0 0 24 24"
          >
            <path
              d="M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z"
              fill="currentColor"
            />
          </svg>
        </button>
        <div
          className="chakra-modal__body css-0"
          id="chakra-modal--body-1715723249-2"
        >
          Content
        </div>
        <footer
          className="chakra-modal__footer css-192qrng"
        >
          <button
            className="chakra-button css-5c2176"
            onClick={[Function]}
            type="button"
          >
            Close
          </button>
          <button
            className="chakra-button css-5c2176"
            type="button"
          >
            Secondary Action
          </button>
        </footer>
      </section>
    </div>
  </div>,
  <div
    data-focus-guard={true}
    style={
      Object {
        "height": "0px",
        "left": "1px",
        "overflow": "hidden",
        "padding": 0,
        "position": "fixed",
        "top": "1px",
        "width": "1px",
      }
    }
    tabIndex={0}
  />,
]
`;

Discussion