はじめに
Playwright は JavaScript でブラウザを操作し、 E2E (End to End) のテストを自動化するフレームワークです
スクリーンショットを撮ることで直接的に画面の状態が確認できます
"Playwright" = 「脚本家」という意味で、ブラウザに「こういう操作して」という脚本を書くイメージですね
まだプレビューバージョンではありますが、 Playwright for Elixir があったので Livebook から実行してみます
実装したノートブックはこちら
Playwright のインストールには Node.js が必要です
先にインストールしておきましょう
セットアップ
Livebook で新しいノートブックを開きます
以下のコードを実行し、必要なモジュールをインストールします
Mix.install([
{:playwright, "~> 1.44.0-alpha.2"},
{:kino, "~> 0.13"},
{:image, "~> 0.53.0"}
])
実行環境上で初めて Playwright を使用する場合、以下のコードでブラウザなどをインストールします
Playwright.SDK.CLI.install()
初回実行時のみ、以下のように Cromium がインストールされます
10:47:17.879 [info] Installing playwright browsers and dependencies
10:47:54.918 [info] Downloading Chromium 125.0.6422.26 (playwright build v1117) from https://playwright.azureedge.net/builds/chromium/1117/chromium-mac-arm64.zip
| | 0% of 134.6 MiB
|■■■■■■■■ | 10% of 134.6 MiB
|■■■■■■■■■■■■■■■■ | 20% of 134.6 MiB
...
Web サイトの表示
ブラウザを起動します
画面サイズを指定することで、モバイルで表示したときなど、レスポンシブ対応の UI も確認できます
# Chromium ブラウザを起動
{:ok, browser} = Playwright.launch(:chromium, %{})
# 新しいページを開く
page = Playwright.Browser.new_page(browser)
# 画面サイズの設定 (iPhone SE)
Playwright.Page.set_viewport_size(page, %{width: 375, height: 667})
Playwright.Page.goto
で Web サイトの URL を指定してアクセスします
# Web サイトにアクセス
Playwright.Page.goto(page, "https://qiita.com/")
Playwright.Page.screenshot
で画面のスクリーンショットを取得し、 Base64 の文字列として返します
# スクリーンショットを取得
base64 = Playwright.Page.screenshot(page)
実行結果
"iVBORw0KGgoAAAANSUhEUgAAAXcAAAKbCAIAAACBzF3NAAAgAElEQVR4n" <> ...
画像を Livebook 上で表示してみましょう
tensor =
base64
|> Base.decode64!()
|> Image.from_binary!()
このように、特定の操作をした状態の画面を残せるため、出力を含めてノートブックを保存しておけば、そのまま E2E テストのエビデンスとして使えます
要素の指定
Playwright.Page.query_selector
で JavaScript や CSS と同じように要素を指定できます
指定した部分だけのスクリーンショットを取得することも可能です
page
|> Playwright.Page.query_selector("nav[aria-label='Global']")
|> Playwright.ElementHandle.screenshot()
実行結果
指定した要素内のテキストを取得する場合は Playwright.ElementHandle.text_content
を使用します
page
|> Playwright.Page.query_selector("nav[aria-label='Global'] ol li:nth-child(2) a")
|> Playwright.ElementHandle.text_content()
実行結果
"Question"
ブラウザ操作
スクロール
Playwright.ElementHandle.scroll_into_view
によって、指定した要素が画面内に見えるまでスクロールします
page
|> Playwright.Page.query_selector("nav[aria-label='Global'] ol li:last-child a")
|> Playwright.ElementHandle.scroll_into_view()
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
実行結果
page
|> Playwright.Page.query_selector("footer nav dl:last-child")
|> Playwright.ElementHandle.scroll_into_view()
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
実行結果
クリック
Playwright.Page.click
で指定要素をクリックします
Playwright.Page.click(page, "nav[aria-label='Global'] ol li:nth-child(2) a")
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
Playwright.Page.url
で現在の URL を取得できます
Playwright.Page.url(page)
実行結果
"https://qiita.com/question-feed"
DOM 上には存在していても、 UI 上で表示されていない要素(display:none
や height:0
になっている要素)は操作できません
# 検索アイコンをクリックしないと表示されない要素
Playwright.Page.click(page, "form[aria-label='Search'] input")
操作できない要素を操作しようとすると、以下のようなエラーが発生します
{:error, %Playwright.SDK.Channel.Error{message: "Timeout 30000ms exceeded."}}
操作のデフォルトのタイムアウトが 30 秒に設定されているため、 30 秒経過してからエラーになります
即時エラーを返して欲しい場合、 第3引数に %{force: true}
を指定します
その場合、以下のようなエラー文言になります
15:30:05.868 [error] GenServer #PID<0.2060.0> terminating
** (RuntimeError) %Playwright.SDK.Channel.Error{message: "Element is not visible"}
...
また、操作のレスポンスに 30 秒以上かかるような場合、 第3引数に %{timeout: 60_000}
のようにタイムアウト時間を指定します
要素が見えているかどうかは Playwright.ElementHandle.is_visible
で判定します
page
|> Playwright.Page.query_selector("form[aria-label='Search']:nth-child(2) input")
|> Playwright.ElementHandle.is_visible()
操作した後、特定の要素が表示されるまで待つ場合は Playwright.Page.wait_for_selector
を使います
Playwright.Page.click(page, "header button")
Playwright.Page.wait_for_selector(page, "form[aria-label='Search']:nth-child(2) input")
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
テキスト入力
Playwright.Page.fill
で指定した要素にテキストを入力します
Playwright.Page.fill(page, "form[aria-label='Search']:nth-child(2) input", "Elixir")
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
Playwright.Page.press
で任意のキーを入力できます
Playwright.Page.press(page, "form[aria-label='Search']:nth-child(2) input", "Enter")
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()
E2E テスト
ExUnit を使った単体テストと同じように E2E テストを記述することができます
Livebook から実行する場合、必ずテストモジュールの定義前に ExUnit.start
を実行してください
ExUnit.start()
テストモジュールを定義し、 assert
などを使って検証します
モジュールの先頭で use ExUnit.Case, async: true
と use PlaywrightTest.Case
を実行しておくことが重要です
テストケースの最後に Playwright.Page.close
することも忘れないようにしましょう
defmodule Sample.TabsTest do
use ExUnit.Case, async: true
use PlaywrightTest.Case
describe "Click tabs" do
test "click all tabs", %{browser: browser} do
page = Playwright.Browser.new_page(browser)
Playwright.Page.set_viewport_size(page, %{width: 375, height: 667})
[
%{
tab_index: 1,
text: "Trend",
title: "Qiita",
url: "https://qiita.com/"
},
%{
tab_index: 2,
text: "Question",
title: "Question - Qiita",
url: "https://qiita.com/question-feed"
},
%{
tab_index: 3,
text: "Official Event",
title: "Official Events - Qiita",
url: "https://qiita.com/official-events"
},
%{
tab_index: 4,
text: "Official Column",
title: "Official Columns - Qiita",
url: "https://qiita.com/official-columns"
},
%{
tab_index: 5,
text: "signpostCareer",
title: "Qiita x Findy エンジニアのキャリアを支援するコラボレーションページ - Qiita",
url: "https://qiita.com/opportunities/findy"
},
%{
tab_index: 6,
text: "Organization",
title: "organization一覧 - Qiita",
url: "https://qiita.com/organizations"
}
]
|> Enum.map(fn map ->
Playwright.Page.goto(page, "https://qiita.com/")
tab_selector = "nav[aria-label='Global'] ol li:nth-child(#{map.tab_index}) a"
assert page
|> Playwright.Page.query_selector(tab_selector)
|> Playwright.ElementHandle.text_content() == map.text
Playwright.Page.click(page, tab_selector)
assert Playwright.Page.title(page) == map.title
assert Playwright.Page.url(page) == map.url
page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> then(&File.write!("/tmp/tab-#{map.tab_index}-clicked.png", &1))
end)
Playwright.Page.close(page)
end
end
end
定義したテストを実行します
ExUnit.run()
実行結果
Running ExUnit with seed: 862499, max_cases: 16
.
Finished in 10.6 seconds (10.6s async, 0.00s sync)
1 test, 0 failures
%{total: 1, failures: 0, excluded: 0, skipped: 0}
失敗した場合は以下のようになります
Running ExUnit with seed: 862499, max_cases: 16
1) test Click tabs click all tabs (Sample.TabsTest)
tmp/livebook_data/dev/autosaved/2024_07_28/02_05_z3kp/playwright.livemd#cell:fjwi4eh2n3cjxkuf:6
Assertion with == failed
code: assert page |> Playwright.Page.query_selector(tab_selector) |> Playwright.ElementHandle.text_content() ==
map.text
left: "Official Column"
right: "Official Columns"
stacktrace:
tmp/livebook_data/dev/autosaved/2024_07_28/02_05_z3kp/playwright.livemd#cell:fjwi4eh2n3cjxkuf:53: anonymous fn/2 in Sample.TabsTest."test Click tabs click all tabs"/1
(elixir 1.17.2) lib/enum.ex:1703: Enum."-map/2-lists^map/1-1-"/2
(elixir 1.17.2) lib/enum.ex:1703: Enum."-map/2-lists^map/1-1-"/2
tmp/livebook_data/dev/autosaved/2024_07_28/02_05_z3kp/playwright.livemd#cell:fjwi4eh2n3cjxkuf:48: (test)
Finished in 7.8 seconds (7.8s async, 0.00s sync)
1 test, 1 failure
%{total: 1, failures: 1, excluded: 0, skipped: 0}
テスト中に出力しておいたスクリーンショットを表示して確認します
1..6
|> Enum.map(fn index ->
Image.open!("/tmp/tab-#{index}-clicked.png")
end)
|> Kino.Layout.grid(columns: 3)
まとめ
Playwright for Elixir を使うことで、 E2E テストが実装できました
Phoenix.ConnTest でも UI 操作をテストできますが、 Playwright では JavaScript や CSS の動作含めテストできます
Alpine.js の動作を確認したいときなど、いろいろなケースで活用できそうです