Playwright
作成日時:2024-08-01
更新日時:2024-08-01
概要
Playwrightのメモ。
公式ドキュメントが充実しているため、この記事を読むよりはそっちを読んだ方がいい。
また、公式のベストプラクティスは必ず読むべき。
FixturesとPage object modelsも読んでおくべき。
参考情報
この記事を読むより、下記の書籍/サイトを読んだ方がいい。
書籍
サイト
- Playwrightのインストール方法と使い方 | フューチャー技術ブログ
- Playwrightを使いこなすためのベストプラクティス #テスト自動化 - Qiita
- Playwrightチートシート - Qiita
- Playwrightの並列実行とテストファイルの分け方まとめ方
検証環境
- Playwright v1.45.3
- Node.js v20.12.0
- Windows 10 22H2 19045.4651
Playwrightはいろいろな言語をサポートしているが、この記事ではJavaScript/TypeScriptを使用する。
今回作成したソース
https://github.com/takc-tech/lab/tree/main/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
他のテストツールと同様に、テストの実行前後に処理を実行できる。
また、「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
- テストは基本的に並列に実行される。
- テストファイル内でテストが並列に行われるかは設定次第。
- playwright.config.tsの「fullyParallel」
- 直列にした場合、定義順に実行されるらしい。
- playwright.config.tsの「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',
},
タイムアウトの設定
短いとテスト中にタイムアウトするし、長いとロケーターが見つからない場合にその時間分、無駄に待つことになる。
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" });
こういった処理を待ちたい場合、待機系のメソッドを使う。
- Pageの待機系メソッド
- waitForEvent
- waitForFunction
- waitForLoadState
- waitForRequest
- waitForResponse
- waitForURL
- 非推奨メソッド
- waitForNavigation
- waitForSelector
- waitForTimeout
- →時間を指定して待機するのは基本的にNG。無駄に待つことになったり、処理が未完了だったりする。
- Locatorの待機系メソッド
- waitFor
- 対象の要素が条件(表示/非表示/存在/非存在など)を満たすまで待つ。
- waitFor
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にしておく。
スクリーンショット
基本
// ページ
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'],
},
]
多言語対応のサイトに対する画面要素指定
多言語対応で条件により画面要素の文言が変わったらどうなるのか。
文言をベースに画面要素を指定していたら、テストが機能しなくなる。
対応するなら下記のいずれかか。
- 文言ではなく、別の指定方法に変更する。
- テストIDを仕込んで、それをキーに画面要素を指定すればいいが、推奨されていない。
- CSSやXPathも同様。
- 対象の文言用に新しくテストを作る。
- テストケースも多言語対応にする。
一番最後の方法に関しては下記の工程を踏む。
- 環境ごとの文言をJSONで保持。デフォルトと各環境分のファイルを作成。
- 環境変数の内容に応じて、対象のJSONから文言を取得。
- 2で取得できなければデフォルトのJSONから取得。(フォールバック)
- 取得した文言を使用して画面要素を指定する。
以下、実装。
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
その他公式記事へのリンク
- テストごとにログイン処理を走らせるから時間がかかる時
- ページ上でスクリプトを実施し、結果を受け取りたい時
- ページ上のイベントを取得したい時
- 1つのテストで複数の環境を操作したい時
- ビジュアルレグレッションテストをしたい時
- ダウンロード処理
- アップロード処理
- コンポーネントテストも実験中らしい
高速化/効率化あれこれ
- Playwrightに限らず、自動テストはFIRST原則に従って作成する
- 並列実行を活用する
- テストの分離と独立性を確保する
- テストファイル内の並列実行を無効化して、直列で実行しなければならないテストは同一ファイルにまとめるとか
- ヘッドレスモードを使用する
- ページオブジェクトモデルの実装
- 不要な待機時間を最小限に抑える
- ブラウザコンテキストの再利用
- テストデータの事前準備
- スクリーンショットやビデオの最小限の使用
- スクリーンショット関数をラップして、環境変数の値に応じて処理を保存するかどうか切り替えるなど
- 撮る必要が無いならば、その時間が無駄なので
- タイムアウト設定の最適化