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 × TypeScriptチートシート|操作・検証・待機からAPI・認証まで

0
Posted at

入力欄を空にする、要素数を確認する、別タブを操作する。

Playwrightの書き方を、やりたいことから引けるようにまとめました。基本操作は表で、数行の手順が必要なものは短いコード例で載せています。

やりたいことから探す

やりたいこと 参照先
名前・ラベル・属性から要素を探す 1. 要素を探す
クリック・入力・選択する 2. 要素を操作する
表示・値・件数を確かめる 3. 結果を検証する
画面更新・通信を待つ 4. 必要な状態を待つ
別タブ・iframe・ダイアログを扱う 5. ページを操作する
ファイルや画像を扱う 6. ファイルとスクリーンショット
APIを呼ぶ・レスポンスを差し替える 7. APIとモック
ログイン済み状態を使う 8. 認証状態を再利用する
テストを実行・絞り込み・調査する 9. 実行とデバッグ
設定やテストの前後処理を確認する 10. 設定とテスト構成

コードの前提

対象はPlaywright Test(@playwright/test)+TypeScriptです。

以下の基本形で使います。短いコード例はそれぞれ独立した例です。

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

test('ホーム画面を表示する', async ({ page }) => {
  await page.goto('/');
  await expect(
    page.getByRole('heading', { name: 'ホーム', exact: true }),
  ).toBeVisible();
});

1. 要素を探す

名前・ラベル・属性で指定する

手がかり 書き方
ボタンのロールと名前 page.getByRole('button', { name: '保存', exact: true })
入力欄のラベル page.getByLabel('メールアドレス', { exact: true })
表示テキスト page.getByText('保存しました', { exact: true })
プレースホルダー page.getByPlaceholder('キーワードを入力')
画像の代替テキスト page.getByAltText('プロフィール画像')
title属性 page.getByTitle('詳細を表示')
テスト用の識別子 page.getByTestId('submit-button')
ID page.locator('#email')
class page.locator('.menu-item')
任意の属性 page.locator('[name="email"]')
XPath page.locator('//input[@name="email"]')

複数の候補から絞る

やりたいこと 書き方
文字を含む要素に絞る locator.filter({ hasText: '公開中' })
文字を含まない要素に絞る locator.filter({ hasNotText: '非公開' })
特定の子孫要素を持つものに絞る locator.filter({ has: page.getByText('商品A', { exact: true }) })
最初の要素 locator.first()
最後の要素 locator.last()
n+1番目の要素(0始まりのため右の例では3番目の要素) locator.nth(2)

同名ボタンは、親要素から範囲を限定できます。

const dialog = page.getByRole('dialog', { name: '設定' });
await dialog.getByRole('button', { name: '保存', exact: true }).click();

参照:Locators/Locator API

2. 要素を操作する

クリック・マウス操作

やりたいこと 書き方
クリック await locator.click();
ダブルクリック await locator.dblclick();
右クリック await locator.click({ button: 'right' });
ホバー await locator.hover();
フォーカスを当てる await locator.focus();
フォーカスを外す await locator.blur();
要素が見える位置までスクロール await locator.scrollIntoViewIfNeeded();
別の要素へドラッグ await source.dragTo(target);

sourceとtargetは、移動元・移動先のLocatorです。

文字入力・キーボード

やりたいこと 書き方
文字を入力・置換 await locator.fill('テスト');
入力欄を空にする await locator.clear();
Enterを押す await locator.press('Enter');
Tabを押す await locator.press('Tab');
入力欄の文字を全選択 await locator.press('ControlOrMeta+A');
1文字ずつキー入力する await locator.pressSequentially('hello');

チェックボックス・セレクトボックス

やりたいこと 書き方
チェックを入れる await locator.check();
チェックを外す await locator.uncheck();
チェック状態をbooleanで指定 await locator.setChecked(true);
選択肢のvalueで選ぶ await locator.selectOption({ value: 'tokyo' });
選択肢の表示名で選ぶ await locator.selectOption({ label: '東京都' });
複数選択する await locator.selectOption(['red', 'blue']);

参照:Actions/Locator API

3. 結果を検証する

以下は、自動再試行するアサーションです。awaitを付けます。

確認したいこと 書き方
表示されている await expect(locator).toBeVisible();
非表示、または存在しない await expect(locator).toBeHidden();
一致する要素がない await expect(locator).toHaveCount(0);
有効になっている await expect(locator).toBeEnabled();
無効になっている await expect(locator).toBeDisabled();
チェックされている await expect(locator).toBeChecked();
チェックされていない await expect(locator).not.toBeChecked();
テキスト全体が一致する await expect(locator).toHaveText('保存しました');
テキストを含む await expect(locator).toContainText('保存');
入力値が一致する await expect(locator).toHaveValue('山田太郎');
要素が3件ある await expect(locator).toHaveCount(3);
複数要素のテキストと順序が一致する await expect(locator).toHaveText(['東京', '大阪']);
属性の値が一致する await expect(locator).toHaveAttribute('aria-expanded', 'true');
ページタイトルが一致する await expect(page).toHaveTitle('ホーム');
URLが条件に一致する await expect(page).toHaveURL(/\/complete$/);
検証ではなく、値を取得する
取得したいもの 書き方
innerText const text = await locator.innerText();
textContent const text = await locator.textContent();
入力値 const value = await locator.inputValue();
属性値 const href = await locator.getAttribute('href');
要素数 const count = await locator.count();
複数要素のテキスト const texts = await locator.allTextContents();
一致するLocatorの配列 const items = await locator.all();
現在のURL const url = page.url();
ページタイトル const title = await page.title();
失敗しても、残りの検証を続ける
await expect.soft(page.getByLabel('氏名')).toHaveValue('山田太郎');
await expect.soft(page.getByLabel('メール')).toHaveValue('qa@example.com');

expect.soft()は失敗を記録しつつ後続の処理を続けます。失敗を無視するわけではなく、テスト全体は失敗になります。

参照:Assertions/LocatorAssertions/Locator API

4. 必要な状態を待つ

click()などは操作に必要な条件を自動で待ちます。

待ちたい状態 書き方
要素が表示される await expect(locator).toBeVisible();
文言が変わる await expect(locator).toHaveText('完了');
指定URLへ移動する await page.waitForURL('**/complete');
DOMに追加される await locator.waitFor({ state: 'attached' });
DOMから削除される await locator.waitFor({ state: 'detached' });

特定のAPIレスポンスを待つ

// 通信を発生させる操作より先に登録する。
const responsePromise = page.waitForResponse((response) =>
  new URL(response.url()).pathname === '/api/search'
  && response.request().method() === 'GET',
);

await page.getByRole('button', { name: '検索', exact: true }).click();

const response = await responsePromise;
expect(response.status()).toBe(200);

ステータスは待機条件に含めず、受信後に検証しています。500が返った場合も、待機タイムアウトではなくステータスの不一致として確認するためです。

同じAPIが複数回呼ばれる画面では、クエリや送信内容でも対象を絞ります。レスポンスを受け取った後の画面反映は、別途アサーションで確認します。

タイムアウトを個別に調整する
調整する対象 書き方
特定の操作 await locator.click({ timeout: 10_000 });
特定の検証 await expect(locator).toBeVisible({ timeout: 10_000 });
テスト1件の上限 test.setTimeout(60_000);

単位はミリ秒です。

参照:Auto-waiting/Page API/Timeouts

5. ページを操作する

やりたいこと 書き方
URLを開く await page.goto('https://example.com/');
再読み込み await page.reload();
前のページへ戻る await page.goBack();
次のページへ進む await page.goForward();
ページを閉じる await page.close();

クリックで開く別タブ

const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'ヘルプ', exact: true }).click();

const popup = await popupPromise;
await expect(popup).toHaveTitle('ヘルプ');

iframe内の要素

await page
  .frameLocator('#preview-frame')
  .getByRole('button', { name: '印刷', exact: true })
  .click();

JavaScriptの確認ダイアログ

page.once('dialog', async (dialog) => {
  await dialog.accept();
});

await page.getByRole('button', { name: '削除', exact: true }).click();

参照:Pages/Page API/Dialogs

6. ファイルとスクリーンショット

アップロードするファイルを選択する

ここでのlocatorは<input type="file">を指定します。

やりたいこと 書き方
1ファイルを選択 await locator.setInputFiles('fixtures/sample.pdf');
複数ファイルを選択 await locator.setInputFiles(['fixtures/a.csv', 'fixtures/b.csv']);
選択を解除 await locator.setInputFiles([]);

ダウンロードしたファイルを保存する

// 冒頭のtest・expectのimportを使用する。
test('CSVをダウンロードする', async ({ page }, testInfo) => {
  await page.goto('/downloads');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'CSV出力', exact: true }).click();

  const download = await downloadPromise;
  expect(download.suggestedFilename()).toMatch(/\.csv$/);
  await download.saveAs(testInfo.outputPath('report.csv'));
});

testInfo.outputPath()で、そのテスト用の保存先を使えます。

スクリーンショット・画像比較

やりたいこと 書き方
表示領域を保存 await page.screenshot({ path: testInfo.outputPath('page.png') });
ページ全体を保存 await page.screenshot({ path: testInfo.outputPath('full.png'), fullPage: true });
特定の要素だけ保存 await locator.screenshot({ path: testInfo.outputPath('element.png') });
基準画像と比較 await expect(page).toHaveScreenshot('home.png');

参照:Actions/Downloads/TestInfo/Visual comparisons

7. APIとモック

APIを直接呼ぶ

requestフィクスチャを受け取ります。以下は、レスポンスが{ items: [...] }という形式の場合の例です。

test('一覧APIが応答する', async ({ request }) => {
  const response = await request.get('/api/items');
  await expect(response).toBeOK();

  const body = await response.json();
  expect(body).toMatchObject({ items: expect.any(Array) });
});
やりたいこと 書き方
GETで取得 await request.get('/api/items');
クエリパラメータを渡す await request.get('/api/items', { params: { q: 'test' } });
JSONをPOST await request.post('/api/items', { data: { name: 'sample' } });
JSONをPATCH await request.patch('/api/items/123', { data: { name: 'updated' } });
DELETEで削除 await request.delete('/api/items/123');

APIの応答を差し替える

対象の通信が発生する前に登録します。この例はGET /api/itemsだけを差し替えます。

await page.route(
  (url) => url.pathname === '/api/items',
  async (route) => {
    if (route.request().method() !== 'GET') {
      await route.continue();
      return;
    }

    await route.fulfill({
      status: 200,
      json: { items: [] },
    });
  },
);

await page.goto('/items');

ハンドラー内の処理を変えると、次の状態も再現できます。

再現したい状態 ハンドラー内の書き方
HTTP 500 await route.fulfill({ status: 500, json: { message: 'Internal error' } });
通信自体の失敗 await route.abort('failed');
差し替えず実通信を続ける await route.continue();

HTTPエラーと通信失敗は別の状態です。 モックしたAPIについては、実バックエンドとの結合を確認していない点も区別します。

参照:API testing/APIRequestContext/APIResponseAssertions/Mock APIs/Route

8. 認証状態を再利用する

ログイン完了後に保存する

playwright/.auth/を作成し、ログインが完了したことを確認してから保存します。

await page.context().storageState({
  path: 'playwright/.auth/user.json',
});

保存済みファイルを読み込む

test.use()はテスト関数の外側に置きます。

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

test.use({ storageState: 'playwright/.auth/user.json' });

test('アカウント画面を開く', async ({ page }) => {
  await page.goto('/account');
  await expect(page.getByRole('heading', { name: 'アカウント' }))
    .toBeVisible();
});

保存用の処理を先に完了させ、ファイルが存在する状態で実行します。実行順を自動化する構成は、公式のsetupプロジェクトによる認証例を参照してください。

認証状態ファイルは、非公開リポジトリにもコミットしません。 Cookieなどの機密情報を含むため、CIの共有成果物にも混ぜないようにします。

# .gitignore
playwright/.auth/

参照:Authentication/BrowserContext

9. 実行とデバッグ

準備

# 新規プロジェクトを初期化する。
npm init playwright@latest

# 必要なブラウザをインストールする。
npx playwright install

# 使用中のバージョンを確認する。
npx playwright --version

実行・絞り込み

やりたいこと コマンド
全テストを実行 npx playwright test
ファイルを指定 npx playwright test tests/example.spec.ts
行番号を指定 npx playwright test tests/example.spec.ts:10
テスト名で絞る npx playwright test -g "検索"
プロジェクトを指定 npx playwright test --project=chromium
ブラウザを表示 npx playwright test --headed
UIモードで実行 npx playwright test --ui
Inspectorでデバッグ npx playwright test --debug
前回失敗したテストだけ実行 npx playwright test --last-failed
10回繰り返す npx playwright test --repeat-each=10
ワーカー数を1にする npx playwright test --workers=1
実行対象を一覧表示 npx playwright test --list
操作からコードを生成 npx playwright codegen http://localhost:3000

-gは正規表現、--projectは設定済みのプロジェクト名です。

レポート・トレース

# HTMLレポートを生成して実行する。
npx playwright test --reporter=html

# 作成済みのHTMLレポートを開く。
npx playwright show-report

# トレースを記録して実行する。
npx playwright test --trace on

# 保存されたトレースを開く。
npx playwright show-trace test-results/対象ディレクトリ/trace.zip

エラー別の確認ポイント

症状 まず確認すること
strict mode violation 複数の要素に一致していないか。範囲や名前で絞れるか
クリックでタイムアウト 要素の有無、disabled、オーバーレイ、アニメーション
アサーションでタイムアウト 期待値、APIエラー、画面更新のタイミング
レスポンス待機でタイムアウト 待機を登録した順序、URL、HTTPメソッド
モックが効かない 登録前に通信していないか。URL条件やService Workerの影響
単独では通り、並列だと落ちる 共有アカウント・共有データの干渉、同時負荷
CIでだけ落ちる ブラウザ、環境変数、データ、実行資源の違い

切り分けの入り口として、Trace ViewerのDOM・通信・ログと照らし合わせます。

npx playwright test tests/example.spec.ts \
  --repeat-each=10 --retries=0 --workers=1

これは単独実行での確認です。並列時だけ失敗する場合は、実際の並列条件とも比較します。

参照:Installation/Command line/Trace viewer/Network

10. 設定とテスト構成

playwright.config.ts

以下は設定例であり、デフォルト値の一覧ではありません。

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

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  forbidOnly: Boolean(process.env.CI),
  reporter: 'html',

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

baseURLを設定すると、page.goto('/login')のように書けます。この設定だけではアプリは起動しないため、テスト対象は別途起動しておきます。

設定 用途
timeout テスト1件の時間上限
expect.timeout アサーションの待機時間
forbidOnly test.only()の残存をエラーにする
use.trace トレースの記録方法
use.screenshot スクリーンショットの記録方法

トレースは、何回目の実行を残したいかで選びます。

設定値 記録対象
retain-on-failure 各実行を記録し、失敗した実行の分を残す
on-first-retry 最初の再試行を記録する。初回失敗そのものではない

再試行がなければ、on-first-retryでは記録されません。トレースやスクリーンショットの画面・通信情報にも注意し、閲覧範囲と保持期間を決めておきます。

テストをまとめる・前後の処理を入れる
やりたいこと 書き方
テストをグループ化 test.describe('検索', () => { /* テストを定義 */ });
各テストの前に処理 test.beforeEach(async ({ page }) => { /* 準備 */ });
各テストの後に処理 test.afterEach(async () => { /* 後片付け */ });
実行手順に名前を付ける await test.step('検索する', async () => { /* 操作 */ });

describeやフックはテスト関数の外側、test.step()はテスト関数の中で使います。

参照:Configuration/TestOptions/Playwright Test API/TypeScript

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?