0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Playwright E2E で学んだ 8 つの罠 — sleep / セレクタ / Cookie / Dialog focus の対策

0
Posted at

この記事は約 5 分で読めます。

筆者プロフィール: ソフトウェアエンジニア。「知った気にならない。いつまでも学び続ける」を信条に、業務と個人開発の両輪で技術を磨いています。AI 駆動開発で複数の個人開発アプリを構築・運用中。
👉 ポートフォリオ: 筆者ホームページ

Playwright で E2E を書くと、フレーキー (不安定) な失敗 に悩まされます。本記事では、運用中の SaaS 「たすきば Knowledge Relay」 で蓄積した E2E_LESSONS (3,800 行) から、特に踏みやすい罠 8 点を整理します。

サービスの機能紹介・画面イメージ・コンセプトは公式プロダクトページをご覧ください。
👉 たすきば Knowledge Relay — 公式プロダクトページ

罠 1: sleep ベースの待機

// ✗ NG
await page.click('button');
await page.waitForTimeout(2000);
await expect(page.locator('div.result')).toBeVisible();

環境によって遅延が異なる。フレーキー

// ✓ OK: 条件ベース待機
await page.click('button');
await expect(page.locator('div.result')).toBeVisible();

Playwright の toBeVisible()内部で条件待機。明示的な sleep は不要。


罠 2: セレクタが脆い

// ✗ NG: CSS セレクタが UI 変更で壊れる
await page.click('div > button:nth-child(3)');

// ✗ NG: text に依存
await page.click('button:has-text("保存する")');

// ✓ OK: data-testid で安定
await page.click('[data-testid="save-button"]');

data-testid を本番コードに付ける。UI 変更でも壊れない


罠 3: テスト間でデータが残る

// ✓ OK: 各テストで独立した状態
test.beforeEach(async () => {
  await resetDatabase();
});

各テストが独立、順序に依存しない


罠 4: 並列実行時の競合

// ✗ NG: 並列で DB が競合
test.describe.parallel(...)

// ✓ OK: worker ごとに分離した DB
test.describe.serial(...)
// または storageState で worker isolation

Playwright の worker isolation で、各 worker が独立した状態


罠 5: localStorage / Cookie の前提

// ✓ OK: 各テストで明示的にログイン
test.beforeEach(async ({ context }) => {
  await context.clearCookies();
  await loginAs('test@example.com');
});

Cookie をクリアしてから始める


罠 6: モーダル / ダイアログの focus 競合

// ✗ NG: dialog が開く前に click
await page.click('button:has-text("削除")');
await page.click('button:has-text("確認")');

// ✓ OK: dialog 内のボタンを getByRole 経由で
await page.click('button:has-text("削除")');
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: '確認' }).click();

dialog 内のボタンを getByRole('dialog') 経由で指定


罠 7: 非同期データの待機不足

// ✓ OK: ローディング完了を待つ
await page.goto('/projects');
await page.waitForLoadState('networkidle');
await expect(page.locator('text=テストプロジェクト')).toBeVisible();

networkidle で全ネットワーク完了を待つ


罠 8: ローカル / CI で挙動が違う

原因 内容
フォントレンダリング Linux と macOS で差
タイムゾーン UTC vs Asia/Tokyo
環境変数 違うシークレット
DB 初期状態 リセット漏れ

対策:

□ ローカルでも Docker で本番に近い環境
□ CI で fail したら、同じ環境で再現を試みる
□ 環境差分を最小化する設計

E2E 実行時間の管理

E2E は時間がかかる。全テスト 15 分以上だと CI 全体が遅い。

対策 内容
並列実行 Playwright workers
重要シナリオに絞る 全 page をカバーしない
nightly ジョブ 全シナリオは夜間に
PR では主要シナリオのみ 高速化

CI 体験を維持するため、E2E は戦略的に書く


Page Object Pattern の活用

class ProjectsPage {
  constructor(public page: Page) {}

  async goto() {
    await this.page.goto('/projects');
  }

  async create(name: string) {
    await this.page.click('button:has-text("新規")');
    await this.page.fill('input[name=name]', name);
    await this.page.click('button:has-text("作成")');
  }
}

// テスト
const projects = new ProjectsPage(page);
await projects.goto();
await projects.create('テスト');

UI 変更時の修正範囲を Page Object に閉じ込められます


Trace ファイルでデバッグ

E2E が fail したとき、Playwright Trace でデバッグ。

// playwright.config.ts
use: {
  trace: 'on-first-retry',
}
trace に含まれるもの 用途
スクリーンショット 視覚的に確認
DOM スナップショット 構造の確認
ネットワークログ API リクエスト
console エラー エラー詳細

CI fail 時に確認すれば、原因が即特定


おわりに

対策
sleep ベース待機 条件ベース待機
脆いセレクタ data-testid
テスト間データ残り beforeEach でリセット
並列競合 worker isolation
Cookie 前提 clearCookies
Dialog focus 競合 getByRole('dialog')
非同期待機不足 waitForLoadState
環境差分 Docker + 環境変数管理

E2E の罠は無数だが、構造化することで対処可能 です。

本記事の罠と対策は、運用中の SaaS 「たすきば Knowledge Relay」 で蓄積したものです。
👉 たすきば Knowledge Relay — 公式プロダクトページ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?