はじめに
「新しいタブでURLを開く」ボタンをテストしようとしたら、window.open が jsdom では動かなくて詰まりました。
jsdom(Vitestのデフォルト環境)はブラウザAPIを完全に実装しているわけではないので、window.open をそのまま呼ぼうとすると not implemented エラーになることがあります。
そこで vi.spyOn を使ってモックする方法を整理しました。
完成コード
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import { ChakraProvider } from "@chakra-ui/react";
import { UserCard } from "./UserCard";
import type { User } from "../../../shared/types/user";
describe("UserCard", () => {
const user: User = {
id: "testUser1",
name: "テストユーザー1",
description: "テストユーザーの自己紹介",
skills: [
{ id: 1, name: "React" },
{ id: 2, name: "TypeScript" },
],
githubId: "testUser1",
qiitaId: "testUser1",
xId: "testUser1",
createdAt: new Date().toISOString(),
};
beforeEach(() => {
render(
<ChakraProvider>
<UserCard user={user} />
</ChakraProvider>
);
});
it("GitHubアイコンをクリックすると正しいURLが新しいタブで開かれる", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const button = screen.getByRole("button", { name: "GitHub Url" });
await userEvent.click(button);
expect(openSpy).toHaveBeenCalledWith(
"https://github.com/testUser1",
"_blank"
);
openSpy.mockRestore();
});
});
ポイント解説
vi.spyOn(window, "open").mockImplementation(() => null)
vi.spyOn で window.open をスパイに差し替えます。.mockImplementation(() => null) を追加することで、実際の処理(新しいタブを開く)を無効化しつつ、「どんな引数で呼ばれたか」だけを記録できます。
null を返しているのは、window.open の戻り値の型が WindowProxy | null だからです。型を合わせておくとTypeScriptに怒られません。
screen.getByRole("button", { name: "GitHub Url" })
aria-label 属性の値がボタンのアクセシブル名になります。name オプションにはコンポーネントの aria-label と完全に一致する文字列を指定する必要があります。
"GitHub Url" と "GitHub URL" は別物として扱われます。テストが TestingLibraryElementError: Unable to find an accessible element で落ちたら、まずコンポーネントの aria-label の文字列を確認してみてください。
openSpy.mockRestore() でモックを元に戻す
テスト終了後にモックを元に戻しておかないと、後続のテストに影響が出ます。テストの最後に呼ぶか、afterEach にまとめておくと安全です。
afterEach(() => {
openSpy.mockRestore();
});
まとめ
- jsdomでは
window.openが動かないのでvi.spyOnでモックする -
.mockImplementation(() => null)で実処理を無効化しつつ呼び出しを記録できる -
getByRoleのnameオプションはaria-labelと完全一致が必要 - テスト後は
mockRestore()でモックを解除する