X(Twitter) Zenn GitHub RSS 共有

Playwright

作成日時:2024-08-01
更新日時:2024-08-01

概要

Playwrightのメモ。

公式ドキュメントが充実しているため、この記事を読むよりはそっちを読んだ方がいい。

また、公式のベストプラクティスは必ず読むべき。
FixturesPage object modelsも読んでおくべき。

参考情報

この記事を読むより、下記の書籍/サイトを読んだ方がいい。

書籍

サイト

検証環境

Playwrightはいろいろな言語をサポートしているが、この記事ではJavaScript/TypeScriptを使用する。

今回作成したソース

https://github.com/takc-tech/lab/tree/main/Playwright

個人用のテンプレ。

基本操作

インストール

Installation | Playwrightを参照。

npm init playwright@latest

でインストール。
何個か選択肢が出てくるが、環境に応じた任意の値を選択。
下記のように出力される。

出力内容
>npm init playwright@latest
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
? Do you want to use TypeScript or JavaScript? ...
> TypeScript
 Do you want to use TypeScript or JavaScript? · TypeScript
 Where to put your end-to-end tests? · tests 
 Add a GitHub Actions workflow? (y/N) · false
 Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Initializing NPM project (npm init -y)…
Wrote to (作業ディレクトリ)\package.json:       

{
  "name": "playwright",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}


Installing Playwright Test (npm install --save-dev @playwright/test)…

added 3 packages, and audited 4 packages in 3s

found 0 vulnerabilities
Installing Types (npm install --save-dev @types/node)…

added 2 packages, and audited 6 packages in 1s

found 0 vulnerabilities
Writing playwright.config.ts.
Writing tests\example.spec.ts.
Writing tests-examples\demo-todo-app.spec.ts.
Writing package.json.
Downloading browsers (npx playwright install)…
 Success! Created a Playwright Test project at (作業ディレクトリ)

Inside that directory, you can run several commands:

  npx playwright test
    Runs the end-to-end tests.

  npx playwright test --ui
    Starts the interactive UI mode.

  npx playwright test --project=chromium
    Runs the tests only on Desktop Chrome.

  npx playwright test example
    Runs the tests in a specific file.

  npx playwright test --debug
    Runs the tests in debug mode.

  npx playwright codegen
    Auto generate tests with Codegen.

We suggest that you begin by typing:

    npx playwright test

And check out the following files:
  - .\tests\example.spec.ts - Example end-to-end test
  - .\tests-examples\demo-todo-app.spec.ts - Demo Todo App end-to-end tests
  - .\playwright.config.ts - Playwright Test configuration

Visit https://playwright.dev/docs/intro for more information.

Happy hacking! 🎭

テストコード作成

Writing tests | Playwrightを参照。

テストコードはplaywright.config.tsの「testDir」で指定したファルダに置く。
インストールしたらサンプルソースが生成されているので、それを参考に作る。

画面要素の取得方法はこれこれを参照。
:has-text("")は便利。(だがあまり使用すべきではない)
画面要素に対するいろいろなアクションはこれ参照。
Assertに関してはこれ参照。

実際に画面を操作してテストコードを書きたいならば、Generating tests | Playwrightを参照。

npx playwright codegen対象のサイト

と叩くとジェネレーターが起動する。
ジェネレーター上で対象のサイトにアクセスし、画面を操作すると、その内容がコードとして出力される。

beforeEach / afterEach / beforeAll / afterAll

Writing tests | Playwright

他のテストツールと同様に、テストの実行前後に処理を実行できる。
また、「test.describe」で囲むことにより、それらが動作するグループを限定できる。

注意:(before/after)ALLはworkerごとに実行される
複数workerが動く状態で、beforeAllを使用してテストデータの登録等を行うと、意図しない結果となる可能性がある。
テストデータの登録はグローバルセットアップなどを使用したほうがいいと思われる。

ファイル内のパラレル実行が有効の場合、workerの数だけbeforeAllが実行される。

サンプルコード
/**
 * All系がworkerごとに動いていることの確認
 */
import { test } from '@playwright/test';

test.describe('group', () => {
  test.beforeAll(async () => {
    console.log("group-beforeAll");
  });

  test.beforeEach(async ({ }) => {
    console.log("group-beforeEach");
  });

  test('main1', async ({ }) => {
    console.log("group-main1");
  });
  test('main2', async ({ }) => {
    console.log("group-main2");
  });
});
# 実行結果
# worker:1の場合
>npx playwright test --workers=1 sample.spec.ts

Running 3 tests using 1 worker
[setup] › setup.ts:3:6 › setup
@setup
[Google Chrome] › sample.spec.ts:15:7 › group › main1
group-beforeAll # 1回だけ
group-beforeEach
group-main1
[Google Chrome] › sample.spec.ts:18:7 › group › main2
group-beforeEach
group-main2

# worker:2の場合
>npx playwright test --workers=2 sample.spec.ts

Running 3 tests using 2 workers
[setup] › setup.ts:3:6 › setup
@setup
[Google Chrome] › sample.spec.ts:15:7 › group › main1
group-beforeAll # ここ
[Google Chrome] › sample.spec.ts:18:7 › group › main2
group-beforeAll # ここ
[Google Chrome] › sample.spec.ts:15:7 › group › main1
group-beforeEach
group-main1
[Google Chrome] › sample.spec.ts:18:7 › group › main2
group-beforeEach
group-main2

また、beforeEach / afterEach / beforeAll / afterAllをdescribeと組み合わせた際の実行順序は、下記のようになる。

サンプルコード
// sample.spec.ts
/**
 * 実行順序の検証
 */
import { test } from '@playwright/test';

test.beforeAll(async ({ }) => {
  console.log("@Before All");
});

test.beforeEach(async ({ }) => {
  console.log("@Before Each");
});

test.afterEach(async () => {
  console.log('@After Each');
});

test.afterAll(async () => {
  console.log('@After All');
});

test.describe('group', () => {
  test.beforeAll(async ({ }) => {
    console.log("@Group Before All");
  });

  test.beforeEach(async ({ }) => {
    console.log("@Group Before Each");
  });

  test.afterEach(async () => {
    console.log('@Group After Each');
  });

  test.afterAll(async () => {
    console.log('@Group After All');
  });

  test('main', async ({ }) => {
    console.log("@Group Main");
  });
});
# 実行結果
# worker:1の場合
>npx playwright test --workers=1 sample.spec.ts

Running 2 tests using 1 worker
[setup] › setup.ts:3:6 › setup
@setup
[Google Chrome] › sample.spec.ts:39:7 › group › main
@Before All
@Group Before All
@Before Each
@Group Before Each
@Group Main
@Group After Each
@After Each
@Group After All
@After All

テストを実行

下記を読む。

Running and debugging tests | Playwright
Command line | Playwright

以下、私がよく使うコマンド。

# 基本
npx playwright test
# ファイル単位
npx playwright test xxx.spec.ts
# ファイル内の特定のテスト
npx playwright test xxx.spec.ts:行番号
# 対象の名前を持ったテスト
npx playwright test -g "テスト名"
# 最後に失敗したテスト
npx playwright test --last-failed
# UIモードで起動
# 思った通りにテストが完了しなければこれで確認
npx playwright test --ui
# デバッグモード
npx playwright test --debug
# 並列処理をさせない
npx playwright test --workers=1

レポートを確認

npx playwright show-report

テストに失敗した場合は自動で表示される。
console.logの出力内容も保存されている。

設定系あれこれ

下記を読む。

Test configuration | Playwright
Test use options | Playwright

以下、設定系で詰まったことを個別にメモ。

画面サイズの設定

projectsごと/テストごとに設定できる。
優先順位は、test.describe直下 > ファイル直下 > projectsの順っぽい。

// playwright.config.ts
projects: [
  {
    name: 'Google Chrome'.
    use: {
      ...devices['Desktop Chrome'], channel: 'chrome',
      viewport: {
        width: 1920,
        height: 1080
      }
    }
  }
]
// xxx.spec.ts
// 直下かtest.describe内に記載
test.use({
  viewport: { width: 400, height: 300 },
});

ロケールの設定

デフォルトのままだとロケールが日本ではない。
デフォルトの設定でWikipediaのスクリーンショットを撮ると、英語になっていることが分かる。
playwright.config.tsやテストファイル内でロケールを設定する。

use: {
  locale: 'ja-JP',
},

タイムアウトの設定

Timeouts | Playwright

短いとテスト中にタイムアウトするし、長いとロケーターが見つからない場合にその時間分、無駄に待つことになる。
playwright.config.tsでタイムアウト時間を調整するよりも、遅いケースにだけ「test.slow()」や「test.setTimeout(ms)」を追加するのがいい感じか。

ブラウザの設定

Emulation | Playwright
Projects | Playwright

playwright.config.tsで検証する環境を設定する。
複数のブラウザやモバイル環境などを同時にテストできる。
検証が不要な物は消す。

操作系あれこれ

FixturesとPage object models

Fixtures | Playwright
Page object models | Playwright

Page object modelsは(以降、POMと表記)ページ上の操作をカプセル化するためのヘルパークラス。
たとえば、特定要素を操作する際にpage.locator()を毎回書くのではなく、その処理をヘルパークラス内に定義する。
そうすればlocatorの指定方法が変更しても、ヘルパークラス内のメソッドを修正するだけで済む。

POMを定義し、PlaywrightのFixturesを通して各テストで使用する。

ソースコード

POM定義。

// SamplePage.ts
import type { Page, Locator, TestInfo } from '@playwright/test';

export default class SamplePage {

  readonly page: Page;
  readonly testInfo: TestInfo;
  readonly button: Locator;
  readonly textBox: Locator;

  constructor(page: Page, testInfo: TestInfo) {
    this.page = page;
    this.testInfo = testInfo;
    this.textBox = page.getByRole('textbox');
    this.button = page.getByRole('button', { name: 'クリック' });
  }

  /**
   * 試験対象のページに移動
   */
  async goto() {
    await this.page.goto('https://suikentsukai.com/sample.html');
  }

  /**
   * ボタンをクリック
   */
  async clickButton() {
    await this.button.click();
  }

  /**
   * テキストボックスに値を設定
   */
  async setText(text: string) {
    await this.textBox.fill(text);
  }

  /**
   * 現在実行しているテストのタイトルを取得
   * @returns 
   */
  getTestTitle() {
    return this.testInfo.title;
  }

  /**
   * スクリーンショットを取得
   * @param suffix接尾辞
   */
  async screenshot(suffix: string) {
    const path = "./img/" + this.testInfo.title + suffix + ".png";
    await this.page.screenshot({ path });
  }
}

Fixtures定義。

// fixtures.ts
import { test as base } from '@playwright/test';
import SamplePage from '@/pages/SamplePage';

export const test = base.extend({
  // テストケースごとに実行される
  samplePage: async ({ page }, use, testInfo) => {

    // 事前処理
    // POMの生成やログイン処理、テストデータ登録など
    const samplePage = new SamplePage(page, testInfo);
    console.log("@fixture:setup samplePage");

    // テストケースにPOMを渡す
    await use(samplePage);

    // 事後処理
    // データの削除やブラウザの状態のリセットなど
    console.log("@fixture:teardown samplePage");

  }
});
export { expect } from '@playwright/test';

テストケースで使用する。

// sample.spec.ts
// import { test, expect } from '@playwright/test';
// fromを変えないと、当然samplePageの未定義エラーが出る。
import { test } from '@/fixtures';

test.describe('test group', () => {
  test('test', async ({ samplePage }) => {
    await samplePage.goto();
    await samplePage.screenshot("1_初期表示");
    await samplePage.setText(samplePage.getTestTitle());
    await samplePage.screenshot("2_テストタイトルをテキストボックスに入力後");
    await samplePage.clickButton();
    await samplePage.screenshot("3_ボタン押下後");
  });
});

使用可能なFixtures

公式では一部のビルトインフィクスチャが紹介されているが、他にもあるらしい。
例えば、playwright.config.tsのuseで定義している内容やtestInfoは取得できた。
testInfoは実行中のテストに関する情報が含まれている。

test('sample', async ({ samplePage, viewport, isMobile }, testInfo) => {
    console.log(`viewport:${JSON.stringify(viewport)}`);
    console.log(`isMobile:${isMobile}`);
    console.log(`testInfo.title:${testInfo.title}`);
});

待ち処理

画面を操作する際、Playwrightは操作対象の要素が操作可能になるまで待ってくれる。
Auto-waiting | Playwright

それ以外は待ってくれない。
例えば下記のコード。

// 「クリックするとFetchで情報を取得し、レスポンスを画面に反映するボタン」をクリックする。
await page.getByRole('button', { name: "クリック" }).click();
// レスポンスの返却と画面への反映は待ってくれない
// レスポンスが遅ければ、画面反映前の内容でスクリーンショットを撮ってしまう。
await page.screenshot({ path: "./click.png" });

こういった処理を待ちたい場合、待機系のメソッドを使う。

test('sleep', async ({ page }) => {

  await page.goto("http://localhost/sample.html");

  // レスポンスを待つ
  const responsePromise = page.waitForRequest('http://localhost/sleep.php');
  await page.getByRole('button', { name: "クリック" }).click();
  const response = await responsePromise;

  // もしくは画面に反映されるまで待機する(今回のケースはこれ使用すべき)
  await page.waitForFunction(() => {
    // レスポンスを突っ込む要素の文字数が0じゃなくなるのを待つとか。
    const element = document.querySelector('#target');
    return element && element.innerText.length != 0;
  }, { timeout: 5000 });
  await page.screenshot({ path: "./click.png" });
});

CSP起因のエラー

上記のテストケースを検証していたところ、テストが失敗し、下記のエラーログが表示された。

Error: page.waitForFunction: EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: (CSPに設定してたURL一覧)

テスト対象サーバーにCSP(Content-Security-Policy)を設定したのが原因っぽい。
サーバーからCSP設定をいったん消すか、設定でbypassCSPをtrueにしておく。

スクリーンショット

Screenshots | Playwright

基本

// ページ
await page.screenshot({path: '保存先'});
// 要素
await page.locator('.header').screenshot({ path: '保存先' });

縦長画面のスクリーンショット

スクロールが発生している場合、fullPageオプションをつければいい。

await page.screenshot({path: '保存先', fullPage: true});

横長画面のスクリーンショット

コードでスクロールさせる。

await page.getByTestId('scrolling-container').evaluate(e => e.scrollLeft += 100);

容量削減

jpegで品質を下げれば、スクリーンショットのサイズは減らせる。

await page.screenshot({path: '保存先.jpeg', quality: 50});

DB登録

いろいろとやり方がある。
下記はPostgreSQLでpsqlコマンドを使用して、SQLを実行するサンプル。

const { execSync } = require('child_process');
await execSync(`psql -f SQLファイルのパス -U xxx -d xxx -h xxx`);
// パスワードを聞かれる場合
await execSync(`SET PGPASSWORD=パスワード&&psql -f SQLファイルのパス -U xxx -d xxx -h xxx`);

ファイル直下、またはtest.describe直下のbeforeAll内に定義しておくべきなのだろうか。
workerが複数だとその分、実行されてしまうけども。

環境変数の使用

Parameterize tests | Playwright

dotenvを使用する。

# インストール
npm install dotenv
# .env
SAMPLE='SAMPLE_DATA'
// playwright.config.ts
const dotenv = require('dotenv');
// 複数の設定ファイルを指定可能
// 基本的には先に記述したものが優先される。
// 後に記述した内容で上書きしたい場合は、overrideオプションを使用
// https://www.npmjs.com/package/dotenv?activeTab=readme#-examples
dotenv.config({ path: [".env"] });
// xxx.spec.ts
console.log(process.env.SAMPLE);
// output => SAMPLE_DATA

制御文の使用

当然の如く、ifやforが使える。
パラメタライズも配列のforEachを使って実装したり。

if (condition) {
  test('something', async ({ page }) => {
    console.log('@something');
  });
}

// 公式のParameterized Testsより
[
  { name: 'Alice', expected: 'Hello, Alice!' },
  { name: 'Bob', expected: 'Hello, Bob!' },
  { name: 'Charlie', expected: 'Hello, Charlie!' },
].forEach(({ name, expected }) => {
  // You can also do it with test.describe() or with multiple tests as long the test name is unique.
  test(`testing with ${name}`, async ({ page }) => {
    await page.goto(`https://example.com/greet?name=${name}`);
    await expect(page.getByRole('heading')).toHaveText(expected);
  });
});

グローバルセットアップ

Global setup and teardown | Playwright参照。

projects: [
    {
      name: 'setup',
      testMatch: /setup\.ts/,
      teardown: 'cleanup',
    },
    {
      name: 'cleanup',
      testMatch: /teardown\.ts/,
    },
    {
      name: 'Google Chrome',
      use: {
        ...devices['Desktop Chrome'], channel: 'chrome',
      },
      dependencies: ['setup'],
    },
]

多言語対応のサイトに対する画面要素指定

多言語対応で条件により画面要素の文言が変わったらどうなるのか。
文言をベースに画面要素を指定していたら、テストが機能しなくなる。

対応するなら下記のいずれかか。

一番最後の方法に関しては下記の工程を踏む。

  1. 環境ごとの文言をJSONで保持。デフォルトと各環境分のファイルを作成。
  2. 環境変数の内容に応じて、対象のJSONから文言を取得。
  3. 2で取得できなければデフォルトのJSONから取得。(フォールバック)
  4. 取得した文言を使用して画面要素を指定する。
以下、実装。

JSONを読み込めるように設定。

tsconfig.json
"resolveJsonModule": true

デフォルトの文言ファイルを作成。

default.json
{
  "label": {
    "sample1": "label_1",
    "sample2": "label_2"
  }
}

環境ごと(ここではcustomのみ)の文言ファイルを作成。

custom.json
{
  "label": {
    "sample1": "custom_label_1"
  }
}

JSONを読み込むクラスを作成。

export default class I18n {

  /** デフォルト */
  private defaultDefinition;
  /** カスタム */
  private customDefinition;

  constructor() {
    this.defaultDefinition = require("./default.json");
    try {
      // 環境変数から対象となるcustomIdを取得
      const customId = process.env.CUSTOM_ID;
      // 対象のJSONを読み込む
      this.customDefinition = require(`./${customId}.json`);
    } catch (e) {
      // 対象のJSONが無かったらデフォルトを設定
      this.customDefinition = this.defaultDefinition;
    }
  }

  /**
   * 文言を取得する。
   * @param key文言に対応するキー
   * 例)label.sample
   * @returns keyに対応する文言
   */
  get(key: string): string {
    // customに無ければdefaultにフォールバック
    return this.search(this.customDefinition, key)
      || this.search(this.defaultDefinition, key);
  }

  search(target, key: string) {
    try {
      const keys: string[] = key.split(".");
      let obj = target[keys[0]];
      for (let i = 1; i < keys.length; i++) {
        obj = obj[keys[i]];
      }
      return obj;
    } catch (e) {
      return null;
    }
  }
}

テストファイルでの使用方法。

// xxx.spec.ts
import I18n from "./I18n"
const i18n = new I18n();

// process.env.CUSTOM_IDがcustomならば、「custom_label_1」で検索される。
// process.env.CUSTOM_IDがcustomではないならば、「label_1」で検索される。
await page.getByRole("button", name: i18n.get("label.sample1"));

// 「label_2」で検索される。
// customのjsonには定義がないため、defaultにフォールバックされる。
await page.getByRole("button", name: i18n.get("label.sample2"));

HTMLを取得する

const content = await page.content();

別件で作ったSolidJS製のSPAサイトを対象に実行してみたら、レンダリング後のHTMLが取得できた。

a11y

a11yのテストも行える。

@axe-core/playwright - npm
Accessibility testing | Playwright

その他公式記事へのリンク

高速化/効率化あれこれ