Python と Playwright でブラウザを自動操作させるコードを自動生成したよ

Playwright が昨年１年間で大幅パワーアップしていたので、使い方を確認したときの記録のまとめです。
ブラウザを自動操作できるということは、簡単なスクレイピングやブラウザ側のテスト自動化が簡単にできるようになります。
特に、Python での解説がまだまだ少なかったので、自分の学習を含めてまとめました。
今回は入門編ということで全体像をつかみつつ使用方法の流れを確認していただければありがたいです。
**Selenium や Puppeteer を使っている方も、一度試す価値ありと思っています。**

## 選定した理由

ブラウザのテストを Python で自動化したかったんです。
私なりの要件がありまして、非常にわがままな要件でしたが余裕ですべてクリアしました。

* **Python で書ける**こと。社内で Python を使える方が多いので。**pytest と連携してくれる**となおうれしい。
* **Docker コンテナで動かせる**こと。CI/CD に組み込みたい。
* **非同期動作できる**こと。
* **Windows 環境でテストコードが書きやすい**こと。というか、**GUI でブラウザ操作しながらテストコードを自動で作成してくれる**と嬉しい。
* **スクリーンショットをとれる**こと。エビデンスとしてテスト結果に使いたい。

さらに、こんなこともできます。

* Chromium、Firefox、Webkit のクロスブラウザテスト
* iPhone および Android 環境でのテスト
* ブラウザの動作をビデオレコーディング
* NodeJS、C#、Java もサポート

ほんとに至れり尽くせりです。。。さすが Microsoft 様！


## 環境およびインストール

今回の作業環境はこちら

* Windows 10 Pro (2004)
* Python 3.9.1

Playwright のインストール

```
# インストール
pip install playwright==1.8.0a1
playwright install

# インストールの確認
playwright --version
# Version 1.8.0-1611168053000

# もしくはこれでインストールの確認
python -m playwright --version
# Version 1.8.0-1611168053000
```

もしかしたら、下記のものも必要かもしれません（未検証）
私の環境で、もしかしたら影響を与えているかもしれないものをピックアップしました。
うまくいかなかった場合は、順番の若い順にインストールを試してみてください。
（それでもうまくいかなかった場合はコメントください。）

1. WSL2
2. Ubuntu 20.04
3. gow

公式ドキュメントの導入手順情報は、https://playwright.dev/python/docs/intro で確認できます。

## おすすめの機能

公式はこちら

* Official: https://playwright.dev/python/
* GitHub: https://github.com/microsoft/playwright-python

この中で、使い勝手の良い機能を簡単にピックアップしてご紹介します。


### Python コードをブラウザ操作で自動生成

これが僕の中で目玉機能です！
プログラマでなくても、簡単な Python コードを作れてしまう素晴らしい機能です！

自動生成するには、コマンドラインから実行します。
wikipedia.org を指定して起動する例として

```
playwright codegen wikipedia.org
```

終了するには、ブラウザを手動で閉じればOKです。

実際に作業している内容がこちら。

![playwright.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252217/62e91541-3663-822e-0016-f7ee38429d82.gif)

コードがコマンドプロンプトに自動生成されているのがわかります。

私の感覚ですと、コード自動生成したものがなにもせずそのまま完全に動くケースは6割くらいでした。
若干の手修正が必要となりますが、それでも一から作成することに比べれば、作業コストが雲泥の差です。
特に、要素セレクタをいちいち調べてコードに書き起こさなくていいところがポイントです！

`-o` で、テストコードをファイルにも出力してくれます。

```
playwright codegen wikipedia.org -o test.py
```

`--device` でスマホ環境が自動生成ができます。
iPhone は `iPhone 8`、`iPhone 11`、Android は `Pixel 2` を受け付けてくれました。`Pixel 3` はダメでした。

```
playwright codegen wikipedia.org --device "iPhone 11"
```

よく使うのはこの2つかと思います。
機能も結構豊富なので、他の機能など詳しくは下記で確認してください。
PDF 作れたりする機能もありますよ。

* 公式ドキュメント：https://playwright.dev/python/docs/cli
* コマンド： `playwright --help`

ここから先の説明は自動生成されたコードを修正していきます。
自動生成するためにブラウザ側で操作した手順は下記のとおりです。

1. wikipedia.org が表示される
2. 日本語 をクリック
3. 最近の出来事 をクリック

```
playwright codegen wikipedia.org -o test.py
```

自動生成されたコードはこちら

```python
from playwright.sync_api import sync_playwright

def run(playwright):
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context()

    # Open new page
    page = context.new_page()

    # Go to https://www.wikipedia.org/
    page.goto("https://www.wikipedia.org/")

    # Click text="日本語"
    page.click("text=\"日本語\"")
    # assert page.url == "https://ja.wikipedia.org/wiki/メインページ"

    # Click text="最近の出来事"
    page.click("text=\"最近の出来事\"")
    # assert page.url == "https://ja.wikipedia.org/wiki/Portal:最近の出来事"

    # Close page
    page.close()

    # ---------------------
    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)
```

テストコードの実行はこちら

```
python ./test.py
```


### コード実行時にブラウザの動作を見たい、見たくない

テストコードがきちんと実行されているか画面で動作を確認したい場合は `headless=False` を指定してあげればOKです。

```python
    browser = playwright.chromium.launch(headless=False)
```

テストコードがきちんと実行されているか画面で動作を確認する必要がない場合は `headless=True` を指定もしくは、何も指定しないでOKです。
Docker コンテナ化する際は基本こちらです。

```python
    browser = playwright.chromium.launch()
    # or
    browser = playwright.chromium.launch(headless=True)
```


### Firefox や Safari で動作を確認したい

> 【注意】
> テストで使用されるブラウザは、playwright 用に一部変更されているものです。
> 厳密な製品版ブラウザと全く同じではありません、とのことです。

テストコードは特に指定がない場合、chromium (Google Chrome の大もと) で実行されるように指定されています。
こちらをほかのブラウザで確認したい場合、`playwright.chromium` 部分を変更します。

この部分を

```python
    browser = playwright.chromium.launch(headless=False)
```

Firefox にしたいならこう、

```python
    browser = playwright.firefox.launch(headless=False)
```

Safari (Webkit) にしたいならこうすればOKです。

```python
    browser = playwright.webkit.launch(headless=False)
```


ブラウザ指定部分を別関数に分離したほうが使いやすいかと思います。
ですので、この部分を

```python
def run(playwright):
    browser = playwright.chromium.launch(headless=False)
```

このように分離すれば、同じテストコードでクロスブラウザテストが簡単にできます。

```python
def run(playwright):
    # PC Browser
    pc_browser_run(playwright.chromium)
    pc_browser_run(playwright.firefox)
    pc_browser_run(playwright.webkit)
    

def pc_browser_run(browser_type):
    browser = browser_type.launch(headless=False)
```

テストが不要なブラウザはコメントアウトすればよいので使い勝手が良いかなと思います。


### iPhone や Android で動作を確認したい

> 【注意】
> テストで使用されるブラウザは、PC用ブラウザをスマホ設定に変えているものです。
> 厳密な製品版ブラウザと全く同じではありません、とのことです。

こちらもほかのデバイス設定で確認したい場合はデバイス名を指定します。
`browser.new_context()` にデバイスを追加します。

この部分を

```python
    context = browser.new_context()
```

iPhone の『iPhone 11 Pro』にできるだけ設定を合わせるにはこう、

```python
    context = browser.new_context(**playwright.devices["iPhone 11 Pro"])
```

Android の『Pixel 2』にできるだけ設定を合わせるにはこう、

```python
    context = browser.new_context(**playwright.devices["Pixel 2"])
```

こちら、デバイス一覧がドキュメントのどこにも載っていなかったので、いろいろ試してみてください。
（playwright のソースコード読めばいいだけなんですが。。。そのうちちゃんと読みます）

デバイス指定部分を別関数に分離したほうが使いやすいかと思います。
ですので、この部分を

```python
def run(playwright):
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context()
```

このように分離すれば、同じテストコードでクロスモバイルデバイステストが簡単にできます。

```python
def run(playwright):
    # Mobile Browser
    mobile_browser_run(playwright.chromium, "Pixel 2")
    mobile_browser_run(playwright.webkit, "iPhone 11 Pro")
    

def mobile_browser_run(browser_type, device_name: str):
    browser = browser_type.launch(headless=False)
    context = browser.new_context(**playwright.devices[device_name])
```

テストが不要なデバイスはコメントアウトすればよいので使い勝手が良いかなと思います。


### スクリーンショットを撮影したい

資料作成やエビデンス保管のために、自動でスクリーンショットは撮りたいですよね。
こちらは簡単です。

スクリーンショットを取りたい部分に下記のコードを挿入するだけでOKです。

```python
    page.screenshot(path="./screenshot.png")
```

上記ではブラウザに表示される部分のみしか取得できません。
ページ全体をスクリーンショットしたい場合は、`full_page=True` を指定します。

```python
    page.screenshot(path="./screenshot.png", full_page=True)
```

対応フォーマットは PNG か JPG のようです。


### 自動テストされている様子を動画レコーディングしたい

こちらも自動で取得できるなら、動画レコーディングしておきたいですよね？
こちらも簡単です。

この部分を

```python
    context = browser.new_context()
```

こうします。
指定できるのは、ビデオサイズと出力先ディレクトリの2点のみです。

```python
    context = browser.new_context(
        record_video_dir="./videos",
        record_video_size={"height": 768, "width": 1024},
    )
```

対応フォーマットは WebM となります。
ファイル名を指定できないみたいです。ちょっと残念。


### ここまでのまとめ

自動生成されたコードを、上記の修正を加えたものがこちらです。
PC とスマートフォンでテストケースを分けています。

```python
from playwright.sync_api import sync_playwright


def run(playwright):
    # PC Browser
    pc_browser_run(playwright.chromium)
    pc_browser_run(playwright.firefox)
    pc_browser_run(playwright.webkit)

    # Mobile Browser
    mobile_browser_run(playwright.chromium, "Pixel 2")
    mobile_browser_run(playwright.webkit, "iPhone 11 Pro")


def pc_browser_run(browser_type):
    browser = browser_type.launch(headless=False)
    context = browser.new_context(
        record_video_dir="./videos",
        record_video_size={"height": 768, "width": 1024},
    )

    # Open new page
    page = context.new_page()

    # Go to https://www.wikipedia.org/
    page.goto("https://www.wikipedia.org/")

    # Click text="日本語"
    page.click('text="日本語"')
    # assert page.url == "https://ja.wikipedia.org/wiki/メインページ"

    # Click text="最近の出来事"
    page.click('text="最近の出来事"')
    # assert page.url == "https://ja.wikipedia.org/wiki/Portal:最近の出来事"

    page.screenshot(path="./screenshot.png", full_page=True)

    # Close page
    page.close()

    # ---------------------
    context.close()
    browser.close()


def mobile_browser_run(browser_type, device_name: str):
    browser = browser_type.launch(headless=False)
    context = browser.new_context(
        **playwright.devices[device_name],
        record_video_dir="./videos",
        record_video_size={"height": 768, "width": 1024},
    )

    # Open new page
    page = context.new_page()

    # Go to https://www.wikipedia.org/
    page.goto("https://www.wikipedia.org/")

    # Go to https://ja.m.wikipedia.org/wiki/メインページ
    page.goto("https://ja.m.wikipedia.org/wiki/メインページ")

    # Click div[id="fa_and_ga"] >> text="おまかせ表示"
    page.click('div[id="fa_and_ga"] >> text="おまかせ表示"')
    # assert page.url == "https://ja.m.wikipedia.org/wiki/0.999..."

    page.screenshot(path="./screenshot.png", full_page=True)

    # Close page
    page.close()

    # ---------------------
    context.close()
    browser.close()


with sync_playwright() as playwright:
    run(playwright)
```

## 注意点

* 最近 1.8.0a1 に大幅バージョンアップした影響で、0.x 系との互換性がありません。
 特に関数名が camelCase から snake_case に変更となっていますので、バージョン 0.x 系のソースはそのままコピペしても動きません。
* 1.8.0a1 から NodeJS と同じバージョンになりベータ版扱いではなくなったみたいらしいですが、公式にその表記がないのが少し気がかりです。
* スクレイピングはサイトによっては禁止されているケースがあります。注意してください。


## 感想

もともと Playwright は NodeJS 向けに開発されていたものを、Python 他言語でも使用できるようにされたものみたいです。
実は以前に調査したことはあったのですが、Python に対応していないのはもちろん、こんな充実した機能は実装されていませんでした。
開発スピードにただただ驚かされるばかりです。

自動テストコード機能が実装されているので、スキルが高くない方にもテストコードのひな型作成が頼みやすくなるかなと思っています。
まだまだ Playwright for Python の情報が少ないので、他の方の使い方も参考にしたいところです。

~~余談ですが今月末ごろ Python ウェビナー登壇する予定で、こちらのデモも行いたいなーと思っています。
（ウェビナー事前リハでOKがでましたら、またウェビナーの開催日時をここに書く予定です。）~~
ウェビナーでこちらのデモも行いますした。ご参加いただいた皆様、ありがとうございました！
(https://python-career.doorkeeper.jp/events/117944)