<

Playwright Component Test を試してみた

Playwright上で直接ブラウザ上のコンポーネントテストを実行できる「Playwright Component Test」(以下、playwright-ct)について知り、実際に試してみました。 この記事では、その体験を共有します。実際に使用したリポジトリは下記の通りです。

https://github.com/silverbirder/react-todoMVC-2

準備

playwright-ctはReactなど複数のフレームワークをサポートしています。 今回は、create-react-appが非推奨となっているため、Next.jsを使ってアプリケーションを構築しました。 TodoMVCを例に取り入れました。

playwright-ctのセットアップは npm init playwright@latest -- --ctで行い、いくつかのファイルが生成されます。 生成されたファイルのうち、playwright-ct.config.ts に以下のようなコードを追加しました。 これは、Next.jsのtsconfigでpaths設定をしているので、その内容をplaywright-ctにも反映させるためです。

// playwright-ct.config.ts
export default defineConfig({
  ...
  use: {
    ...
    ctViteConfig: {
      resolve: {
        alias: {
          '#': resolve(__dirname, './'),
        },
      },
    },
  },
});

また、todoMVCのスタイルを適用するために、playwright/index.tsx というファイルに、以下のコードを追加しました。

// playwright/index.tsx
import "todomvc-app-css/index.css";

playwright-ctの仕組みは、コードをコンパイルし、それをローカルWebサーバーで提供し、playwright/index.html に読み込ませて描画し、テストします

テストコードの例

Todoリストコンポーネントのコードは以下のようになります。

// todo-list.tsx
import React from "react";
import { Todo } from "./types";

const TodoList = ({ todos, toggleTodo, deleteTodo }) => (
  <ul className="todo-list">
    {todos.map((todo) => (
      <li key={todo.id} className={todo.completed ? "completed" : ""}>
        <div className="view">
          <input
            type="checkbox"
            className="toggle"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <label>{todo.text}</label>
          <button className="destroy" onClick={() => deleteTodo(todo.id)}></button>
        </div>
      </li>
    ))}
  </ul>
);

export default TodoList;

テストケースは、以下のように書きました。

// テストケース
import { test, expect } from "@playwright/experimental-ct-react";
import TodoList from "./todo-list";

test.use({ viewport: { width: 500, height: 500 } });

test("completed状態でtodoの表示が異なること", async ({ mount, page }) => {
  await mount(
    <TodoList
      todos={[
        { id: 1, text: "My Todo 1", completed: true },
        { id: 2, text: "My Todo 2", completed: false },
      ]}
      toggleTodo={() => {}}
      deleteTodo={() => {}}
    />
  );
  await expect(page).toHaveScreenshot();
});

playwright-ctでは 本物のブラウザ環境でテストができる のが魅力です。viewportやwindowオブジェクト、WebAPIも扱えます。 また、クロスブラウザテスト(chromium,firefox,webkit)も可能です。

テストは、playwright test -c playwright-ct.config.ts で実行できます。

次は、Next.jsのページコンポーネントのコードです。(React Server Components ではありません)

// page.tsx
"use client";

import TodoList from "@/ui/todo-list";
import { Todo } from "@/ui/types";
import { useState, type KeyboardEvent } from "react";

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");

  const addTodo = () => {
    if (!text) return;
    const newTodo: Todo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
    setText("");
    const params = new URLSearchParams("");
    params.set("added", "true");
    window.history.pushState(null, "", `?${params.toString()}`);
  };

  const toggleTodo = (id: number) => {
    setTodos(
      todos.map((todo) => {
        if (todo.id === id) {
          return { ...todo, completed: !todo.completed };
        }
        return todo;
      })
    );
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  const handleKeydown = (e: KeyboardEvent) => {
    if (e.key !== "Enter") {
      return;
    }
    addTodo();
  };

  return (
    <section className="todoapp">
      <header className="header">
        <h1>todos</h1>
        <input
          className="new-todo"
          value={text}
          onChange={(e) => setText(e.target.value)}
          onKeyDown={(e) => handleKeydown(e)}
          placeholder="What needs to be done?"
          autoFocus
        />
      </header>
      <main className="main">
        <TodoList
          todos={todos}
          toggleTodo={toggleTodo}
          deleteTodo={deleteTodo}
        />
      </main>
    </section>
  );
}

addTodo 関数は、敢えて window.history を使用しています。 Next.jsのuseRouterのような機能を使うためには、ある程度の準備が必要となります。 従来のテストでは、このようなwindowオブジェクトに関連する部分をモック(模倣)してテストすることが一般的でした。 しかし、playwright-ctを使用すると、これらの本物のブラウザ機能を使ってテストを行うことが可能です。

ページコンポーネントのテストコード例を、以下に示します。

// page.test.tsx
import { test, expect } from "@playwright/experimental-ct-react";
import App from "./page";

test.use({ viewport: { width: 500, height: 500 } });

test("todosの文字が表示されること", async ({ mount }) => {
  // Act
  const component = await mount(<App />);

  // Assert
  await expect(component).toContainText("todos");
});

test("todoを追加したらURLにaddedクエリパラメータが追加されること", async ({
  mount,
  page,
}) => {
  // Arrange
  const component = await mount(<App />);
  await component.getByRole("textbox").fill("My Todo 1");

  // Act
  await page.keyboard.press("Enter");

  // Assert
  expect(page).toHaveURL(/added/);
});

playwright-ctでは、コンポーネントに対する操作だけでなく、Playwrightのpageオブジェクトを直接操作することも可能です。 これにより、テストのバリエーションが大きく広がり、より多角的なテストシナリオの実行が可能になります。

まとめ

playwright-ctは、実際のブラウザ環境により近い形でテストを行うことを可能にし、結合テストの役割を果たします。 PlaywrightはJestのようなモッキング機能を持たないため、例えば todo-list.tsx のハンドラ関数が正しく実行されたかどうかの検証は難しい面があります。 単体テストレベルの詳細な検証には、依然としてJestのようなツールが必要ですが、画面を構成するコンポーネントのテストには、playwright-ctが非常に役立つと思います。 今後もこのようなツールを積極的に活用し、品質の高いWebアプリケーションの開発を目指していきたいと考えています。

役立ったら、☕でサポートしてね!

シェアしよう

関連するタグ