mcpField
E
e2e-tester

e2e-tester

github.com/pageai-proテスト・品質33378 回インストール17 週間前に更新
リポジトリ

e2e-tester について

Playwright を使ったエンドツーエンドテスト作成のためのスキルで、実際のページフローを確認してからテストを書く手順を示します。ページオブジェクトの再利用、セレクター、待機、アサーション、モック、実行方法を扱います。

主な機能

  1. 01エンドツーエンドテストの価値を評価
  2. 02MCP でページフローを確認
  3. 03ページオブジェクトとヘルパーの再利用
  4. 04セレクターと待機方法
  5. 05モック、アサーション、デバッグ

活用シーン

  1. 01ログインや登録フローのテスト作成
  2. 02ページスナップショットに基づくセレクター確認
  3. 03ページオブジェクトとテストヘルパーの再利用
  4. 04待機やアサーションの問題調査

MCP 対応

利用可能な場合、テスト作成前に Playwright MCP ツールでページ遷移、スナップショット確認、要素操作、スクリーンショット取得を行い、フローとセレクターを検証するよう定めています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/pageai-pro/ralph-loop --skill e2e-tester

インストール後、Claude Code で/e2e-testerを入力して読み込まれたか確認します。

詳細

開始する前に:

  • テスト対象の機能をカバーする既存のテストがあるか確認します
  • 単体テストでより適切に機能をカバーできるか確認します
  • 単体テストと比べて、エンドツーエンドテストに価値があるか分析します

上記のいずれかに該当する場合は、エンドツーエンドテストの作成に進みます。

MCP ワークフロー(利用可能な場合は必須)

⚠️ Playwright MCP ツールが利用できる場合、テストを作成する前に必ず使用してください:

  1. 対象ページに移動します
  2. ページ構造と要素を確認するため、スナップショットを取得します
  3. フォームや要素を操作し、ユーザーの操作フローを正確に確認します
  4. 想定される状態を記録するため、スクリーンショットを撮影します
  5. 一連のフロー(読み込み、成功、エラー)を通じてページ遷移を確認します
  6. 実際のセレクターをスナップショットから記録します(実際の参照とラベルを使用します)
  7. 探索が完了してから、確認済みのセレクターを使ってテストコードを作成します

MCP が利用できない場合: ドキュメントとコードの分析に基づいてテストを作成します。

これが重要な理由:

  • ✅ 正確なテスト — 必要な手順を正確に把握できます
  • ✅ 正確なセレクター — 想像ではなく、実際の DOM 構造に基づきます
  • ✅ 実際のフローの検証 — ユーザーの操作が実際に機能することを確認できます
  • ✅ 過剰な設計の回避 — 実在する機能に対して最小限のテストを作成できます
  • ✅ 不安定なテストの防止 — 実際に探索することで、安定したテストを作成できます
  • ❌ UI がどのように動作する「はず」かを決して想定しないでください

待機戦略(重要)

// ❌ NEVER use fixed waits
await page.waitForTimeout(2000);

// ✅ Wait for specific conditions
await expect(element).toBeVisible();
await page.waitForResponse(resp => resp.url().includes('/api/data'));
await page.waitForURL('**/dashboard');
await expect(page.getByText('Success')).toBeVisible({ timeout: 10000 });

// ✅ For SPAs, prefer explicit waits over networkidle
await page.goto('/app', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('main')).toBeVisible();

ファイル構成

tests/
├── base-page.ts              # Parent class for ALL pages
├── helpers.ts                # Shared utilities
└── {page-name}/
    ├── {page-name}-page.ts   # Page Object Model
    ├── {page-name}.spec.ts   # ALL tests here (NO separate files!)
    └── {page-name}.md        # Test documentation

ファイル名:

  • ✅ sign-up.spec.ts(すべてのサインアップテスト)
  • ✅ sign-up-page.ts(ページオブジェクト)
  • ✅ sign-up.md(ドキュメント)
  • ❌ sign-up-critical-path.spec.ts(誤り — ファイルを分けないでください)
  • ❌ sign-up-validation.spec.ts(誤り)

セレクターの優先順位(必須)

// 1. BEST - getByRole for interactive elements
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navLink = page.getByRole("link", { name: "Dashboard" });

// 2. BEST - getByLabel for form controls
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");

// 3. SPARINGLY - getByText for static content only
this.errorMessage = page.getByText("Invalid credentials");
this.pageTitle = page.getByText("Welcome");

// 4. LAST RESORT - getByTestId when above fail
this.customWidget = page.getByTestId("date-picker");

// ❌ AVOID fragile selectors
this.button = page.locator(".btn-primary");  // NO
this.input = page.locator("#email");         // NO

スコープの判定(曖昧な場合は確認)

ユーザーの依頼対応
「テストを1つ」「テストを追加」「新しいテスト」既存の spec に test() を1つ作成
「包括的なテスト」「すべてのテスト」「テストスイート」「テストを生成」テストスイート全体を作成

例:

  • 「ユーザーのサインアップをテストしてください」→ テストを1つだけ作成
  • 「ログインページの E2E テストを生成してください」→ テストスイート全体を作成
  • 「フォームのバリデーションを確認するテストを追加してください」→ 既存の spec にテストを1つ追加

ページオブジェクトパターン

import { Page, Locator, expect } from "@playwright/test";

// BasePage - ALL pages extend this
export class BasePage {
  constructor(protected page: Page) {}

  async goto(path: string): Promise<void> {
    await this.page.goto(path);
    await this.page.waitForLoadState("networkidle");
  }

  // Common methods go here (see Refactoring Guidelines)
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }

  async verifyNotificationMessage(message: string): Promise<void> {
    const notification = this.page.locator('[role="status"]');
    await expect(notification).toContainText(message);
  }
}

// Page-specific implementation
export interface LoginData {
  email: string;
  password: string;
}

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.submitButton = page.getByRole("button", { name: "Sign in" });
  }

  async goto(): Promise<void> {
    await super.goto("/login");
  }

  async login(data: LoginData): Promise<void> {
    await this.emailInput.fill(data.email);
    await this.passwordInput.fill(data.password);
    await this.submitButton.click();
  }

  async verifyCriticalOutcome(): Promise<void> {
    await expect(this.page).toHaveURL("/dashboard");
  }
}

ページオブジェクトの再利用(重要)

新しいページオブジェクトを作成する前に、必ず既存のページオブジェクトを確認してください!

// ✅ GOOD: Reuse existing page objects
import { SignInPage } from "../sign-in/sign-in-page";
import { HomePage } from "../home/home-page";

test("User can sign up and login", async ({ page }) => {
  const signUpPage = new SignUpPage(page);
  const signInPage = new SignInPage(page);  // REUSE
  const homePage = new HomePage(page);      // REUSE

  await signUpPage.signUp(userData);
  await homePage.verifyPageLoaded();  // REUSE method
  await homePage.signOut();           // REUSE method
  await signInPage.login(credentials); // REUSE method
});

// ❌ BAD: Recreating existing functionality
export class SignUpPage extends BasePage {
  async logout() { /* ... */ }  // ❌ HomePage already has this
  async login() { /* ... */ }   // ❌ SignInPage already has this
}

ガイドライン:

  • まず tests/ に既存のページオブジェクトがないか確認します
  • 既存のページをインポートして再利用します
  • ページが存在しない場合にのみ、ページオブジェクトを作成します
  • テストで複数のページが必要な場合は、すべてのページオブジェクトが存在することを確認します(必要に応じて作成します)

リファクタリングのガイドライン

次の場合は BasePage に移動します:
  • ✅ 複数のページで使用されるナビゲーションヘルパー(waitForPageLoad()、getCurrentUrl())
  • ✅ 共通の UI 操作(通知、モーダル、テーマの切り替え)
  • ✅ 複数のページで繰り返される確認パターン(isVisible()、waitForVisible())
  • ✅ すべてのページに適用されるエラー処理
  • ✅ デバッグ用のスクリーンショットユーティリティ
次の場合は helpers.ts に移動します:
  • ✅ テストデータの生成(generateUniqueEmail()、generateTestUser())
  • ✅ セットアップ/ティアダウンのユーティリティ(createTestUser()、cleanupTestData())
  • ✅ カスタムアサーション(expectNotificationToContain())
  • ✅ テストセットアップ用の API ヘルパー(seedDatabase()、resetState())
  • ✅ 時間関連のユーティリティ(waitForCondition()、retryAction())

変更前(悪い例):

// Repeated in multiple page objects
export class SignUpPage extends BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }
}
export class SignInPage extends BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');  // DUPLICATED!
  }
}

変更後(良い例):

// BasePage - shared across all pages
export class BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }
}

// helpers.ts - data generation
export function generateUniqueEmail(): string {
  return `test.${Date.now()}@example.com`;
}

export function generateTestUser() {
  return {
    name: "Test User",
    email: generateUniqueEmail(),
    password: "TestPassword123!",
  };
}

タグを使ったテストパターン

import { test, expect } from "@playwright/test";
import { LoginPage } from "./login-page";

test.describe("Login", () => {
  test("User can login successfully",
    { tag: ["@critical", "@e2e", "@login", "@LOGIN-E2E-001"] },
    async ({ page }) => {
      const loginPage = new LoginPage(page);

      await loginPage.goto();
      await loginPage.login({ email: "[email protected]", password: "pass123" });

      await expect(page).toHaveURL("/dashboard");
    }
  );
});

タグのカテゴリ:

  • 優先度:@critical、@high、@medium、@low
  • 種類:@e2e
  • 機能:@signup、@signin、@dashboard
  • テスト ID:@SIGNUP-E2E-001、@LOGIN-E2E-002

テストドキュメントの形式({page-name}.md)

### E2E Tests: {Feature Name}

**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}

---

## Test Case: `{TEST-ID}` - {Test case title}

**Priority:** `{critical|high|medium|low}`

**Tags:**
- type → @e2e
- feature → @{feature-name}

**Description/Objective:** {Brief description}

**Preconditions:**
- {Prerequisites for test to run}
- {Required data or state}

### Flow Steps:
1. {Step 1}
2. {Step 2}
3. {Step 3}

### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}

### Key verification points:
- {Assertion 1}
- {Assertion 2}

### Notes:
- {Additional considerations}

ドキュメントのルール:

  • ❌ 一般的なテスト実行手順は記載しない
  • ❌ ファイル構成の説明は記載しない
  • ❌ コード例やチュートリアルは記載しない
  • ❌ トラブルシューティングのセクションは記載しない
  • ✅ 個別のテストケースのみに焦点を当てる
  • ✅ 可能な場合は60行以内に収める

認証状態の再利用

// auth.setup.ts - Run once, reuse across tests
import { test as setup } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByLabel("Password").fill("password");
  await page.getByRole("button", { name: "Sign in" }).click();
  await page.waitForURL("/dashboard");
  await page.context().storageState({ path: ".auth/user.json" });
});

// playwright.config.ts
projects: [
  { name: "auth", testMatch: /auth\.setup\.ts/ },
  { name: "logged-in", dependencies: ["auth"], use: { storageState: ".auth/user.json" } },
  { name: "logged-out", use: { storageState: { cookies: [], origins: [] } } }
]

API モック

// Mock API responses for isolated tests
await page.route("**/api/users", route =>
  route.fulfill({ json: { users: [{ id: 1, name: "Test" }] } })
);

// Mock error states
await page.route("**/api/submit", route =>
  route.fulfill({ status: 500, json: { error: "Server error" } })
);

// Abort requests (e.g., block analytics)
await page.route("**/analytics/**", route => route.abort());

テストフィクスチャ

// fixtures.ts - Custom fixtures for setup/teardown
import { test as base } from "@playwright/test";
import { AdminPage } from "./admin/admin-page";

export const test = base.extend<{ adminPage: AdminPage }>({
  adminPage: async ({ page }, use) => {
    const admin = new AdminPage(page);
    await admin.goto();
    await use(admin);
    // Teardown runs after test
  },
});

// Usage in tests
test("admin can manage users", async ({ adminPage }) => {
  await adminPage.deleteUser("[email protected]");
});

並列実行

// Tests run in parallel by default. Use serial when tests share state:
test.describe.configure({ mode: "serial" });

// Isolate test data to prevent conflicts
test("create user", async ({ page }) => {
  const uniqueEmail = `user-${Date.now()}@test.com`; // ✅ Unique per run
});

アサーション

// Soft assertions - collect multiple failures
await expect.soft(page.getByText("Title")).toBeVisible();
await expect.soft(page.getByText("Subtitle")).toBeVisible();
// Test continues, reports all failures at end

// Polling assertions - retry until condition met
await expect(async () => {
  const count = await page.getByRole("listitem").count();
  expect(count).toBeGreaterThan(5);
}).toPass({ timeout: 10000 });

// Visual regression
await expect(page).toHaveScreenshot("dashboard.png");
await expect(page.getByRole("dialog")).toHaveScreenshot();

よくある落とし穴

// ❌ Element detached after navigation
const button = page.getByRole("button", { name: "Submit" });
await button.click();
await button.click(); // May fail if page navigated

// ✅ Re-query after navigation
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("button", { name: "Confirm" }).click();

// ❌ Race condition with animations
await element.click();

// ✅ Wait for animation to complete
await element.click();
await expect(modal).toBeVisible();

// Iframes
const frame = page.frameLocator("#iframe-id");
await frame.getByRole("button").click();

// Shadow DOM - Playwright pierces by default, but for closed shadow:
await page.locator("custom-element").locator("internal:shadow=button").click();

モバイルとビューポートのテスト

// In test file
test.use({ viewport: { width: 375, height: 667 } });

// Device emulation
import { devices } from "@playwright/test";
test.use({ ...devices["iPhone 13"] });

// Or in playwright.config.ts projects
projects: [
  { name: "desktop", use: { viewport: { width: 1280, height: 720 } } },
  { name: "mobile", use: { ...devices["iPhone 13"] } },
]

デバッグとトレース

# Enable traces on failure (recommended for CI)
npx playwright test --trace on-first-retry

# View trace file
npx playwright show-trace trace.zip

# Debug mode - step through test
npx playwright test --debug

# Headed mode to see browser
npx playwright test --headed
// In playwright.config.ts
use: {
  trace: "on-first-retry",      // Capture trace on retry
  screenshot: "only-on-failure", // Screenshot on failure
  video: "retain-on-failure",    // Video on failure
}

CI/CD の設定

// playwright.config.ts for CI
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: process.env.CI
    ? [["html"], ["github"], ["junit", { outputFile: "results.xml" }]]
    : [["html"]],
  use: {
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
});
# GitHub Actions example
- name: Run Playwright tests
  run: npx playwright test
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: playwright-report/

テストの実行(部分実行を優先)

フィードバックを早く得るため、テストスイート全体ではなく、特定のテストを実行することを常に優先してください。

# Run ALL tests
npx playwright test

# ✅ PREFERRED: Run specific file
npx playwright test tests/login/login.spec.ts

# ✅ PREFERRED: Run specific folder
npx playwright test tests/login/

# ✅ PREFERRED: Run by test name (grep)
npx playwright test --grep "login"
npx playwright test --grep "user can sign up"

# ✅ PREFERRED: Run by tag
npx playwright test --grep "@critical"
npx playwright test --grep "@LOGIN-E2E-001"

# ✅ Run single test by line number
npx playwright test tests/login/login.spec.ts:42

# ✅ Run tests matching multiple patterns
npx playwright test --grep "login|signup"

# ✅ Exclude tests by pattern
npx playwright test --grep-invert "slow"
その他のコマンド
npx playwright test --debug            # Debug mode (step through)
npx playwright test --headed           # See browser
npx playwright test --trace on         # Enable tracing
npx playwright test --project=mobile   # Run specific project
npx playwright codegen                 # Record tests

部分文字列マッチング(堅牢性のために重要)

メンテナンスの負担を減らすため、完全一致ではなく、部分一致やパターンマッチングを常に使用してください。

// ❌ FRAGILE: Exact string matches break easily
await expect(page.getByText("Welcome to Our Application!")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit Form" })).toBeVisible();
await expect(notification).toHaveText("Your account has been created successfully.");

// ✅ ROBUST: Partial matches survive copy changes
await expect(page.getByText(/welcome/i)).toBeVisible();
await expect(page.getByRole("button", { name: /submit/i })).toBeVisible();
await expect(notification).toContainText("account");
await expect(notification).toContainText(/created/i);

// ✅ ROBUST: Use toContainText over toHaveText
await expect(page.locator(".message")).toContainText("success");

// ✅ ROBUST: Pattern matching for dynamic content
await expect(page.getByText(/order #\d+/i)).toBeVisible();
await expect(page.getByText(/\d+ items? in cart/i)).toBeVisible();

部分一致を使用する理由:

  • ✅ 軽微な文言の変更にも対応できます
  • ✅ ロケールをまたいで動作します(大文字と小文字を区別しません)
  • ✅ 空白の変更による影響を受けにくくなります
  • ✅ 長期的にメンテナンスしやすくなります
  • ❌ 完全一致は句読点、大文字・小文字、文言の微調整で失敗します

コメント