新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Playwrightの使い方メモ

最近Playwrightを試していたので、使い方を忘れてもまた思い出せるよう、Playwrightの使い方をメモしておこうと思います。

インストール

npm install --save playwright
  • Playwrightをインストールすると、同時にブラウザChromium・Firefox・Webkitもインストールされる
  • デフォルトではPlaywrightから呼び出されるブラウザは、その時にインストールされたブラウザが使われる
  • ブラウザをインストールせずPlaywrightのみをインストールし、ブラウザは別途インストールしたものを使用することも可能
  • しかし、Playwrightのバージョンとブラウザのバージョンは密接に関係があるため、Playwrightとブラウザは同時にインストールして、インストールしたブラウザを使うようにした方がよい

構成

Playwrightは下記クラスで構成される。

Browser > Context > Page > Locator

Brower

  • いわゆる Chromium・ChromeやFirefox、Edgeなどといったブラウザにあたるもの
  • 設定可能項目例
    • ヘッドレスモード
    • ブラウザ起動オプション
    • ゆっくり操作(slowMo)

Context

  • 実行環境にあたるもの
  • ブラウザで言うところの各ウィンドウ(タブではない)
  • 1つのブラウザウィンドウが1つのContextに紐づく
  • 設定可能項目例
    • ユーザーエージェント
    • ロケール
    • タイムゾーン
    • ウィンドウサイズ

Page

  • いわゆるブラウザのタブ
  • JavaScriptのポップアップも1つのPageになる

Locator

  • html内の各dom要素

Context.close() Browser.close()

終わったらContextBrowserの順に.close()で閉じる必要がある。

サンプル

例)Googleで「test」と検索し、結果ページのソースを表示する。

import { chromium } from 'playwright';

(async () => {
    try {

        const browser = await chromium.launch({
            headless: false,
        });

        const context = await browser.newContext();

        const page = await context.newPage();

        await page.goto('https://www.google.com/');
        await page.locator('input[name="q"]').type("test\n");
        await page.locator('#pnnext').click();
        await page.waitForURL(/search\?q/);

        console.log(await page.content());

        await context.close();
        await browser.close();

    } catch (err) {
        console.error(err);
    }
})();

Locator

ブラウザ操作の流れは、対象となるdomを取得し、そのdomに対して操作の繰り返しになる。

Playwrightでは、domLocatorで表現し、domの属性取得や操作は、全てLocatorを介して行う。

自動待機

  • dom取得時にそのdomがまだ生成されれていないことがある。
  • Locatorを使ってdomの属性取得や操作をしようとすると、Locatorはそのdomが生成されるまで自動で待って、生成された後に実行してくれる。
  • 設定時間まで待ってdomが見つからない場合はエラーになる。
  • 設定時間はデフォルトで30sec

下記例は、Locator内部でinput[type="button"]が見つかるまで自動で待って、見つかったらクリックしてくれている。なので、見つかるまでwaitを入れる必要がない。

await page.locator('input[type="button"]').click();

Locatorで要素の指定方法は複数ある。

指定方法を以下に記載。

page.getBy...()

  • pagepage.getBy...()というLocatorを取得する関数がいくつかある
  • あまり小回りが効かないので、積極的には使わない

CSSセレクター・XPathセレクター

  • page.locator(<selector>)で、CSSセレクター・XPathセレクターが使える
  • css=と書くとCSSセレクター、xpath=と書くとXPathセレクター。(いずれも省略可能)
await page.locator('css=input.test1').click();
await page.locator('input.test1').click();
await page.locator('xpath=//input[@class="test1"]').click();
await page.locator('//input[@class="test1"]').click();

Playwright独自のセレクター

CSSセレクターにはない、Plawright独自のセレクターがあり、CSSセレクターと組み合わせてより複雑な選択ができる。

特定文字列を含む

  • locator.filter({hasText: <RegExp>})
    • locatorの中から特定の文字列を含むものを絞り込む
    • 文字列指定には正規表現が使える
    • 文字列検索対象は子ノード以下に及ぶ

html

<div id="small">abc</div>
<div id="large">ABC</div>

code

// large
console.log(
    await page.locator('div').filter({ hasText: /ABC/ }).getAttribute('id')
);

文字列を子ノードまで見るので、下記の場合はidが「test1」「test2」「large」が該当する。

html

<div id="test1">
    <div id="test2">
        <div id="small">abc</div>
        <div id="large">ABC</div>
    </div>
</div>

code

await page.locator('div').filter({hasText: /test/).click();

親要素をたどる

  • locator.locator('..')
    • locatorの1つ上のdomを指す
    • チェーンして上にたどっていくことができる

html

<div id="a">
    <div id="b">
        <div id="c">abc</div>
    </div>
</div>

code

// c
console.log(
    await page.locator('#c').getAttribute('id')
);

// b
console.log(
    await page.locator('#c').locator('..').getAttribute('id')
);

// a
console.log(
    await page.locator('#c').locator('..').locator('..').getAttribute('id')
);

複数要素

  • セレクターが複数要素にマッチする場合がある
  • locator.all()でマッチするlocatorの配列を取得できる
for(const l of await page.locator('input').all()){
    console.log(await l.getAttribute('id'));
}

Locator(操作)

  • locator.click()
    • クリック
  • locator.fill(<string>)
    • inputvalueを指定した文字列に設定する
  • locator.type(<string>)
    • 文字列入力
    • キーボード入力をエミュレート
  • locator.clear()
    • inputvalueをクリアにする
  • locator.press(<key>)
    • 要素にフォーカスを移して、指定したキーを押して離す
    • キー定義一覧
  • locator.forcus()
    • フォーカスを移す
  • locator.scrollIntoViewIfNeeded()
    • 要素が見える位置までスクロールしてくれる

Locator(属性)

  • locator.getAttribute(<attribute_name>)
    • 属性値取得
    • 属性名例
      • id
      • name
      • value

Locator(その他)

  • locator.isChecked()
    • チェックボックス・ラジオボタンのチェック状態を判別
  • locator.setChecked(true|false)
    • チェックボックス・ラジオボタンのチェック状態を設定
  • locator.setInputFiles([<file>,...])
    • <input type="file" />のファイルを設定
    • 複数ファイル選択の場合、配列で複数ファイルを渡す
  • locator.selectOption([<val>,...])
    • selectoptionを選択
    • valueもしくはlabelを指定
    • 複数option選択の場合、配列で複数の値を渡す
  • locator.waitFor()
    • その要素が現れるまで待つ
    • 属性取得や操作は行わないが、表示まで待ちたい場合に使う
    • ページのロード完了待ちなどに使う

selectのoptionの選択状態を取得

Playwrightの機能だけではできない。JavaScriptを使って取得する。

select(単一選択)

select.selectedIndexを使って取得

html

<select>
  <option value="v1">v1</option>
  <option value="v2" selected>v2</option>
  <option value="v3">v3</option>
</select>

code

const selectedOption = await page.locator('select')
    .evaluate((node: HTMLSelectElement) => {
        return node.options[node.selectedIndex].value;
    });

// v2
console.log(selectedOption);

select(複数選択)

option.selectedを使って取得

html

<select multiple>
  <option value="v1" selected>v1</option>
  <option value="v2">v2</option>
  <option value="v3" selected>v3</option>
</select>

code

const selectedOptions = await page.locator('select#test7')
    .evaluate((node: HTMLSelectElement) => {
        const selectedValues: Array<string> = [];
        for (const option of node.options) {
            if (option.selected) {
                selectedValues.push(option.value);
            }
        }
        return selectedValues;
    });

// [ 'v1', 'v3' ]
console.log(selectedOptions);

ナビゲーション

  • デフォルトはloadイベントが起こるまで待つ
  • 引数でnetworkidleになるまで待つように変更できる

遷移

  • page.goto(<URL>)
    • 自動でロード完了まで待ってくれる
  • page.reload()
    • 自動でロード完了まで待ってくれる
  • page.waitForTimeout()
    • 指定時間(ms)待つ

ページロード完了を待つ

  • page.waitForLoadState()
    • 今のページのロード完了まで待つ
  • page.waitForURL(<url|glob_string|RegExp>)
    • 指定したURLでのロード完了を待つ
    • URL・glob形式・正規表現が使える
  • page.waitForSelector(<selector>)
    • 指定の要素が出るまで待つ
    • その要素を使うのであれば、locatorを直接使えば自動待機してくれるのであえて記述する必要はない。
  • locator.waitFor()
    • 指定の要素が出るまで待つ
    • page.waitForSelector(<selector>)とやっていることは同じ
    • その要素を使うのであれば、locatorを直接使えば自動待機してくれるのであえて記述する必要はない。

例)リンクをクリックして新しいURLに遷移し、その遷移先でボタンをクリックする

ボタンクリックのlocatorが、ボタンが表示されるまで自動待機するので、ページロード完了を調べる必要がない。

await page.locator('a').click();
await page.locator('button').click();

例)リンクをクリックして新しいURLに遷移し、その遷移先のページソースを表示する

ページロード完了まで待つ必要がある。待つ方法は下記のいずれも可。

  • page.waitForLoadState()
  • page.waitForURL(<url>)
  • page.waitForSelector(<selector>)
  • page.locator(<selector>).waitFor()
await page.locator('a').click();

// wait load finish
await page.waitForLoadState();

console.log(page.content());

属性

  • page.title()
  • page.url()
  • page.content()
    • ページのhtml

フレーム

  • iframeも他のタグ同様セレクターで取得できる
  • ifremeを取得するにはlocatorではなく、page.frameLocator(<selector>)を使う
  • 取得したiframeは、pageのようなもので、そこを起点として、locatorを使ってiframe内の要素にアクセスできる。

例)iframe内のbuttonをクリックする

const iframe = await page.frameLocator('iframe');
await iframe.locator('button').click();

Tab

  • タブとpageは同じもの
  • 新しいタブが作成されると、新しいpageが作成される

新規タブの取得方法

<a target="_blank">などで新しくタブが生成される時に、新しく生成されたタブのpageを取得する方法。

  • contextpageイベントハンドラーの戻り値が、新しく生成されたpageオブジェクトになる
  • イベントハンドラーが新しくタブを作成するアクションをブロックしないよう、Promiseで生成しておき、アクション後にawaitで待機する。
const pagePromise = context.waitForEvent('page');
await page.locator('a').click();
const newPage = await pagePromise;
await newPage.locator('button').click();

Browser・Context設定

  • 動作をゆっくりにする
    • Browser
      • slowMo
  • ヘッドレスモードにする
    • Browser
      • headless
  • 地域・言語設定
    • Context
      • locale
      • timezoneId
const browser = await chromium.launch({
    headless: false,
    slowMo: 100,
});

const context = await browser.newContext({
    locale: 'ja',
    timezoneId: 'Asia/Tokyo',
});

ブラウザの状態保存

cookie・localStorage 保存

cookie・localStorageを保存して、ブラウザの状態保存ができる。

  • context.storageState({ path: <file_path> })でcookeiとlocalStorageの情報をファイルに保存する
  • browser.newContext({ storageState: <file_path> })でファイルに保存されたcookieとlocalStorage情報を復元してウィンドウを立ち上げる
  • context.close()の前に保存しないといけない
const context = await browser.newContext({
    storageState: './state.json',
});

//////////

await context.storageState({ path: './state.json' });

ブラウザのユーザーデータ保存

ブラウザのユーザーデータ保存先を指定して、ブラウザの状態保存ができる。

  • BrowerType.launchPersistentContext()でユーザーデータ保存先を指定する
  • BrowerType.launchPersistentContext()BrowserContextの生成を同時に行うので、BrowserContextのオプションの設定はここで行う
const saveDir = './save/browser'
const context = await chromium.launchPersistentContext(
    saveDir,
    {
        headless: false,
        locale: 'ja',
        timezoneId: 'Asia/Tokyo',
        slowMo: 100,
    }
);

スマートフォンエミュレート

  • playwright.devicesにスマートフォンテンプレートが定義されている
  • iPhoneの場合はwebkitを使った方がなお良い
  • TLS certificateエラーが出る場合はignoreHTTPSErrorsをセット
import { webkit, devices } from 'playwright';

const browser = await webkit.launch();
const context = await browser.newContext({
    ...devices['iPhone 12'],
    ignoreHTTPSErrors: true,
});

ファイルダウンロード

仕様・手順

  • ファイルはテンポラリに保存される
  • contextが閉じられるとテンポラリのファイルは削除される
  • ダウンロードファイルの情報はDownloadオブジェクトに格納される
  • pagedownloadイベントハンドラーの戻り値がDownloadオブジェクトになる
  • イベントハンドラーがダウンロードアクションをブロックしないよう、Promiseで生成しておき、アクション後にawaitで待機する。
  • ダウンロードが完了すると、await download.path()が完了する
  • ファイル名はdownload.suggestedFilename()
  • テンポラリに保存されたファイルを、download.saveAs(<path>)で別の場所にコピーして使う
const downloadPromise = page.waitForEvent('download');
await page.locator('button#btn_save').click();
const download = await downloadPromise;
await download.path();
const fileName = download.suggestedFilename();
await download.saveAs(`./download/${fileName}`);

ファイルアップロード

フォーム

  • <input type="file" />にアップロードするファイルの絶対パスをlocator.setInputFiles([<path>])で設定する
await page.locator('input[type="file"]').setInputFiles([<path>]);
await page.locator('input[type="submit"]').click();

ファイル選択ダイアログ

クリックするとファイル選択ダイアログが出て、ファイルを指定してアップロードするパターン

仕様・手順

  • ファイル選択ダイアログにファイルパスを設定してOKボタンを押すのは、FileChooserオブジェクトで行う
  • pagefilechooserイベントハンドラーの戻り値がFileChooserオブジェクトになる
  • イベントハンドラーがダウンロードアクションをブロックしないよう、Promiseで生成しておき、アクション後にawaitで待機する。
  • FileChooser.setFiles([<path>])でファイルパスを指定してOKボタンを押してくれる
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('button#btn_save').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([<path>]);

Playwrightとブラウザ間のやり取りの仕組み

PlaywrightからブラウザでJavaScriptを実行させるにあたり、Playwrightとブラウザ間のやり取りをある程度知っておく必要があるので簡単に説明。

Playwrightに限らず、puppeteerやSeleniumも大体同じような仕組みになっている。

ブラウザのオブジェクトをPlaywrightに持ってくることは出来ない

ブラウザの<input><button>を取得して、属性取得・設定やクリックなどができる。

const input = await page.locator('input');
await input.fill('TEST');

const button = await page.locator('button');
await button.click();

一見、ブラウザのオブジェクトをPlaywrightに持ってきているように見えるが、そんなことは無理で、ブラウザからそのオブジェクトを識別するための参照情報を取得しているに過ぎない。

そして、Playwrightからそのオブジェクトの操作をしたい場合は、Playwrightからブラウザに識別情報と操作内容を伝えて、ブラウザ側で実行してもらい、その結果を受け取っている。

Locator & JSHandle

上記の識別情報をPlaywrightとpuppeteerではJSHandleで定義している。

更にPlaywrightでは、JSHandleを使いやすいようラップして拡張したLocatorがあり、JSHandleではなくLocatorを使う。

PlaywrigthでJSHandleを使う場面

Playwrightからブラウザ上でJavaScriptを実行する際、ブラウザとブラウザ上のオブジェクトのやり取りは、LocatorではなくJSHandleを使って行う。

詳しくは下記JavaScriptで説明。

JavaScript

Playwrightからブラウザ上でJavaScriptを実行することができる。

値の送受信

  • PlaywrightからJavaScriptに値を渡すことができる
  • JavaScriptからPlaywrightに値を返すことができる

送受信データ

ブラウザのオブジェクトを直接受け渡しできないので、domJSHandleとして受け渡しされる。

page.evaluate()

  • JavaScriptを実行
  • returnで戻り値を返す
  • 戻り値はスカラー・配列・JSONが使える
  • 引数をJavaScriptに渡すことができる
  • 引数はスカラー・配列・JSON、およびJSHandleが使える
  • 引数に渡したJSHandleはブラウザ上のJavaScriptオブジェクトに復元される
//////////
// return json
const res1 = await page.evaluate(() => {
    const obj = {
        val1: 123,
        val2: 'abc'
    };
    return obj;
});

// { val1: 123, val2: 'abc' }
console.log(res1);

//////////
// pass json
const res2 = await page.evaluate((arg) => {
    const obj = {
        val1: arg.num,
        val2: arg.str
    };
    return obj;
}, { num: 123, str: 'abc' });

// { val1: 123, val2: 'abc' }
console.log(res2);

locator.evaluate()

  • page.evaluate()とほぼ同じ
  • 関数の第1引数が、locatorで選択したdomになる
// set input value 'test'
await page.locator('input').evaluate((dom: HTMLInputElement, arg) => {
    dom.value = arg
}, 'test');

page.evaluateHandle()

  • JavaScriptを実行
  • returnで戻り値を返す
  • 戻り値はJSHandleのみ
  • JSHandle以外の値は返せない
  • 引数をJavaScriptに渡すことができる
  • 引数はスカラー・配列・JSON、およびJSHandleが使える
  • 引数に渡したJSHandleはブラウザ上のJavaScriptオブジェクトに復元される

ブラウザからinputを取得して、そのinputvalueに「abc」をセットする。

const res = await page.evaluateHandle(() => {
    return document.querySelector('input');
});

await page.evaluateHandle((arg) => {
    arg.dom.value = arg.val;
}, { dom: res, val: 'abc' });
  • document.querySelector('input')は、ブラウザ上のinputを指す
  • resinputJSHandle
  • resevaluateHandle()に渡すと、JSHandleからそれが参照するブラウザ上のオブジェクトinputに復元される
    • つまり、arg.domJSHandleではなくdocument.querySelector('input')

locator.evaluateHandle()

  • page.evaluateHandle()とほぼ同じ
  • 関数の第1引数が、locatorで選択したdomになる

デバッグ

console出力

  • ブラウザのconsoleをPlaywrightに出力する
await page.on('console', msg => {
    console.log(msg.text());
});

一時停止

  • await page.pause()を一時停止したい箇所に記入
    • 一時停止してInspectorウィンドウが立ち上がる
    • 一時停止中もブラウザの操作は可能

要素を選択するセレクターの書き方を調べる

  • await page.pause()Inspectorウィンドウを立ち上げる
  • Pic locatorをクリック
  • プラウザのページからセレクターを調べたい要素をクリック
  • その要素を選択するセレクター文が表示される

セレクターがページのどの要素かを調べる

  • await page.pause()Inspectorウィンドウを立ち上げる
  • Pic locatorの隣のテキストボックスにセレクター式を入力する
    • locator('input')など
  • 該当する要素がハイライト表示される

操作からコード自動生成

  • await page.pause()Inspectorウィンドウを立ち上げる
  • Recordをクリック
  • (コード化したい操作を行う)
  • Recordをクリック
  • 操作内容がコード化されてInspectorウィンドウに表示される

感想など

クローラーは思いついた時にさっと作れると便利なのですが、使い方を調べるのが面倒なのでなかなか作る気になりません。

なので、必要最小限の機能説明をまとめておけば便利かなと思って書き始めたのですが、長くなってしまいました。

JavaScript関連で長々と書いてしまいましたが、JSHandleを扱うケースはほとんどないので、evaluete()の説明だけにしておいてもよかったかも。

あと、await page.pause()がめっちゃ便利。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com