1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vitest】【Chakra V3】TypeError: win.ResizeObserver is not a constructorエラーの解消方法

Last updated at Posted at 2026-01-23

問題

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();
  });
});
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",
  },
});
vitest.setup.ts
import "@testing-library/jest-dom/vitest";

解決方法

以下を追記すればエラーは出なくなります

vitest.setup.ts
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で行っています。

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?