DELOGs
はじめてのPlaywright-"録画"で体験する E2E テスト入門

はじめてのPlaywright-"録画"で体験する E2E テスト入門

ボタンをクリックしているだけで、テストが自動生成。『壊れてないか?』を毎回手動で確かめる作業とおさらば

初回公開日

最終更新日

今回は、Playwrightを使ったE2Eテストについてです。PlaywrightとかE2Eテストの話題はよく聞くのですが、この記事を書いている時点では、私は全く利用したことがありませんでした。E2Eテスト? うーーん。ブラウザでチェックするのは普通にやっているけどって感じでした。しかし、少し調べてみるとこれを自動化するメリットはたくさんあることに気付かされました。それをまとめています。

この記事は誰のため?

  • テスト=難しいと感じているフロントエンド初心者
  • 「動くかどうか」の確認を毎回手動でやっている 個人開発者
  • CI/CD って聞いたことはあるけどまだ触っていない エンジニア
Playwright の "録画" 機能を使えば、ブラウザを操作するだけで自動でテストが書ける ので、コーディング不要で E2E テストの世界を体験できます 。

1. E2E テストってなに?

End‑to‑End(エンドツーエンド)テスト:アプリを ユーザー視点で最初から最後まで 通して動かし、「ちゃんと動く?」を確かめるテスト手法。
というわけで、E2Eテストといっても、WEBアプリケーションやWEBサイト制作をしている皆さんなら、普通に複数のブラウザでチェックをしていると思います。それをE2Eテストって呼んでいるというだけですね。
  • ユニットテスト: 関数やコンポーネント単位のチェック
  • 結合テスト: 機能同士の連携をチェック
  • E2E テスト: ブラウザを操作して人間の操作を再現
💡 例えば「ログイン → ダッシュボード表示まで問題なく進めるか?」を自動で繰り返し確認できます。

2. Playwright って?

上記のE2Eテストを自動化できるツールがPlaywrightというツールです。下記のようなことができます。
  • Microsoft 製 OSS。Chromium / Firefox / WebKit を同一 API で操作
  • 録画(codegen) 機能が超カンタン:ブラウザ操作 → TypeScript/JavaScript テストを自動生成
  • 画像比較・動画キャプチャ・モバイルエミュレータなど多機能
複数のブラウザで同じようなことをチェックするのは面倒だったり、ちょっとした修正でもフォームがしっかり動くのか何度もテストするって手間が省けます。これは便利です。
ここでは 録画して即再生 の部分だけ触ります。難しい設定は一切ナシで OK👌

3. 今日のゴール

今回は、下記のことを体験してみます。
    1. Playwright をプロジェクトにインストール
    1. ローカルでアプリを起動した状態で 録画開始
    1. クリック操作を数回 → テストファイルを保存
    1. テストを実行し、HTML レポートで結果を確認

4. 事前準備(環境とインストール)

一応、私の環境は下記の通りです。ローカルPCのOS依存の部分はないとは思います。
ソフトバージョン例
macOS14.5 (Apple Silicon)
Node.jsv22.13.1
Next.js プロジェクト15.3.5
今回は、Next.jsとshadcn/uiをインストールして、見た目だけを少しフォームっぽくしたものを利用します。
サンプルフォームの状態
簡単なフォームサンプルをGitHubに用意してありますので、そちらを利用いただくとこの記事と同様の体験が可能です。
上記を利用する場合は下記を実行してください。
zsh
1# クローン生成 2git clone https://github.com/delogs-jp/playwright-e2e.git 3cd playwright-e2e 4 5# 依存インストール(Playwright はまだ入っていない) 6npm install

Playwrightのインストール

Playwrightはテストでしか利用しませんので-Dオプションをつけて、対象プロジェクトにインストールします。
bash
1# プロジェクト直下で Playwright を追加 2npm i -D @playwright/test 3npx playwright install # ブラウザバイナリを一括取得
初回は数百 MB のダウンロード が走ります。Wi‑Fi 環境でどうぞ。
上記2つは下記のように、役割が異なりますので、それぞれインストールが必要です。
手順何をする?実体いつ必要?
npm i -D @playwright/testPlaywright のライブラリ+テストランナー を npm に追加JavaScript/TypeScript のコード & CLI (npx playwright …)プロジェクトで Playwright を 使う ため
npx playwright install各ブラウザ用の実行バイナリ をローカルに落とすChromium / Firefox / WebKit(数百 MB)初回セットアップ時・バージョンを上げたとき

5. "録画" してみよう(codegen)

さっそく、録画機能を体験してみます。npm run devでプロジェクトを実行して、ターミナルの分割で別タブ等で立ち上げて、コマンド実行します。
zsh
1# ターミナル 1 2npm run dev # ← 開発サーバーを起動 3 4# ターミナル 2(3000番で開発サーバーが立ち上がった場合) 5npx playwright codegen http://localhost:3000
  • 実行すると「Chromium」が2つ立ち上がります:右ペイン = 実際のブラウザ左ペイン = 自動生成されるテストコード
  • クリックや入力をすると、リアルタイムでコードが増えていく様子が見えます
ただ、実行するとNext.jsのエラーマークが下図のように左に表示されると思います。 Next.jsのエラーアラート
これは結論、無視しましょうという話になるのですが、Playwright codegen は録画のために、ページに下記の 計測用属性 を動的に挿入します。
html
1<body data-pw-cursor="pointer">
ところが Next.js(開発モード)は
  • SSR で描いた HTML ← data-pw-cursor なし
  • クライアント側で再描画(Hydration) ← data-pw-cursor がある …という DOM 不一致 を検出し、赤い “Hydration failed” オーバーレイを表示します。 つまり Playwright の録画専用タグが原因 で、アプリのバグではありません。
なので、ちょっと気になりますが、npm run devで録画を実行しているとこれが出てしまうのは正常なことです。あくまでE2Eテストの実行と割り切って無視してしまいましょう。もし、どうしても気になる方はビルドしてnpm startして録画実行すればNext.jsのエラーマークは表示されなくなります。
  • 操作が終わったら左ペインの内容をコピーして tests/home.spec.ts など任意の名前で保存します。毎回コピーして保存が面倒という方は
zsh
1npx playwright codegen -o tests/home.spec.ts http://localhost:3000
上記のように-oオプションでファイル名を指定することで自動保存することもできます。 私の環境では-oオプション出力ファイルを指定すると上掲のNext.jsのエラーマークが表示されなくなりました。これは調べたのですが、ちょっと原因はわかりませんでした。。
🎥 スクロールやドラッグも録画できます。不必要な行はあとで Cursorなどのエディタで削除すれば OK。

6. テストを実行してみる

先程の内容を保存したファイルを指定して、録画結果を実行することができます。 ただし、ちょっと気をつけることがあります。 tests/home.spec.tsの内容を見てみます。
ts : home.spec.ts
1import { test, expect } from "@playwright/test"; 2 3test("test", async ({ page }) => { 4 await page.goto("http://localhost:3001/"); 5 await page.getByRole("textbox", { name: "お名前 必須" }).click(); 6 await page.getByRole('textbox', { name: 'お名前 必須' }).press('KanjiMode'); 7 await page.getByRole("textbox", { name: "お名前 必須" }).fill("テスト"); 8 await page.getByRole('textbox', { name: 'お名前 必須' }).press('Enter'); 9 await page.getByRole("textbox", { name: "お名前 必須" }).press("Tab"); 10});
問題はawait page.getByRole('textbox', { name: 'お名前 必須' }).press('KanjiMode');のところです。 KanjiMode は日本語 IME 切り替え用の特殊キーです。実ブラウザでは KanjiMode が押されても入力欄の状態は変わらない ため、Playwright は “押したキーに反応が無い → まだ終わっていない” と判断し、30 秒でタイムアウト してエラー終了してしまいます。 このように、テスト実行に不要なものは予め削除しておく必要があります。
下記のように編集します。
diff : home.spec.ts
1import { test, expect } from "@playwright/test"; 2 3test("test", async ({ page }) => { 4 await page.goto("http://localhost:3001/"); 5 await page.getByRole("textbox", { name: "お名前 必須" }).click(); 6- await page.getByRole('textbox', { name: 'お名前 必須' }).press('KanjiMode'); 7 await page.getByRole("textbox", { name: "お名前 必須" }).fill("テスト"); 8- await page.getByRole('textbox', { name: 'お名前 必須' }).press('Enter'); 9- await page.getByRole("textbox", { name: "お名前 必須" }).press("Tab"); 10});
録画機能はとにかくすべての操作を記録してしまうので、余計なキー入力は削除する必要があります。 編集が終わったら、下記のコマンドを実行します。
zsh
1npx playwright test tests/home.spec.ts
実行すると:
  • ターミナルに緑(成功) or 赤(失敗)の結果
text
1✓ tests/home.spec.ts:5:1 › Home page should render hero section (2s) 2 3 1 passed (3s)
また、下記のようにレポーター(--reporter)オプションを追加にして:
zsh
1npx playwright test tests/home.spec.ts --reporter=html
上記を実行した後、
zsh
1npx playwright show-report
とすると、レポートを画面で確認することができます。
Playwrightのレポート画面
  • playwright-report/index.html が生成され、ブラウザで詳細レポートを閲覧可能
🤖 レポートにはスクリーンショットも自動添付。失敗時はどのステップで落ちたか一目で分かります。

7. フォームで実践!見た目チェック & 入力自動化

とりあえず、どんな感じってことがわかったので、ここでは「お問い合わせフォーム」を題材に、スクリーンショット差分入力自動化の両方を体験してみます。録画だけでは見えにくい “実践的テスト” を一気に味わえる章です。

スクリーンショット差分でレイアウト崩れを検知

1. テストファイルを作成
tests/contact-visual.spec.tsなどtestsディレクトリ配下に任意の名称で、下記のようにtsファイルを作成します。
ts : contact-visual.spec.ts
1import { test, expect } from "@playwright/test"; 2 3test("お問い合わせフォームのレイアウト", async ({ page }) => { 4 await page.goto("http://localhost:3000/"); // ポートに合わせて変更 5 await page.setViewportSize({ width: 1280, height: 720 }); 6 7 // ❶ 初回: contact-baseline.png を生成 (必ず pass) 8 // ❷ 2 回目以降: baseline と比較 → 差分 0.2% を超えたら fail & diff.png 生成 9 await expect(page).toHaveScreenshot("contact-baseline.png", { 10 threshold: 0.2, // 許容差分 0.2% 11 }); 12});
2.テストを実行して baseline 画像を生成
zsh
1npx playwright test tests/contact-visual.spec.ts --reporter=html --update-snapshots
--update-snapshots は baseline(基準となるスクリーンショット)が存在しない場合に自動生成/更新するオプション です。初回はこれを付けて実行し、フォームの“正しい見た目”を baseline として保存します。
  • 1 回目 は基準画像が保存されるだけ(失敗しません)
3.フォームの見た目を変更
ここで、フォームのボタンの上下のマージンを増やしてみます。src/app/page.tsxを下記のように変更してみます。
diff : page.tsx
1{/* 送信ボタン */} 2- <Button type="submit" className="my-4 w-full"> 3+ <Button type="submit" className="my-8 w-full"> 4 送信 5 </Button>
4.再テスト実行で差分を検知
zsh
1npx playwright test tests/contact-visual.spec.ts --reporter=html
Playwrightの差分検知画面
  • 2 回目以降 に差分が 0.2% を超えると赤くハイライトされてテスト失敗
  • 差分が 0.2% を超えると赤 (fail) + diff.png を生成。
🌟 デザインリファクタ時に「どこが崩れた?」が画像で一発判明。
どんな差分が検知されるかをまとめると下記のようになります。
変更例差分が出る?検知される理由
CSS マージン/パディングを変更
(例: .btn { margin-top: 16px; }32px
ボタンの位置が数ピクセル下がり、スクリーンショットのピクセル配列がbaselineとズレる
フォントサイズ・行間を変更テキストの高さが変わる=ピクセル配置が変わる
色・背景色・ボーダー色を変更RGB 値が違うピクセルとして差分カウント
非表示→表示 / 表示→非表示要素の有無自体がピクセル差分
テキスト文言の変更文字形状が変わるのでピクセルが異なる
アニメーションだけ
(hover など動きで変わる部分)
△ ※静止状態で同じフレームなら差分なし
動きがランダムに止まると差分が出る場合も
同じレイアウトでデータだけ動的
(日時・ランダム数値)
or画像に映らなければ差分なし / 映れば差分
必要なら固める or モックに置換
これはチーム開発だと価値が出る場面は容易にイメージできますが、一人開発でも下記のようなケースで活きます。
ケース具体例
リファクタやデザイン刷新いくつも SCSS / Tailwind クラスを触るとき、過去ページの崩れをセルフ QA できる
将来の自分への安全網深夜のリリースや急ぎバグ修正で「実はパディング消えてた…」を防ぐ
複数ブラウザ確認の時短Safari だけ崩れた/Firefox だけフォントが太い…を同時にキャッチ

入力バリデーションを複数パターンでテスト

ここは少しイメージが付きやすい部分です。同様にテストファイルを作成します。
◯正常パターンだけすばやくテスト
tests/contact-happy.spec.tsというファイルを下記内容で作成します。
ts : contact-happy.spec.ts
1import { test, expect } from "@playwright/test"; 2 3test("お問い合わせフォーム 正常送信", async ({ page }) => { 4 await page.goto("http://localhost:3000/"); 5 6 await page.getByLabel("お名前").fill("テスト太郎"); 7 await page.getByLabel("メールアドレス").fill("taro@example.com"); 8 await page.getByLabel("お問い合わせ内容").fill("Playwright の動作確認です。"); 9 10 await page.getByRole("button", { name: "選択してください" }).click(); 11 await page.getByRole("option", { name: "質問" }).click(); 12 13 await page.getByRole("button", { name: "送信" }).click(); 14 15 await expect(page).toHaveURL(/\/thanks/); 16 await expect( 17 page.getByRole("heading", { name: "送信ありがとうございました" }) 18 ).toBeVisible(); 19});
下記で実行します。
zsh
1npx playwright test tests/contact-happy.spec.ts --reporter=html 2npx playwright show-report # (任意)結果をブラウザで確認
少しページを修正したけど、一応フォームもテストしておかないというときは、とても便利です。
◯NGパターンを含めたテスト
4 パターン(正常2 + バリデーションNG2 + 文字数上限NG1)の入力をループでテストします。 成功ケースはサンクスページ遷移を、失敗ケースはフォーム内エラーメッセージの表示を検証します。
tests/contact-validation.spec.tsで下記内容のファイルを作成します。
ts : contact-validation.spec.ts
1import { test, expect } from "@playwright/test"; 2 3/** テストケース定義 */ 4type Case = { 5 name: string; 6 data: { 7 name: string; 8 email: string; 9 category: "question" | "feedback" | "other"; 10 message: string; 11 newsletter?: boolean; 12 }; 13 expectSuccess: boolean; 14}; 15 16const cases: Case[] = [ 17 // --- 正常系 ----------------------------- 18 { 19 name: "正常: 全項目OK (質問)", 20 data: { 21 name: "テスト太郎", 22 email: "taro@example.com", 23 category: "question", 24 message: "Playwright 導入について質問です。", 25 newsletter: true, 26 }, 27 expectSuccess: true, 28 }, 29 { 30 name: "正常: ニュースレター OFF (フィードバック)", 31 data: { 32 name: "フィードバック花子", 33 email: "hanako@example.com", 34 category: "feedback", 35 message: "UI が見やすかったです!", 36 newsletter: false, 37 }, 38 expectSuccess: true, 39 }, 40 41 // --- バリデーション NG ------------------- 42 { 43 name: "エラー: メール形式不正", 44 data: { 45 name: "メールエラー", 46 email: "wrong-format", 47 category: "other", 48 message: "メール形式がおかしいケース", 49 }, 50 expectSuccess: false, 51 }, 52 { 53 name: "エラー: お名前未入力", 54 data: { 55 name: "", 56 email: "noname@example.com", 57 category: "question", 58 message: "名前が空のケース", 59 }, 60 expectSuccess: false, 61 }, 62 63 // --- 文字数上限 NG ----------------------- 64 { 65 name: "エラー: お問い合わせ内容が 1000 文字超", 66 data: { 67 name: "長文エラー", 68 email: "long@example.com", 69 category: "feedback", 70 message: "長文".repeat(501), // 1002 文字 71 }, 72 expectSuccess: false, 73 }, 74]; 75 76test.describe("お問い合わせフォーム入力バリデーション", () => { 77 for (const tc of cases) { 78 test(tc.name, async ({ page }) => { 79 /* 1️⃣ フォームを開く */ 80 await page.goto("http://localhost:3001/"); // ポート変更時は合わせる 81 82 /* 2️⃣ 入力 */ 83 await page.getByLabel("お名前").fill(tc.data.name); 84 await page.getByLabel("メールアドレス").fill(tc.data.email); 85 await page.getByLabel("お問い合わせ内容").fill(tc.data.message); 86 87 // カテゴリ選択 88 await page.locator("#category-trigger").click(); 89 await page 90 .getByRole("option", { 91 name: 92 tc.data.category === "question" 93 ? "質問" 94 : tc.data.category === "feedback" 95 ? "フィードバック" 96 : "その他", 97 }) 98 .click(); 99 100 // ニュースレター 101 if (tc.data.newsletter) { 102 await page 103 .getByRole("switch", { name: "ニュースレターを受け取る" }) 104 .click(); 105 } 106 107 /* 3️⃣ 送信 */ 108 await page.getByRole("button", { name: "送信" }).click(); 109 110 /* 4️⃣ 期待値 */ 111 if (tc.expectSuccess) { 112 // サンクスページへ遷移し、見出しが表示される 113 await expect(page).toHaveURL(/\/thanks/); 114 await expect( 115 page.getByRole("heading", { name: "送信ありがとうございました" }), 116 ).toBeVisible(); 117 } else { 118 // 成功ページに遷移していない(=バリデーションで止まった) 119 await expect(page).not.toHaveURL(/\/thanks/); 120 } 121 }); 122 } 123});
下記で実行します。
zsh
1npx playwright test tests/contact-validation.spec.ts --reporter=html 2npx playwright show-report # (任意)結果をブラウザで確認
  • 下記のようにバリデーションテストの結果を確認できます。 Playwrightのフォームバリデーションテストの結果画面
これもエラーパターンを何度も実行する場合にとても便利です。 これだけで「手動確認」から 一歩先の自動テスト へ進めます。まずはローカルで試し、効果を実感してみてください!

8. Playwright が特に威力を発揮するシーン

シーン課題Playwright がどう解決?
頻繁な UI リファクタデザイン刷新のたびに全ページ確認は大変基準スクショ差分で崩れを即検知
多言語・通貨対応言語ごとにテキスト長が変わる → レイアウト崩れtest.use({ locale: 'fr-FR' }) などで自動巡回
フィーチャーフラグ機能 ON/OFF 組合せの検証地獄project 毎にフラグを切り替えて並列テスト
外部 API 依存ステージング API が落ちてテスト不能page.route() で API をモック & オフラインでも実行
パフォーマンス監視秒単位の劣化を見逃しがちpage.evaluate(() => performance.now()) で速度アサート
アクセシビリティa11y lint だけでは不十分expect(page).toPassAxe() プラグインで自動 a11y レポート

9. 次の一歩:CI で自動実行へ

録画したテストを GitHub Actions に組み込み、PR ごとに自動実行→ 緑になればマージ、を目指します。続編記事「CI で E2E を回す」で詳しく解説予定です。

🔗 参考リンク

この記事の執筆・編集担当
DE

松本 孝太郎

DELOGs編集部/中年新米プログラマー

ここ数年はReact&MUIのフロントエンドエンジニアって感じでしたが、Next.jsを学んで少しずつできることが広がりつつあります。その実践記録をできるだけ共有していければと思っています。