Playwright実践ガイド：基本設定と主要メソッドの使い方

## はじめに
本記事は、Playwrightを用いたWebアプリのE2Eテスト自動化を通して、よく利用するメソッドや覚えておくと便利なポイントについてまとめたものです。

※より詳細な使い方は[公式ドキュメント](https://playwright.dev/docs/intro)をご参照ください。

## 目次
- [導入と実行](#導入と実行)
- [基本設定](#基本設定)
- [並列処理](#並列処理)
- [前処理と後処理](#前処理と後処理)
- [ロケーター](#ロケーター)
- [アクション](#アクション)
- [アサーション](#アサーション)
- [テストコード例](#テストコード例)

## 導入と実行
Playwrightの導入方法や実行方法については、既に多くの事例が公開されていますので、そちらの記事を参考にしてください。  
[Playwright の導入から各種実行方法について試してみた](https://qiita.com/ore88ore/items/5c9f93b4158c30ccd1bd)

## 基本設定
Playwrightの設定ファイル（`playwright.config.ts`）を用いることで、テスト実行に必要なオプションを一元管理できます。

```ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://example.com',
    headless: true,
    viewport: { width: 1280, height: 720 },
  },

  projects: [
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Google Chrome',
      use: { ...devices['Desktop Chrome'], channel: 'chrome' },
    },
  ],
});
```

| オプション  | 説明 |
|-------------|------|
| **testDir** | テストファイルが格納されているディレクトリを指定 |
| **fullyParallel** | テストファイル内のテストを並列で実行するかどうかの設定<br>（デフォルト: `true`） |
| **forbidOnly** | CI環境で `test.only` や `test.describe.only` が残っている場合にテストを失敗させる設定<br>（デフォルト: `true`）<br>・test.only: 特定のテストケースのみを実行するメソッド<br>・test.describe: テストケースをグループ化するメソッド<br>・test.describe.only: グループ内のテストケースのみを実行するメソッド|
| **retries** | 各テストのリトライ回数の最大値 |
| **workers** | 並列実行に使用するworkerプロセスの最大同時実行数
| **reporter** | テスト結果のレポート形式の設定<br>詳細は[Reoprters](https://playwright.dev/docs/test-reporters)参照|
| **use** | グローバルなテストオプションを指定<br>例: `baseURL`, `headless`, `viewport` など |
| **projects** | 複数のテスト環境やブラウザをグループ化して設定。<br>各プロジェクトごとに個別のオプションが可能。<br>プロパティの例:<br> **name**: プロジェクト名<br> **use**: プロジェクト単位のオプション<br> **devices**: 実行するデバイスまたはブラウザの種類<br>**channel**: ブラウザのリリースチャンネル（例: chrome, edge）<br> **dependencies**: 実行前に必要なプロジェクトのリスト<br>|

詳細については、[Test Configuration](https://playwright.dev/docs/test-configuration)をご参照ください。

## 並列処理
Playwrightでは、テストファイル単位での並列実行が可能です。

並列実行の最大数は、設定ファイルの workers オプションで制御可能です。

また、テストファイル内のテストの並列実行についても、設定ファイル（`playwright.config.ts`）の「fullyParallel」 によって一括制御できます。

一括でなく各テストファイルで並列実行や順次実行を制御したい場合は、テストファイル内でtest.describe.configure({ mode }) を設定することで制御可能です。

| mode      | 特徴                                                           |
|-----------|----------------------------------------------------------------|
| **parallel**  | ファイル内のテストを並列に実行                     |
| **default**   | テストを順次実行（失敗しても後続テストは実行）                   |
| **serial**    | テストを順次実行し、途中で失敗した場合、以降のテストをスキップする |

```ts
test.describe.configure({ mode: 'default' });
test.describe('順次実行テスト例', () => {
  test('テストケース1', async ({ page }) => {
    // 何らかの処理
  });
  test('テストケース2', async ({ page }) => {
    // 何らかの処理
  });
});
```

詳細については、[Parallelism](https://playwright.dev/docs/test-parallel)をご参照ください。

## 前処理と後処理

テストの実行前後に処理を仕込むため、Playwrightでは以下のメソッドが用意されています。
各メソッドは、テストの実行範囲（グローバルまたはdescribe内）によって適用されるタイミングが異なります。
### beforeEach / afterEach

- 実行タイミング: 各テストケースの直前・直後に実行されます
- 適用範囲: テストファイル全体または test.describe() 内に限定して設定可能

| スコープ             | 実行タイミング                                      |
|----------------------|---------------------------------------------------|
| テストファイル全体       | ファイル内の各テスト前後             |
| test.describe()内  | グループ内の各テスト前後        |

### beforeAll / afterAll

- 実行タイミング: 各テストケースの直前・直後に実行されます
- 適用範囲: テストファイル全体または test.describe() 内に限定して設定可能

| スコープ             | 実行タイミング                                      |
|----------------------|---------------------------------------------------|
| テストファイル全体       | ファイル内のテストの最初と最後に一度だけ実行　|
| test.describe()内  |グループ内のテストの最初と最後に一度だけ実行|

```ts
import { test, expect } from '@playwright/test';

// グローバルな前処理
test.beforeAll(async () => {
  console.log('【全体前処理】 テスト開始前に必要なセットアップを実施');
});

// グローバルな後処理
test.afterAll(async () => {
  console.log('【全体後処理】 テスト終了後のクリーンアップ処理');
});

// 各テストケース前の処理
test.beforeEach(async ({ page }) => {
  console.log('【各テスト前処理】 テスト前に毎回実施する処理');
});

// 各テストケース後の処理
test.afterEach(async ({ page }) => {
  console.log('【各テスト後処理】 テスト後に毎回実施する処理');
});

test('サンプルテスト1', async () => {
  console.log('テスト1実行');
});

test('サンプルテスト2', async () => {
  console.log('テスト2実行');
});
```

:::note warn
beforeAll / afterAllは、workerプロセス単位で1回だけ実行されます。
そのためテストファイル内のテストを複数のworkerプロセスを用いて並列実行する場合は、それぞれのworkerプロセスごとに実行されるので注意が必要です。
:::

## ロケーター
ロケーターは、ページ上の要素を簡潔かつ正確に指定するための機能です。

- getByRole()
アクセシビリティ上のロール（例: button, textbox）に基づいて要素を特定・取得します

```ts
await page.getByRole('button', { name: '送信' });
```

- getByLabel()
フォームのラベルに基づいて要素を特定・取得します
```ts
await page.getByLabel('ユーザー名');
```

- getByPlaceholder()
placeholder属性を利用して要素を取得します
```ts
await page.getByPlaceholder('パスワード');
```

- getByText()
要素に含まれるテキストを取得します。
また、部分文字列、完全な文字列、正規表現でマッチさせることが可能です。
```ts
await page.getByText('ログインしました');
```

詳細については、[Locators](https://playwright.dev/docs/locators)をご参照ください。

## アクション
アクションは、ロケーターで取得した要素に対してユーザー操作（クリック、入力、ファイルアップロードなど）をシミュレートするための機能です。

- fill()
テキスト入力を行います。複数回呼び出すと、最後の入力内容が反映されます。
```ts
await page.getByPlaceholder('携帯電話番号/メールアドレス/ID').fill('test@example.com');
```

- click()
シングルクリックを行います
```ts
await page.getByRole('button', { name: 'ログイン' }).click();
```

- check() / uncheck()
チェックボックスやラジオボタンの選択・解除を行います。
```ts
await page.getByLabel('同意する').check();
```

- setInputFiles()
ファイルのアップロード操作を行います
```ts
await page.getByLabel('ファイル').setInputFiles(filePath);
```

詳細については、[Actions](https://playwright.dev/docs/input)をご参照ください。

## アサーション
アサーションは、操作結果が期待通りかどうかを検証するための機能です。

アサーションには、大きく分けて自動リトライするものと自動リトライしないものがあります。
ページの読み込み遅延や非同期な処理がある場合でもより安定したテストが実現できるため、前者の自動リトライするアサーションの使用が推奨されています。

ここでは自動リトライするアサーションの中から、実際に使っているものを紹介します。

その他のアサーションについては、[Assertions](https://playwright.dev/docs/test-assertions)をご参照ください。
***
- toBeVisible()
要素が画面上に見えていることを確認します。
```ts
// 「ログイン」と書かれた見出しが画面に表示されていることを確認
await expect(page.getByRole('heading', { name: 'ログイン' })).toBeVisible();
```

- toHaveText()
要素内のテキストが、指定した文字列と**完全に一致するか**を確認します
```ts
// 商品説明セクション（role: "region", accessible name: "商品説明"）のテキストに「割引中」というキーワードが含まれていることを確認
await expect(page.getByRole('region', { name: '商品説明' })).toContainText('割引中');
```

- toContainText()
要素内のテキストが、指定の文字列を**部分的に含んでいるか**を確認します
```ts
// 例: 「ログイン」と書かれた見出しが画面に表示されていることを確認
await expect(page.getByRole('heading', { name: 'ログイン' })).toBeVisible();
```

- toBeChecked()   
チェックボックスやラジオボタンが選択状態であることを確認します。
```ts
// 「利用規約に同意する」チェックボックスがチェックされていることを確認
await expect(page.getByRole('checkbox', { name: '利用規約に同意する' })).toBeChecked();
```

- toBeEnabled()
ボタンや入力欄などが活性状態であることを確認します。
```ts
// 「送信」ボタンがクリック可能な状態であることを確認
await expect(page.getByRole('button', { name: '送信' })).toBeEnabled();
```

- toHaveValue()
フォーム要素の持つ値が期待通りであることを確認します。
```ts
// テキストボックスに「ユーザー名」が入力されていることを確認
await expect(page.getByRole('textbox', { name: 'ユーザー名' })).toHaveValue('testuser');
```

:::note info
expect(...).**not**.toHaveText()のように、否定の検証も可能です。
また、toBeEnabled()とtoBeVisible()には、対になる否定形（それぞれ toBeDisabled(), toBeHidden()）が用意されています。
:::


## テストコード例
今までに紹介したものを可能な限り、一つのテストスイートにまとめたものです。

```ts
import { test, expect } from '@playwright/test';

// グローバルな前処理・後処理（各worker単位で実行される）
test.beforeAll(async () => {
  console.log('【全体前処理】 テスト開始前に必要なセットアップを実施');
});

test.afterAll(async () => {
  console.log('【全体後処理】 テスト終了後のクリーンアップ処理');
});

// test.describe.configureでグループ内の実行モードを設定（順次実行例）
test.describe.configure({ mode: 'default' });

test.describe('E2Eテストサンプル', () => {
  // 各テスト前後の処理
  test.beforeEach(async ({ page }) => {
    // 前処理例：ログイン処理
    await page.goto('https://example.com/login');
    await page.getByPlaceholder('ユーザー名').fill('testuser');
    await page.getByPlaceholder('パスワード').fill('password123');
    await page.getByRole('button', { name: 'ログイン' }).click();
    // ログイン成功を検証
    await expect(page.getByRole('heading', { name: 'ダッシュボード' })).toBeVisible();
  });

  test.afterEach(async ({ page }) => {
    // 後処理例：ログアウト処理
    await page.getByRole('button', { name: 'ログアウト' }).click();
    await expect(page.getByText('ログアウトしました')).toBeVisible();
  });

  // 1. お問い合わせフォームの送信テスト
  test('お問い合わせフォームの送信テスト', async ({ page }) => {
    await page.goto('https://example.com/contact');

    // 各フィールドへの入力
    await page.getByLabel('お名前').fill('テストユーザー');
    await page.getByPlaceholder('メールアドレス').fill('test@example.com');
    await page.getByPlaceholder('お問い合わせ内容').fill('内容の確認テストです。');

    // 入力内容の検証
    await expect(page.getByLabel('お名前')).toHaveValue('テストユーザー');

    // 送信ボタンがクリック可能な状態であることを検証
    await expect(page.getByRole('button', { name: '送信' })).toBeEnabled();

    // 送信ボタンをクリック（アクション）
    await page.getByRole('button', { name: '送信' }).click();

    // 送信結果の検証
    // 完全一致を検証する場合：toHaveText
    await expect(page.getByRole('alert')).toHaveText('お問い合わせありがとうございます');
    // 部分一致を検証する場合：toContainText
    await expect(page.getByRole('alert')).toContainText('ありがとうございます');
  });

  // 2. ファイルアップロードのテスト
  test('ファイルアップロードのテスト', async ({ page }) => {
    await page.goto('https://example.com/upload');

    // ファイルパス
    const filePath = 'tests/fixtures/sample.txt';
    // ファイルのアップロード
    await page.getByLabel('アップロードするファイル').setInputFiles(filePath);

    // アップロードボタンをクリック
    await page.getByRole('button', { name: 'アップロード' }).click();

    // アップロード成功のメッセージを検証
    await expect(page.getByText('アップロード成功')).toBeVisible();
  });

  // 3. チェックボックス・ラジオボタンのテスト
  test('チェックボックスとラジオボタンのテスト', async ({ page }) => {
    await page.goto('https://example.com/preferences');

    // チェックボックスの選択
    await page.getByLabel('メール通知を受け取る').check();
    // チェックボックスが選択されていることを検証
    await expect(page.getByRole('checkbox', { name: 'メール通知を受け取る' })).toBeChecked();

    // ラジオボタンの検証
    // まず、ラジオボタンが有効であることを確認
    await expect(page.getByRole('radio', { name: 'オプション1' })).toBeEnabled();
    // クリックして選択
    await page.getByRole('radio', { name: 'オプション1' }).click();
    // 選択状態の確認
    await expect(page.getByRole('radio', { name: 'オプション1' })).toBeChecked();
  });
});

```