🎃
Chakra UIのモーダルのスナップショットを取る方法
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 タグになっているのがわかります。
解決方法
色々調べた結果、下記の記事を参考にしました。
こちらはSemantic-UI-React
に関する記事ですが、
何をしているかというとjest.mock
を使ってModal
コンポーネントをラップしているPortal
コンポーネントを、Modal
以下をレンダリングするコンポーネントとしてモックしています。
Chakra UI
のModal
の実装を見ていると同様にPortal
でラップしていることがわかります。
Portal
については下記に説明が載っていますが、任意のコンポーネントや要素をdocument.body
の末尾に移動し、そこに React ツリーをレンダリングするために使用されるものです。
popovers や dropdowns、modals のためのものと書いてあります。
Portal
の実装も一応見てみると、Portal
がレンダリングされて span タグのスナップショットが返ってきたことがわかります。
なので、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