9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Playwright for Elixir で Web UI を操作、 E2E テストを実行する

Last updated at Posted at 2024-07-28

はじめに

Playwright は JavaScript でブラウザを操作し、 E2E (End to End) のテストを自動化するフレームワークです

スクリーンショットを撮ることで直接的に画面の状態が確認できます

"Playwright" = 「脚本家」という意味で、ブラウザに「こういう操作して」という脚本を書くイメージですね

まだプレビューバージョンではありますが、 Playwright for Elixir があったので Livebook から実行してみます

実装したノートブックはこちら

Playwright のインストールには Node.js が必要です

先にインストールしておきましょう

セットアップ

Livebook で新しいノートブックを開きます

以下のコードを実行し、必要なモジュールをインストールします

  • Kino: Livebook の UI/UX (Playwright 自体には不要)
  • Image: スクリーンショットを扱うために使用
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!()

スクリーンショット 2024-07-28 15.46.12.png

このように、特定の操作をした状態の画面を残せるため、出力を含めてノートブックを保存しておけば、そのまま E2E テストのエビデンスとして使えます

要素の指定

Playwright.Page.query_selector で JavaScript や CSS と同じように要素を指定できます

指定した部分だけのスクリーンショットを取得することも可能です

page
|> Playwright.Page.query_selector("nav[aria-label='Global']")
|> Playwright.ElementHandle.screenshot()

実行結果

nav.png

指定した要素内のテキストを取得する場合は 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!()

実行結果

org.png

page
|> Playwright.Page.query_selector("footer nav dl:last-child")
|> Playwright.ElementHandle.scroll_into_view()

page
|> Playwright.Page.screenshot()
|> Base.decode64!()
|> Image.from_binary!()

実行結果

footer.png

クリック

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!()

questions.png

Playwright.Page.url で現在の URL を取得できます

Playwright.Page.url(page)

実行結果

"https://qiita.com/question-feed"

DOM 上には存在していても、 UI 上で表示されていない要素(display:noneheight: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!()

search.png

テキスト入力

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!()

input.png

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!()

elixir.png

E2E テスト

ExUnit を使った単体テストと同じように E2E テストを記述することができます

Livebook から実行する場合、必ずテストモジュールの定義前に ExUnit.start を実行してください

ExUnit.start()

テストモジュールを定義し、 assert などを使って検証します

モジュールの先頭で use ExUnit.Case, async: trueuse 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)

スクリーンショット 2024-07-28 17.37.09.png

まとめ

Playwright for Elixir を使うことで、 E2E テストが実装できました

Phoenix.ConnTest でも UI 操作をテストできますが、 Playwright では JavaScript や CSS の動作含めテストできます

Alpine.js の動作を確認したいときなど、いろいろなケースで活用できそうです

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?