Claude CodeでE2Eテスト: Playwright/CypressとCIの実践ガイド
Claude CodeでPlaywright E2Eを作る手順。CI、失敗例、動くコードまで実務向けに解説。
E2EテストをClaude Codeに任せる前に決めること
「Claude Codeにログインテストを書かせたら緑になった」。それだけでは、まだ公開前の安心材料にはなりません。E2Eテスト、つまりEnd-to-Endテストは、ユーザーがブラウザで操作する一連の流れを、本番に近い形で確認するテストです。単体テストより遅く、失敗時の調査も重いので、何でもE2Eに入れるとCIが遅くなり、誰も失敗を見なくなります。
大事なのは、Claude Codeに「全部テストして」と頼むことではなく、売上、問い合わせ、登録、管理画面の誤操作など、壊れると痛い流れを先に選ぶことです。Masaが運用で失敗したのは、UIの見た目確認ばかり増やし、記事末尾のCTAや購入導線が壊れたことに気付けなかったケースでした。E2Eは品質保証であると同時に、収益導線を守る仕組みでもあります。
この記事では、2026年6月時点のPlaywright公式ドキュメント、ロケーター、CI設定、トレースビューアを確認したうえで、Claude Codeへの依頼方法、コピペで動かせるPlaywrightコード、CI、headed/headlessの落とし穴、Cypressとの使い分けまで整理します。API側の確認はClaude CodeでAPIテスト自動化、全体設計はClaude Codeテスト戦略ガイドも合わせて読むとつながります。
flowchart LR
A["Claude Codeへの依頼"] --> B["重要フローを3つ選ぶ"]
B --> C["Playwrightで実装"]
C --> D["TraceとHTML Reportで調査"]
D --> E["CIの必須チェック"]
C --> F["CTAと収益導線を確認"]
Claude Codeに渡す依頼の型
最初の依頼では、対象画面、成功条件、失敗条件、触ってよいファイル、検証コマンドをまとめます。曖昧な依頼だと、Claude CodeはCSSセレクターだけで動く脆いテストを作りがちです。Playwright公式は、getByRole、getByLabel、getByText、getByTestIdのようなロケーターを推奨しています。ロケーターは「今その瞬間の要素の探し方」で、Playwrightの自動待機やリトライと相性がよい入口です。
対象: ログイン、購入、ニュースレター登録のE2Eテスト
ツール: Playwright Test。Cypressへ移す判断材料も最後に書く
条件:
- CSSクラスではなく role/label/testid を優先する
- 成功パスだけでなく、入力エラーや空カートも確認する
- CIでは workers: 1、trace: on-first-retry
- 収益CTAの href が消えていないことを検証する
触ってよい範囲:
- tests/e2e/**/*.spec.ts
- playwright.config.ts
検証:
- npx playwright test
- npx playwright test --headed はローカル調査用
この時点で「テスト対象外」も書きます。たとえば決済プロバイダーの本番課金、メール実送信、外部広告のクリックはE2Eで直接叩かず、テスト環境、モック、API確認に分けます。外部連携をまとめて見たい場合はClaude Code webhook実装やアナリティクス実装と分けて設計してください。
セットアップと基本コマンド
新規プロジェクトなら、Playwrightは次の流れで始められます。公式ドキュメントでは、Playwright更新後にブラウザバイナリも入れ直す必要がある点が説明されています。CIではOS依存ライブラリも必要になるため、Linux環境ではinstall --with-depsを使うのが現実的です。
npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright show-report
--headedはブラウザを表示して実行する調査用、--uiはUIモードでステップを見ながら確認するためのものです。通常のCIはheadless、つまり画面を表示しないモードで動きます。ローカルだけ通るテストは、アニメーション、フォント、画面幅、タイムゾーン、CPU速度、外部API待ちのどれかに依存していることが多いです。
そのまま動かせるPlaywrightサンプル
次のコードは、実アプリを立ち上げなくてもpage.setContentで小さなデモ画面を作るため、Playwrightが入っていればそのまま実行できます。実務ではrenderDemoApp(page)をpage.goto('/login')などに置き換え、ラベルやdata-testidを自分の画面に合わせます。
// tests/e2e/claude-code-e2e.spec.ts
import { test, expect, type Page } from '@playwright/test';
async function renderDemoApp(page: Page) {
await page.setContent(`
<!doctype html>
<main>
<form id="login-form" aria-label="Login form">
<label>Email <input id="email" aria-label="Email" /></label>
<label>Password <input id="password" aria-label="Password" type="password" /></label>
<button type="submit">Log in</button>
<p id="login-error" role="alert" hidden></p>
</form>
<section id="dashboard" hidden>
<h1>Dashboard</h1>
<a data-testid="training-cta" href="/training/">Book Claude Code training</a>
<button id="add-plan">Add Pro plan to cart</button>
<span data-testid="cart-count">0</span>
<button id="buy">Complete purchase</button>
<p data-testid="order-status" role="status"></p>
<label>Newsletter email <input id="newsletter-email" aria-label="Newsletter email" /></label>
<button id="newsletter-button" type="button">Join newsletter</button>
<p id="newsletter-error" role="alert" hidden></p>
<p data-testid="newsletter-status" role="status"></p>
</section>
</main>
<script>
const state = { cart: 0 };
document.querySelector('#login-form').addEventListener('submit', (event) => {
event.preventDefault();
const email = document.querySelector('#email').value;
const password = document.querySelector('#password').value;
const error = document.querySelector('#login-error');
if (email === 'masa@example.com' && password === 'password123') {
document.querySelector('#login-form').hidden = true;
document.querySelector('#dashboard').hidden = false;
error.hidden = true;
return;
}
error.textContent = 'Authentication failed';
error.hidden = false;
});
document.querySelector('#add-plan').addEventListener('click', () => {
state.cart += 1;
document.querySelector('[data-testid="cart-count"]').textContent = String(state.cart);
});
document.querySelector('#buy').addEventListener('click', () => {
const status = document.querySelector('[data-testid="order-status"]');
status.textContent = state.cart === 0 ? 'Cart is empty' : 'Order ORD-1001 completed';
});
document.querySelector('#newsletter-button').addEventListener('click', () => {
const email = document.querySelector('#newsletter-email').value;
const error = document.querySelector('#newsletter-error');
const status = document.querySelector('[data-testid="newsletter-status"]');
if (!email.includes('@')) {
error.textContent = 'Enter a valid email';
error.hidden = false;
status.textContent = '';
return;
}
error.hidden = true;
status.textContent = 'Thanks, we will send the checklist.';
});
</script>
`);
}
async function loginAsDemoUser(page: Page) {
await page.getByLabel('Email').fill('masa@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Log in' }).click();
}
test.describe('Claude Code E2E starter', () => {
test('use case 1: login shows dashboard and keeps the training CTA', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('training-cta')).toHaveAttribute('href', '/training/');
});
test('use case 2: checkout flow creates an order number', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await page.getByRole('button', { name: 'Add Pro plan to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
await page.getByRole('button', { name: 'Complete purchase' }).click();
await expect(page.getByTestId('order-status')).toContainText('ORD-1001');
});
test('use case 3: newsletter validation blocks invalid leads', async ({ page }) => {
await renderDemoApp(page);
await loginAsDemoUser(page);
await page.getByRole('button', { name: 'Join newsletter' }).click();
await expect(page.getByRole('alert')).toContainText('Enter a valid email');
await page.getByLabel('Newsletter email').fill('reader@example.com');
await page.getByRole('button', { name: 'Join newsletter' }).click();
await expect(page.getByTestId('newsletter-status')).toContainText('Thanks');
});
});
このサンプルの狙いは3つあります。ログイン後の管理画面、購入フロー、リード獲得フォームという、プロダクトとメディアの両方で壊れると困る流れをまとめて見ています。さらに、training-ctaのhrefも確認しています。記事やSaaSでCTAが消える不具合は、画面が表示されているだけでは見逃します。収益化サイトでは「読める」だけでなく「次の行動に進める」ことまでE2Eに入れるべきです。
実務用のplaywright.config.ts
実アプリに入れるときは、設定ファイルに調査用の証跡を残します。公式CIガイドでは、安定性と再現性を優先するならCIのworkersを1にする方針が案内されています。強いマシンなら並列化やシャーディングを検討できますが、最初は遅くても再現しやすい構成のほうがレビューに向いています。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
use: {
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
trace: 'on-first-retry'にしておくと、1回目に落ちてリトライしたテストの操作履歴を追えます。失敗時に「何が見えていたか」「どのクリックが失敗したか」をHTMLレポートやTrace Viewerで見られるので、Claude Codeにも「このtraceを前提に原因を絞って」と依頼しやすくなります。
GitHub ActionsでCIに入れる
CIではブラウザ本体だけでなく、Linuxの依存ライブラリ不足で落ちることがあります。Playwright公式はnpx playwright install --with-depsを案内しています。ブラウザバイナリのキャッシュは必ずしも速くならないため、まずはシンプルに毎回インストールし、遅さが問題になってから最適化します。
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Claude Codeにこのファイルを作らせる場合は、既存のCI、Nodeバージョン、パッケージマネージャー、デプロイ前後のどちらで動かすかを必ず伝えます。プレビューURLが必要なNext.jsやAstroサイトでは、BASE_URLをVercelやCloudflare PagesのプレビューURLに向ける設計もあります。ローカルサーバーを起動する構成なら、webServer設定を追加します。
3つ以上の実用ユースケース
| ユースケース | E2Eで確認する理由 | 重要な期待値 |
|---|---|---|
| ログインからダッシュボード | 認証、リダイレクト、権限表示がつながる | 見出し、ユーザー名、ログアウト、エラー表示 |
| 購入、予約、問い合わせ | 売上やリードに直結する | 注文番号、確認画面、二重送信防止、CTAリンク |
| 記事から無料PDFや研修ページへ進む | AdSense以外の収益導線を守る | CTAのhref、計測イベント、モバイル表示 |
| 管理画面の危険操作 | 誤削除や権限漏れを防ぐ | 確認モーダル、監査ログ、ロール別の非表示 |
| 多言語ページの主要導線 | 翻訳後のリンク切れを防ぐ | locale付きURL、フォーム文言、外部リンク |
Claude Codeには、この表を先に渡すと出力が安定します。「重要な期待値」を書かないと、ボタンを押して終わるだけのテストになります。収益導線を含む記事なら、コンテンツファネル監査の観点で、CTA、内部リンク、無料チートシート、商品ページ、研修相談のどこにつながるかも確認してください。
よくある失敗と落とし穴
1つ目は、waitForTimeout(3000)で待つことです。固定秒数の待機は、自分のPCでは通ってもCIで落ちます。Playwrightは自動待機とリトライ可能なexpectを備えているので、await expect(page.getByRole(...)).toBeVisible()のように「どうなったら成功か」を待ちます。
2つ目は、CSSクラスやDOM構造に依存することです。.btn-primary:nth-child(2)はデザイン変更ですぐ壊れます。ユーザーが認識するrole、label、textを優先し、どうしても難しい場所だけdata-testidを使います。
3つ目は、テスト同士で状態を共有することです。前のテストが作ったカート、Cookie、DBデータを次のテストが使うと、並列実行やリトライで壊れます。テストごとにユーザー、注文、データを分けるか、APIで準備と後片付けを明示します。
4つ目は、headedとheadlessの差を軽く見ることです。headlessではフォント、GPU、スクロール、動画、クリップボード、ファイル選択などの挙動がローカル表示と違うことがあります。Linux CIでheaded実行したい場合はXvfbが必要になります。普段はheadlessで安定させ、調査時だけ--headedや--uiを使います。
5つ目は、外部サービスを本物のまま叩くことです。決済、メール、広告、CRMをE2Eから毎回呼ぶと、遅く、壊れやすく、費用も発生します。本当に見たいのが「購入完了画面」なら、決済はテストモードかモックにし、WebhookやAPIの契約は別テストで守ります。
PlaywrightとCypressの使い分け
| 観点 | Playwright | Cypress |
|---|---|---|
| ブラウザ | Chromium、Firefox、WebKit | Chrome系、Firefox、Edge中心 |
| 並列実行 | Playwright Testで扱いやすい | Dashboard連携を含めて設計 |
| 複数タブ、複数コンテキスト | 得意 | 制約がある |
| デバッグ体験 | Trace Viewer、UI mode、HTML report | GUIの体験が分かりやすい |
| 向いている場面 | CI、複数ブラウザ、認証状態の分離 | フロントエンドチームの対話的開発 |
Cypressも良い選択肢です。ただ、2026年時点でClaude Codeと組み合わせてCI前提のE2Eを整えるなら、Playwrightのトレース、複数ブラウザ、ロケーター、設定ファイルの一貫性は扱いやすいです。既にCypress資産があるチームは移行を急がず、重要フローだけPlaywrightで追加して比較すると安全です。公式情報はPlaywrightとCypress Documentationを必ず原典で確認してください。
Claude Codeにレビューさせる観点
実装後は、Claude Codeに「テストを増やして」ではなく「壊れやすい理由を探して」と依頼します。見るべき項目は、ロケーターの安定性、期待値の具体性、CIでの再現性、テストデータの独立性、外部サービス依存、収益CTAの消失、失敗時レポートの残り方です。
プロンプト例は次のように短くて十分です。
このPlaywrightテストを公開前レビューしてください。
観点:
- CSSセレクターや固定waitに依存していないか
- 成功パスだけでなく失敗パスがあるか
- CI headlessで落ちやすい箇所がないか
- 収益CTA、内部リンク、計測イベントを壊していないか
- trace/screenshot/videoで失敗原因を追えるか
出力:
- 重大度順の指摘
- 修正案
- 追加すべき最小テスト
マネタイズCTA: テストを売上導線に接続する
E2Eテストは「品質のためのコスト」だけではありません。問い合わせ、研修予約、商品購入、無料PDF登録が壊れていないことを毎回確認できれば、記事やSaaSの改善を安心して進められます。個人でまず試すなら無料チートシートでClaude Codeの基本コマンドを固定し、繰り返し使うプロンプトは商品一覧のテンプレートに寄せると楽です。チームでCI、CLAUDE.md、レビュー観点、E2Eの責任分担まで整えるならClaude Code研修・導入相談が向いています。
この記事で紹介した内容を実際に試した結果
今回のサンプルは、ログイン、購入、ニュースレター登録、CTAリンク確認を1つのPlaywright specにまとめ、構文チェック用にTypeScriptコードとして抜き出して確認しました。Masaの運用で効果が大きかったのは、最初から「収益CTAのhrefを確認する」とテスト名に入れたことです。これにより、単に画面が表示されるだけのE2Eではなく、記事から研修ページへ進む導線まで保護できました。実プロジェクトに入れるときは、最初の3本をログイン、購入または問い合わせ、記事CTAに絞り、CIで安定してから管理画面や多言語ページへ広げるのが現実的です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。