playwright(Node.js) で E2E テスト!
はじめに
みなさん、Playwright をご存知ですか?
これまで、Node.js での E2E テストといえば、puppeteer、TestCafe を使っていたという方も少なくないのではないでしょうか?
Playwright は、そのうち、puppeteer と同じような記述も多く、非常に分かりやすいかと思います。
また、Microsoft によって開発、運用されているため、今後サポートされなくなるというリスクも
ある程度回避できるかと思います。
2020/12/26 時点では、バージョン 1.7.0 なので、その時点での情報になります。
サポート環境
2020/12/26 時点でサポートしているのは以下になります。
- Node.js 10.17 以上
- Windows: Windows 及び WSL で動きます
- macOS: 10.14 以上
- Linux: ディストリビューションによる(Firefox は、Ubuntu 18.04 以上)
古い Microsoft Edge や IE 11 はサポートされていません。
また、Python や C# などでも使えますが、Java や Ruby はサポートしていません。
インストール
$ yarn add -D playwright
簡単な例
Yahoo! JAPAN トップページにアクセスして、スクリーンショットを撮るテストをしてみます。
const { chromium, devices } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({
...devices['iPhone 11 Pro']
});
await page.goto('https://m.yahoo.co.jp');
await page.screenshot({path: './screenshot.png', fullPage: true});
await browser.close();
})()
デバックモードで実行するとより詳細にテスト内容が確認できます。
$ DEBUG=pw:api node test.js
各 API について
全て書くと多すぎるので、使えそうなものだけまとめてみます。
playwright
playwright.chromium
Chromium ブラウザを使用する際に使います。
playwright.firefox
FireFox ブラウザを使用する際に使います。
playwright.webkit
Webkit ブラウザを使用する際に使います。
playwright.devices
テストを行うデバイスを指定します。
指定できるのは、
に記載のあるものになります。
また、同じ形式で連想配列にして指定することで独自のものを指定することも可能です。
Browser
browser.close
browserType.launch や browserType.connect で生成されたブラウザを閉じます。
browserType.launch で生成されたブラウザに対しては、全てのページも閉じます。
browserType.connect で生成されたブラウザに対しては、全ての context をリセットし、サーバとの接続も解除します。
browser.newContext
引数に連想配列を指定して、context を生成します。
いくつか抜粋します。
ignoreHTTPSErrors<boolean>: HTTPS のエラーを無視します。デフォルトは false です。
userAgent<string>: User-Agent を指定します。
isMobile<boolean>: モバイルデバイスかどうかを指定します。Firefox は、サポートされていません。
hasTouch<boolean>: viewport がタッチイベントをサポートしているかどうかです。デフォルトは false です。
geolocation<{latitude: number, longitude: number, accuract: number}>: 位置情報について指定します。
extraHTTPHeaders<{[key: string]: string}>: HTTP ヘッダを指定します。
browser.newPage
引数に連想配列を指定して、context の中で page を生成します。
これは、SPA や短いコードに対して便利な API になります。
プロダクションコードでは、browser.newContext を行ってから browser.newPage を行ってください。
引数は、browser.newContext とほとんど同じになります。
BrowserContext
browserContext.cookies
設定されている cookie のリストを取得します。
引数に url を指定することで特定の cookie に絞り込むことができます。
browserContext.addCookies
cookie を設定します。
browserContext.clearCookies
cookie をリセットします。
browserContext.storageState
cookie や LocalStorage の値を取得します。
browserContext.grantPermissions
さまざまなパーミッションを与えます。
指定できるのは、以下のものになります。
- 'geolocation'
- 'midi'
- 'midi-sysex' (system-exclusive midi)
- 'notifications'
- 'push'
- 'camera'
- 'microphone'
- 'background-sync'
- 'ambient-light-sensor'
- 'accelerometer'
- 'gyroscope'
- 'magnetometer'
- 'accessibility-events'
- 'clipboard-read'
- 'clipboard-write'
- 'payment-handler'
browserContext.clearPermissions
パーミッションをリセットします。
browserContext.exposeBinding
window オブジェクトに関数を追加します。
これは、全ての frame 、page に追加されます。
例:
const { webkit } = require("playwright"); // Or 'chromium' or 'firefox'.
(async () => {
const browser = await webkit.launch({ headless: false });
const context = await browser.newContext();
await context.exposeBinding("pageURL", ({ page }) => page.url());
const page = await context.newPage();
await page.setContent(`
<script>
async function onClick() {
document.querySelector('div').textContent = await window.pageURL();
}
</script>
<button onclick="onClick()">Click me</button>
<div></div>
`);
await page.click("button");
})();
browserContext.exposeFunction
window オブジェクトに関数を追加します。
これは、全ての frame 、page に追加されます。
例:
const { webkit } = require("playwright"); // Or 'chromium' or 'firefox'.
const crypto = require("crypto");
(async () => {
const browser = await webkit.launch({ headless: false });
const context = await browser.newContext();
await context.exposeFunction("md5", (text) =>
crypto.createHash("md5").update(text).digest("hex")
);
const page = await context.newPage();
await page.setContent(`
<script>
async function onClick() {
document.querySelector('div').textContent = await window.md5('PLAYWRIGHT');
}
</script>
<button onclick="onClick()">Click me</button>
<div></div>
`);
await page.click("button");
})();
browserContext.newPage
page を生成します。
browserContext.close
context を閉じます。
browserContext.route
ネットワークリクエストをキャッチして、特定の処理を行うようにハンドリングします。
特定の url で一度設定したら、マッチする全てのリクエストがハンドリングされます。
例えば、画像を取得するリクエストを無視する場合、以下のようになります。
const context = await browser.newContext();
await context.route("**/*.{png,jpg,jpeg}", (route) => route.abort());
const page = await context.newPage();
await page.goto("https://example.com");
await browser.close();
browserContext.unroute
設定した route を解除します。
browserContext.setDefaultNavigationTimeout
navigation タイムアウトを設定します。
影響を与えるのは、以下の API になります。
- page.goBack
- page.goForward
- page.goto
- page.reload
- page.setContent
- page.waitForNavigation
browserContext.setDefaultTimeout
全ての API のタイムアウトを設定します。
ただし、page.setDefaultNavigationTimeout, page.setDefaultTimeout(timeout), browserContext.setDefaultNavigationTimeout(timeout) の方が優先されます。
browserContext.setGeolocation
位置情報を設定します。
この位置情報を取得するためには、browserContext.grantPermissions で権限を与える必要があります。
browserContext.waitForEvent
特定の event が発火されるまで待ちます。
Page
page.on('dialog')
alert, prompt, confirm, beforeunload が呼ばれた時に
呼ばれるイベントリスナーです。
page.on('request')
リクエストが発行された時に呼ばれるイベントリスナーです。
コールバックの引数となる request は、読み込み専用です。
page.on('requestfailed')
タイムアウトなどリクエストが失敗した時に呼ばれるイベントリスナーです。
page.on('requestfinished')
レスポンスボディの取得が成功したときに呼ばれるイベントリスナーです。
page.on('response')
リクエストヘッダ、ステータスを受け取った時に呼ばれるイベントリスナーです。
page.$
セレクタを1つだけ指定します。
複数マッチした時は、最初の1つ目のみのセレクタとなります。
1つもマッチしなかった場合は、null を返します。
page.$$
セレクタを複数指定します。
1つもマッチしなかった場合は、[] を返します。
page.$eval
セレクタを1つだけ指定して、処理を行います。
セレクタがマッチしなかった場合は、エラーを投げます。
const searchValue = await page.$eval("#search", (el) => el.value);
const preloadHref = await page.$eval("link[rel=preload]", (el) => el.href);
const html = await page.$eval(
".main-container",
(e, suffix) => e.outerHTML + suffix,
"hello"
);
page.$$eval
セレクタを複数指定して、処理を行います。
const divsCounts = await page.$$eval(
"div",
(divs, min) => divs.length >= min,
10
);
page.check
セレクタを指定して、チェックを入れます。
以下の順番で処理が行われます。
- マッチするセレクタを探します。もしなければ、マッチする DOM が生成されるまで待ちます。
- input の checkbox か radio かどうかを確認します。もし違った場合は、reject します。既にチェック済みの場合は、すぐに return します。
- force オプションが指定されていない場合は、チェックされるまで待ちます。
- 必要があれば、スクロールを行います。
- page.mouse を使って、セレクタの真ん中をクリックします。
- noWaitAfter オプションが指定されていない場合は、ナビゲーションが成功するかどうかを待ちます。
- セレクタがチェックされたかを確認します。されていなければ、reject します。
page.uncheck
セレクタを指定して、チェックを外します。
処理は page.check と同じ流れで行います。
page.click
セレクタを指定して、クリックします。
以下の順番で処理が行われます。
- マッチするセレクタを探します。もしなければ、マッチする DOM が生成されるまで待ちます。
- force オプションが指定されていない場合は、クリックできるセレクタ可動かをチェックします。
- 必要があれば、スクロールを行います。
- page.mouse を使って、セレクタの真ん中か特定の位置をクリックします。
- noWaitAfter オプションが指定されていない場合は、ナビゲーションが成功するかどうかを待ちます。
page.close
page を閉じます。
runBeforeUnload オプションが false の場合は、結果は page を閉じてから返します。
runBeforeUnload オプションが true の場合は、page が閉じるのを待ちません。
page.dblclick
セレクタを指定して、ダブルクリックします。
page.dispatchEvent
セレクタを指定して、イベントを発火させます。
複数セレクタがマッチした場合は、最初の1つ目になります。
await page.dispatchEvent("button#submit", "click");
page.fill
セレクタを指定して、値を入力します。
複数セレクタがマッチした場合は、最初の1つ目になります。
input や textarea など、入力できないものにマッチした場合は、エラーを投げます。
page.focus
セレクタを指定して、フォーカスします。
複数セレクタがマッチした場合は、最初の1つ目になります。
マッチするセレクタが見つからない場合は、見つかるまで待ちます。
page.getAttribute
セレクタを指定して、属性値を取得します。
複数セレクタがマッチした場合は、最初の1つ目になります。
page.goBack
一つ前の画面に戻ります。
page.goForward
一つ先の画面に進みます。
page.goto
指定された url に遷移します。
以下の場合は、エラーを投げます。
- SSL エラー
- 不正な URL
- タイムアウト
- サーバからレスポンスが返ってこない
- メインリソースのロードに失敗
page.reload
ページを再読み込みします。
page.hover
セレクタを指定して、ホバーします。
page.innerHTML
セレクタを指定して、HTML 要素を取得します。
複数セレクタがマッチした場合は、最初の1つ目になります。
page.innerText
セレクタを指定して、テキスト要素を取得します。
複数セレクタがマッチした場合は、最初の1つ目になります。
page.press
セレクタを指定して、キーを押します。
複数セレクタがマッチした場合は、最初の1つ目になります。
キーは、
F1 - F12, Digit0- Digit9, KeyA- KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp
などが選択できます。
また、複数同時に押すことも可能です。
以下に例を示します。
const page = await browser.newPage();
await page.goto("https://keycode.info");
await page.press("body", "A");
await page.screenshot({ path: "A.png" });
await page.press("body", "ArrowLeft");
await page.screenshot({ path: "ArrowLeft.png" });
await page.press("body", "Shift+O");
await page.screenshot({ path: "O.png" });
await browser.close();
page.route
マッチした url に対して、特定の処理を行います。
一度設定されると全てのリクエストに対して有効です。
const page = await browser.newPage();
await page.route("**/*.{png,jpg,jpeg}", (route) => route.abort());
await page.goto("https://example.com");
await browser.close();
page.unroute
page.route を解除します。
page.screenshot
スクリーンショットを保存します。
保存できるのは、png または jpeg のみになります。
page.selectOption
セレクタを指定して、select の要素を選択します。
// single selection matching the value
page.selectOption("select#colors", "blue");
// single selection matching both the value and the label
page.selectOption("select#colors", { label: "Blue" });
// multiple selection
page.selectOption("select#colors", ["red", "green", "blue"]);
page.setContent
HTML をページに設定します。
const html = `<!DOCTYPE html>
<html>
<head><title>test</title></head>
<body>
<h1>test</h1>
</body>
</html>`;
await page.setContent(html);
page.setDefaultNavigationTimeout
navigation タイムアウトを設定します。
影響を与えるのは、以下の API になります。
- page.goBack
- page.goForward
- page.goto
- page.reload
- page.setContent
- page.waitForNavigation
page.setDefaultTimeout
全ての API のタイムアウトを設定します。
page.textContent
セレクタを指定して、中身を取得します。
page.title
ページのタイトルを取得します。
page.type
セレクタを指定して、文字をタイプします。
await page.type("#mytextarea", "Hello");
await page.type("#mytextarea", "World", { delay: 100 });
page.url
ページの URL を取得します。
page.waitForEvent
指定したイベントが発火されるまで待ちます。
page.waitForFunction
引数で記述した処理が true になるまで待ちます。
const { webkit } = require("playwright");
(async () => {
const browser = await webkit.launch();
const page = await browser.newPage();
const watchDog = page.waitForFunction("window.innerWidth < 100");
await page.setViewportSize({ width: 50, height: 50 });
await watchDog;
await browser.close();
})();
page.waitForLoadState
"load" または "domcontentloaded" または "networkidle" の状態になるまで待ちます。
const [popup] = await Promise.all([
page.waitForEvent("popup"),
page.click("button"),
]);
await popup.waitForLoadState("domcontentloaded");
console.log(await popup.title());
page.waitForNavigation
ページ遷移が終わるまで待ちます。
const [response] = await Promise.all([
page.waitForNavigation(),
page.click("a.delayed-navigation"),
]);
page.waitForRequest
指定されたリクエストが来るまで待ちます。
戻り値には、マッチしたリクエストが返ってきます。
const firstRequest = await page.waitForRequest("http://example.com/resource");
const finalRequest = await page.waitForRequest(
(request) =>
request.url() === "http://example.com" && request.method() === "GET"
);
return firstRequest.url();
page.waitForResponse
指定されたレスポンスが来るまで待ちます。
戻り値には、マッチしたリクエストのレスポンスが返ってきます。
const firstResponse = await page.waitForResponse(
"https://example.com/resource"
);
const finalResponse = await page.waitForResponse(
(response) =>
response.url() === "https://example.com" && response.status() === 200
);
return finalResponse.ok();
page.waitForSelector
セレクタを指定して、そのセレクタが特定のステータスになるまで待ちます。
ステータスは、attached
detached
visible
hidden
から指定できます。
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
let currentURL;
page
.waitForSelector("img")
.then(() => console.log("First URL with image: " + currentURL));
for (currentURL of [
"https://example.com",
"https://google.com",
"https://bbc.com",
]) {
await page.goto(currentURL);
}
await browser.close();
})();
page.waitForTimeout
指定したミリ秒数待ちます。
Dialog
dialog.accept
prompt ダイアログで OK されるまで待ちます。
dialog.defaultValue
prompt ダイアログの場合は、その文字列を、それ以外のダイアログの場合は、空文字列を返します。
dialog.dismiss
ダイアログが閉じられるまで待ちます。
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
page.on("dialog", async (dialog) => {
console.log(dialog.message());
await dialog.dismiss();
await browser.close();
});
page.evaluate(() => alert("1"));
})();
dialog.message
表示されているダイアログのメッセージを取得します。
dialog.type
どんなダイアログを表示しているか取得します。
取得できるのは、alert
beforeunload
confirm
prompt
です。
Keyboard
keyboard.down
指定したキーを keyDown イベントと一緒に入力します。
keyboard.up
指定したキーを keyUp イベントと一緒に入力します。
keyboard.press
指定したキーを keyPress イベントと一緒に入力します。
keyboard.insertText
指定した文字を入力します。
keyDown
keyUp
keyPress
イベントは発行しません。
keyboard.type
指定した文字を入力します。
keyDown
keyUp
keyPress
イベントは発行します。
EvaluationArgument
Playwright は、jest による評価も可能ですが、Playwright にも評価するメソッドが存在します。
// A primitive value.
await page.evaluate((num) => num, 42);
// An array.
await page.evaluate((array) => array.length, [1, 2, 3]);
// An object.
await page.evaluate((object) => object.foo, { foo: "bar" });
// A single handle.
const button = await page.$("button");
await page.evaluate((button) => button.textContent, button);
// Alternative notation using elementHandle.evaluate.
await button.evaluate((button, from) => button.textContent.substring(from), 5);
// Object with multiple handles.
const button1 = await page.$(".button1");
const button2 = await page.$(".button2");
await page.evaluate((o) => o.button1.textContent + o.button2.textContent, {
button1,
button2,
});
// Obejct destructuring works. Note that property names must match
// between the destructured object and the argument.
// Also note the required parenthesis.
await page.evaluate(
({ button1, button2 }) => button1.textContent + button2.textContent,
{ button1, button2 }
);
// Array works as well. Arbitrary names can be used for destructuring.
// Note the required parenthesis.
await page.evaluate(([b1, b2]) => b1.textContent + b2.textContent, [
button1,
button2,
]);
// Any non-cyclic mix of serializables and handles works.
await page.evaluate(
(x) => x.button1.textContent + x.list[0].textContent + String(x.foo),
{ button1, list: [button2], foo: null }
);
Working with Chrome Extensions
Playwright は、ヘッドレスモードでない状態であれば、Chrome Extensions を有効にした状態でのテストも可能です。
const { chromium } = require('playwright');
(async () => {
const pathToExtension = require('path').join(__dirname, 'my-extension');
const userDataDir = '/tmp/test-user-data-dir';
const browserContext = await chromium.launchPersistentContext(userDataDir,{
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`
]
});
const backgroundPage = browserContext.backgroundPages()[0];
// Test the background page as you would any other page.
await browserContext.close();
})();
おわりに
いかがだったでしょうか?
自分が作成したプロダクトに対して、E2E テストを書くことも出来ますし、
さまざまなブラウザを使った自動化にも役立つかと思います。
日々の面倒な作業を Playwright を使って自動化するのも良いかもしれません。
puppeteer を使っていた方であれば、より多くのブラウザをサポートしている Playwright には
メリットも感じられるかと思うので、ぜひ移行してみてください。
お知らせ
Webサイト・ツール・LP作成のご依頼は、
こちらからお問い合わせいただけます。お気軽にご相談ください。
参考
Discussion