問題
Chakra V3を使用したReactコンポーネントに対して表示テストを実行したところ、以下エラーが発生しました
TypeError: win.ResizeObserver is not a constructor
テスト対象コンポーネントは長いため省略します。ただの新規登録フォームを実装したReactコンポーネントです。
テスト対象コンポーネント
import { insertUser } from "@/lib/supabase/supabaseFunction";
import {
Button,
Card,
Card as ChakraCard,
Field,
Heading,
Input,
Portal,
Select,
Stack,
Textarea,
createListCollection,
} from "@chakra-ui/react";
import { memo, useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { useNavigate } from "react-router";
interface RegisterProps {
englishWord: string;
userName: string;
description: string;
skill: number[];
githubId: string;
qiitaId: string;
xId: string;
}
const frameworks = createListCollection({
items: [
{ label: "React", value: 0 },
{ label: "TypeScript", value: 1 },
{ label: "Github", value: 2 },
],
});
const Register = () => {
const navigate = useNavigate();
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<RegisterProps>();
const onSubmit = useCallback(
handleSubmit(async (formData) => {
try {
await insertUser(formData);
navigate("/");
} catch (error) {}
}),
[],
);
return (
<>
<Heading>新規名刺登録</Heading>
<ChakraCard.Root w="450px" maxW="sm" shadow={"md"}>
<ChakraCard.Body>
<form onSubmit={onSubmit} id="register">
<Stack gap="8">
<Field.Root invalid={!!errors.englishWord}>
<Field.Label>好きな英単語</Field.Label>
<Input
id="englishWord"
placeholder="coffee"
{...register("englishWord")}
/>
<Field.ErrorText>
{errors.englishWord && errors.englishWord.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.userName}>
<Field.Label>お名前</Field.Label>
<Input id="userName" {...register("userName")} />
<Field.ErrorText>
{errors.userName && errors.userName.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.description}>
<Field.Label>自己紹介</Field.Label>
<Textarea
id="description"
variant="outline"
autoresize
placeholder="<h1>HTMLタグも使えます</h1>"
{...register("description")}
/>
<Field.ErrorText>
{errors.description && errors.description.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.skill}>
<Field.Label>好きな技術</Field.Label>
<Controller
control={control}
name="skill"
render={({ field, formState }) => (
<Select.Root
onValueChange={({ value }) => field.onChange(value)}
onInteractOutside={() => field.onBlur()}
variant={"outline"}
collection={frameworks}
invalid={!!formState.errors.skill}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select option" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Content>
{frameworks.items.map((framework) => (
<Select.Item
item={framework}
key={framework.value}
>
{framework.label}
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
)}
/>
<Field.ErrorText>
{errors.skill && errors.skill.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.githubId}>
<Field.Label>GitHub ID</Field.Label>
<Input id="githubId" {...register("githubId")} />
<Field.ErrorText>
{errors.githubId && errors.githubId.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.qiitaId}>
<Field.Label>Qiita ID</Field.Label>
<Input id="qiitaId" {...register("qiitaId")} />
<Field.ErrorText>
{errors.qiitaId && errors.qiitaId.message}
</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.xId}>
<Field.Label>X ID</Field.Label>
<Input id="xId" {...register("xId")} placeholder="@は不要" />
<Field.ErrorText>
{errors.xId && errors.xId.message}
</Field.ErrorText>
</Field.Root>
</Stack>
</form>
</ChakraCard.Body>
<Card.Footer justifyContent="flex-end">
<Button
type="submit"
form="register"
variant="solid"
loading={isSubmitting}
>
登録
</Button>
</Card.Footer>
</ChakraCard.Root>
</>
);
};
export default memo(Register);
続いてテストファイルです
import { describe, vi, test, expect, beforeEach } from "vitest";
import { act, render, screen, waitFor, cleanup } from "@testing-library/react";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import { User } from "@/domain/User";
import Register from "@/pages/cards/Register";
import { useNavigate } from "react-router";
vi.mock("react-router", () => {
const actual = vi.importActual("react-router");
return {
...actual,
useNavigate: vi.fn(),
};
});
describe("名刺カード新規登録テスト", () => {
beforeEach(async () => {
act(() => {
render(
<ChakraProvider value={defaultSystem}>
<Register />
</ChakraProvider>,
);
});
});
test("タイトルが表示されていること", async () => {
const sut = await screen.findByText("新規名刺登録");
expect(sut).toBeInTheDocument();
});
});
/// <reference types="vitest/config" />
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), viteTsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./vitest.setup.ts",
},
});
import "@testing-library/jest-dom/vitest";
解決方法
以下を追記すればエラーは出なくなります
import "@testing-library/jest-dom/vitest";
+ globalThis.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ };
エラー解決の理由
TypeError: win.ResizeObserver is not a constructorエラーは、テスト実行環境にwin.ResizeObserverが存在しないため発生します。
win.ResizeObserverとはブラウザ上でDOM要素の高さや幅の変更を監視するためのブラウザAPIです。
今回のケースではChakra V3が利用していました。
今回vitestの実行環境(テスト環境)はjsdomを指定しており、その設定はvite.config.tsで行っています。
/// <reference types="vitest/config" />
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), viteTsconfigPaths()],
test: {
globals: true,
environment: "jsdom", // ←ここ!
setupFiles: "./vitest.setup.ts",
},
});
jsdomとは?
Node.js環境でDOM操作やブラウザのイベントシステムなどを利用できるように、ブラウザ環境をエミュレーション(疑似的に構築)してくれるJavascriptのライブラリです。
といってもすべてのブラウザAPIをエミューレーションしてくれるわけではなく、今回エラーとなったResizeObserverAPIもエミュレーションの対象外です。
つまり、jsdom環境でResizeObserverAPIを使おうとしたが、存在しなかったのでエラーになったということです。
そこで、vitest.setup.tsにResizeObserverのモックを定義することでエラーを解消したということです。
ResizeObserverをモックにしたことでレスポンシブのテストなどはうまくいかなくなる可能性があります。今回私が実施したいテストはそのあたりが無いため問題ありませんが、この点はご留意ください。
おわりに
こういったブラウザAPIをnode.js環境で使う場合、Nodeやjsdomのバージョン、他にも便利パッケージの登場など、考慮しないといけないことが多いのでキャッチアップは気をつけねば。
参考
https://github.com/jsdom/jsdom
https://drafts.csswg.org/resize-observer/#resizeobserver
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼
https://projisou.jp