© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjstesting

Zustand Test を Playwright で E2E テストする

フォーム入力 → リアルタイム同期 → 保存 → リセットまでの一連の流れを Playwright で再現し、Store Monitor の表示と DOM の両方で検証する手順を解説する。

難易度: 中級·更新: 2026-05-07·
zustandplaywright

対応バージョン

nextjs 15react 19zustand 5playwright 1

前提環境

Playwright の test / expect / locator の基本を理解していること

概要

Zustand Test は 入力 → 同期 → 保存 → リセット が画面の中で連鎖します。ユニットテスト (storeSync テスト編) でロジックは個別に押さえられても、「ブラウザ上で実際にこの流れが成立しているか」は別の関心事です。

Playwright で押さえるのは次の 3 点。

  • 入力 → Monitor の表示: watch 経由で settings に値が入り、Monitor の <pre> に出る
  • 保存 → 確定値: Zod を通った値が settings の確定領域に書かれる
  • リセット → 初期値: フォーム / 各 store / Monitor が初期表示に戻る

インストール

npm install -D @playwright/test
npx playwright install

window 経由で store にアクセスできるようにする

Playwright のテストから store の中身を直接 assert するには、開発時だけ window に store を expose しておくのが手早いです。本番ビルドには出さないよう環境変数でガードします。

// src/components/SyncBoot.tsx
"use client";
import { useEffect } from "react";
import { useSettingsStore } from "@/store/settingsStore";

export function SyncBoot() {
  useEffect(() => {
    if (process.env.NEXT_PUBLIC_E2E === "1") {
      // テスト用に store を expose
      (window as unknown as { __settings: typeof useSettingsStore }).__settings = useSettingsStore;
    }
  }, []);
  return null;
}

NEXT_PUBLIC_E2E=1 npm run dev でテスト実行用に立ち上げる、という使い方です。

基本のスペック構造

tests/zustand-demo.spec.ts に 1 ファイルでシナリオをまとめます。beforeEach で開き直すと、毎テスト初期状態で始まります。

// tests/zustand-demo.spec.ts
import { test, expect } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.goto("/zustand-demo");
  // 全ストアをリセット
  await page.getByRole("button", { name: "全ストアをリセット" }).click();
});

test.describe("Zustand Test", () => {
  /* ...各 test がここに並ぶ */
});

page.goto の URL は playwright.config.ts の baseURL を設定すれば相対で書けます。Playground は /zustand-demo を想定。

入力 → Monitor の同期を確認する

getByRole / getByLabel で要素を引き、fill で入力します。Monitor 側の <pre> に書き込まれた JSON を toContainText で検証します。

test("名前を入力すると settingsStore Monitor に反映される", async ({ page }) => {
  await page.getByLabel("名前").fill("alice");

  const monitor = page.getByRole("region", { name: /settingsStore/i });
  await expect(monitor).toContainText('"name": "alice"');
});

Monitor を <section aria-label="settingsStore"> のように アクセシブルな名前 で書いておくと、getByRole("region", { name: "..." }) で安定して引けます。

window 経由で store の値を assert する

DOM を経由せず store を直接見たい場合は、expose しておいた __settings を使います。

test("保存ボタンで Zod を通った値が確定する", async ({ page }) => {
  await page.getByLabel("名前").fill("alice");
  await page.getByLabel("メール").fill("alice@example.com");
  await page.getByLabel("レート").fill("80");

  await page.getByRole("button", { name: "保存" }).click();

  const snapshot = await page.evaluate(() => {
    const s = (window as any).__settings.getState();
    return { name: s.name, email: s.email, rate: s.rate };
  });
  expect(snapshot).toEqual({ name: "alice", email: "alice@example.com", rate: 80 });
});

page.evaluate の中はブラウザ実行コンテキストなので、useSettingsStore を直接 import せずに window.__settings 経由で参照します。

エラー UI のテスト

role="alert" が付いていれば getByRole("alert") で引けます。Zod の検証に引っかかる値を入れて、メッセージが現れることを確認します。

test("メールが不正だとエラーメッセージが表示される", async ({ page }) => {
  await page.getByLabel("名前").fill("alice");
  await page.getByLabel("メール").fill("not-an-email");
  await page.getByRole("button", { name: "保存" }).click();

  await expect(page.getByText("有効なメールアドレスを入力してください")).toBeVisible();
});

エラー UI / aria 編 の通りに aria-describedby を付けていれば、メッセージは関連付き テキストとして可視 になります。

タブ切り替えのキーボード操作テスト

TabBar a11y 編 の矢印キー操作も Playwright で再現できます。

test("矢印キーで詳細タブに移動できる", async ({ page }) => {
  await page.getByRole("tab", { name: "フォーム" }).focus();
  await page.keyboard.press("ArrowRight");

  await expect(page.getByRole("tab", { name: "詳細" })).toBeFocused();
  await expect(page.getByRole("tab", { name: "詳細" })).toHaveAttribute("aria-selected", "true");
});

toBeFocused / toHaveAttribute("aria-selected", "true") の 2 軸で押さえると、a11y の挙動とアプリの状態の両方が検証できます。

リセット動作のテスト

「全ストアをリセット」が UI / store 両方を初期値に戻すことを確認します。

test("全ストアをリセットでフォームと store が初期値に戻る", async ({ page }) => {
  await page.getByLabel("名前").fill("alice");
  await page.getByLabel("レート").fill("80");
  await page.getByRole("button", { name: "保存" }).click();

  await page.getByRole("button", { name: "全ストアをリセット" }).click();

  // DOM 側
  await expect(page.getByLabel("名前")).toHaveValue("");
  await expect(page.getByLabel("レート")).toHaveValue("0");

  // store 側
  const snapshot = await page.evaluate(() => (window as any).__settings.getState());
  expect(snapshot.name).toBe("");
  expect(snapshot.rate).toBe(0);
});

DOM と store の 両方を assert することで、片方だけ戻っているような半端な状態を検出できます。

ポイント

  • store を window.__settings で expose すると、テストから getState() を直接見られる。本番には出さないよう環境変数でガードする
  • getByRole / getByLabel を中心に locator を組む。a11y を満たすほど locator が短く書ける
  • DOM の検証は toBeVisible / toContainText / toHaveAttribute、store の検証は page.evaluate で getState() を読む
  • 「全ストアをリセット」のような 横断機能 は DOM と store の 両方 を assert する。半端な状態を検出できる
  • a11y 関連(タブ・エラー)も Playwright でカバー。toBeFocused / aria-selected で属性まで含めて検証する

関連記事

セットで読むと文脈がつながる関連記事は次のとおりです。

  • storeSync テスト編 — ロジック単位の Vitest テスト
  • TabBar a11y 編 — Playwright で再現する操作の仕様
  • エラー UI / aria 編 — getByRole("alert") で引けるエラー表示
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 追加記事。store の値そのものを確認するために window で expose する書き方と、locator + getByRole で UI を assert するパターンを併記する。

リポジトリを見る

関連サンプル

同じテーマや技術スタックを使った実装例

  • Zustand Test の storeSync を Vitest でユニットテストする

    settings から profile へ subscribe で同期するレイヤを、Vitest で「呼ばれた / 呼ばれない」「整形が正しい」を確かめるテストの書き方を解説する。

  • Playwright でログインから保護ページ到達までの認証フローを E2E テストする

    Playwright でログインフォームへの入力・送信・Cookie 設定・保護ページへのリダイレクト確認・ログアウトまでの認証フローを E2E テストする例。

  • Playwright でファイルアップロードの E2E テストを書く

    Playwright の setInputFiles を使って file input へのファイル選択をシミュレートし、アップロードの成功・エラー・複数ファイルケースを E2E テストする例。

  • Playwright でページネーション付き一覧のページ遷移をE2Eテストする

    一覧ページのページネーション操作(次へ・前へ・ページ番号クリック)を Playwright でE2Eテストし、URL クエリと表示件数の整合性を検証する例。

  • Playwright で検索フォーム入力から結果表示・0 件状態までを E2E テストする

    Playwright で検索フォームへの入力・URL パラメータ更新・一覧フィルタ結果確認・0 件 EmptyState 表示・フィルタリセットまでの検索フロー全体を E2E テストする例。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る