Browserアクション
Browserアクションはchromedp を通して実際のChromeを操作します。ページを開き、内容を読み、入力し、要素を待ち、スクリーンショットを撮ります。Probeを実行する環境にChromeかChromiumが必要です。
外部アクションとしてmozership/probe-browser で公開しています。Probe v1.21.0までは組み込みアクションでした。ワークフローが初めて使うときにProbeがダウンロードし、固定したコミットのaction.ymlが示すSHA-256と一致する実行ファイルだけを実行します。Probe v1.21.0以降が必要です。各リリース のノートの先頭に、コピーして使うusesの行があります。
このアクションはページが何をするかを判定できないため、ガードに従いません。--read-onlyや--allow-hostの下では、--allow-actionで指定しない限り、このアクションを使うステップを拒否します。
基本的な構文
browserステップでは、行う操作をactionsに順番に並べます。ステップごとに新しいブラウザを起動し、操作を実行して閉じます。そのためクッキーやログイン状態は、そのままでは次のステップに引き継がれません。一連の流れを1つのステップにまとめるか、ログイン状態の引き継ぎのとおりsave_storage_stateでログイン状態を保存し、次のステップのブラウザをそこから始めます。
steps:
- name: Read the heading
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
actions:
- name: navigate
url: "{{vars.url}}/"
- name: text
id: heading
selector: h1
test: res.code == 0 && res.results.heading == "Welcome"パラメータ
以下はwithの直下に書きます。
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
actions | List | 必須 | 順に実行する操作。アクションを参照 |
timeout | 期間の文字列、または秒数 | 5s | ステップ全体の制限時間。ブラウザの起動とすべての操作を含む。30s、30、"30"はどれも30秒。それ以外の値や0以下の値はエラーになる |
headless | Boolean | true | ウィンドウを表示せずにChromeを動かす |
window_w | Integer | 1920 | ウィンドウの幅(ピクセル) |
window_h | Integer | 1080 | ウィンドウの高さ(ピクセル) |
evidence_dir | String | 一時ディレクトリ | 操作が失敗したときにページを保存する場所。失敗時のページを参照 |
storage_state | String | なし | save_storage_stateが書いたファイル。ブラウザはこのファイルのクッキーとlocalStorageを持った状態で始まる。ログイン状態の引き継ぎを参照 |
Chromeの起動にもtimeoutの一部を使い、1秒以上かかることもよくあります。短すぎる制限では、最初のページを開く前に時間切れになることがあります。
アクション
actionsの各要素は、nameで操作を指定し、次のフィールドを取ります。
| フィールド | 使うアクション | 説明 |
|---|---|---|
name | すべて | 下の表にある操作の名前 |
id | 結果を返すアクション | res.resultsやres.filepathsで結果を引くキー。省くとアクション名がキーになるため、同じ種類のアクションを後に置くと前の結果を置き換える |
url | navigate | 開くアドレス |
selector | 要素に対するアクション | CSSセレクタ |
value | type、send_keys、select | 使うテキスト |
attribute | get_attribute | 読む属性をhrefか[href]で指定する。リストの場合は最初の要素だけを読む |
quality | full_screenshot | 1から99はその品質のJPEGで保存する。それ以外は、省略、0、100以上、負の数も含めてPNGで保存する |
path | スクリーンショット、save_storage_state | スクリーンショットでは非推奨で、画像をこのパスにも書き出す。代わりにres.filepathsを使う。save_storage_stateでは、一時ディレクトリに新しいファイルを作る代わりに書き出す場所 |
equals | wait_text | テキストがこの値と一致するまで待つ。equals: ""は空のテキストになるまで待つ |
contains | wait_text | テキストがこの値を含むまで待つ |
matches | wait_text | テキストがこの正規表現に一致するまで待つ。正規表現はGoの構文 で書く |
wait_textにはequals、contains、matchesのうち1つまでしか書けず、ほかのアクションには書けません。空のcontainsやmatchesはどのテキストでも通るため、これもエラーになります。これらに反するワークフローは、ブラウザを起動する前にエラーになります。
要素を探すアクションは、要素が現れるまで待ちます。先にtimeoutが尽きるとステップは失敗します。多くは要素が表示されるまでも待ちます。
移動と待機
ページを開くか、次の操作に必要な状態になるまでステップを待たせます。
| アクション | 内容 |
|---|---|
navigate | urlを開き、ページの読み込みを待つ。net::ERR_CONNECTION_REFUSEDのようにブラウザがネットワークのエラーを返すとすぐに失敗する。応答のないアドレスではtimeoutまで待つ |
wait_visible | selectorが表示されるまで待つ |
wait_not_visible | selectorが表示されなくなるまで待つ |
wait_enabled | selectorが表示され、有効になるまで待つ |
wait_ready | ページのbodyの準備ができるまで待つ。selectorは使わない |
ページの読み取り
以下は文字列をres.resultsに、idまたはアクション名をキーにして入れます。
| アクション | 結果 |
|---|---|
text | selectorのテキスト |
wait_text | selectorが表示されるまで待ち、そのテキスト。equals、contains、matchesを書くと、テキストが期待どおりになるまでも待つ。テキストを待つを参照 |
value | inputなどのフォーム項目の値 |
get_attribute | selectorの、attributeの最初の属性の値。要素がその属性を持たなければ空文字列 |
get_html | 表示されたselectorの外側のHTML |
操作
利用者と同じようにページを操作します。結果は返しません。
| アクション | 内容 |
|---|---|
click | 表示されたselectorをクリックする |
double_click | 表示されたselectorをダブルクリックする |
type、send_keys | selectorの内容を消してからvalueを入力する |
submit | selectorが属するフォームを送信する |
focus | selectorにフォーカスを移す |
scroll | selectorが見える位置までスクロールする |
select | selectorの<select>が表示されたら、値がvalueの選択肢を、なければラベルがvalueの選択肢を選び、利用者が選んだときと同じくinputとchangeのイベントを発生させる。選択肢がない、選択肢が無効になっている、<select>が無効になっている、要素が<select>でない場合は、その理由を示してステップが失敗する。囲んでいる<optgroup>や<fieldset>のdisabledによる無効も含む。失敗のメッセージには選択肢の値が入る。<select multiple>では、すでに選ばれている選択肢に加える |
hover | selectorにmouseoverイベントを送る。待たず、一致する要素がなければ何もしない |
right_click | selectorにcontextmenuイベントを送る。待たず、一致する要素がなければ何もしない |
スクリーンショット
以下は画像をファイルに保存し、そのパスをres.filepathsに、idまたはアクション名をキーにして入れます。ファイル名の拡張子は形式に合わせて.pngか.jpgになります。
| アクション | 画像 |
|---|---|
capture_screenshot | ウィンドウに見えている範囲。PNG |
full_screenshot | ページ全体。PNG。qualityが1から99ならJPEG |
screenshot | 表示されたselectorだけ。PNG |
ストレージの状態
| アクション | 内容 |
|---|---|
save_storage_state | ブラウザのクッキーと、開いているページのオリジンのlocalStorageをJSONファイルに書き出し、そのパスをres.filepathsに、idまたはアクション名をキーにして入れる。ファイルはpathに書く。pathがなければ一時ディレクトリにprobe-browser-state-*.jsonを新しく作る。ログイン状態の引き継ぎを参照 |
レスポンスオブジェクト
すべての操作が成功すると、ステップからは次の値を参照できます。
| フィールド | 型 | 説明 |
|---|---|---|
res.code | Integer | 0 |
res.results | Object | 読み取りのアクションが返した値。キーはidまたはアクション名 |
res.filepaths | Object | スクリーンショットとストレージの状態の保存先。キーはidまたはアクション名 |
res.console_errors | List | 操作の実行中にページがコンソールに書いたエラー。書いた順に並ぶ。コンソールの確認を参照 |
rt.duration | String | ステップにかかった時間。"663.268333ms"など |
status | Integer | 0 |
操作が失敗した場合は、テストできるレスポンスはありません。ステップはエラーハンドリングのとおりアクションのエラーとして失敗します。
例
どの例も1つのステップです。1つのステップの操作は同じブラウザを共有し、次のステップは新しいブラウザで始まるためです。ただしログイン状態の引き継ぎは、ログイン状態を次のステップに渡すため、2つのステップからなります。
ログイン
ログインと、それが成功したことの確認は同じブラウザで行う必要があるため、1つのステップにします。
secrets:
- PASSWORD
vars:
url: "{{APP_URL}}"
user: "{{USERNAME}}"
password: "{{PASSWORD}}"
jobs:
- name: Sign in
steps:
- name: Log in and reach the dashboard
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
timeout: 30s
actions:
- name: navigate
url: "{{vars.url}}/login"
- name: type
selector: "#username"
value: "{{vars.user}}"
- name: type
selector: "#password"
value: "{{vars.password}}"
- name: click
selector: "button[type='submit']"
- name: wait_visible
selector: "#dashboard"
- name: text
id: greeting
selector: "#dashboard h1"
test: res.code == 0 && res.results.greeting contains vars.user複数の値の読み取り
結果が互いに置き換わらないよう、読み取りのアクションにはそれぞれidを付けます。
- name: Product page
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
actions:
- name: navigate
url: "{{vars.url}}/products/42"
- name: text
id: title
selector: "h1"
- name: text
id: price
selector: ".price"
- name: get_attribute
id: image
selector: "img.product"
attribute: [src]
test: |
res.code == 0 &&
res.results.title != "" &&
res.results.price startsWith "$"
outputs:
image_url: res.results.imageテキストを待つ
データを取得して表示するページでは、読み込みから少し遅れてデータが表示されます。そのため、要素が表示された時点でtextで読むとLoadingが返り、テストが失敗することがあります。wait_textにequals、contains、matchesを書くと、テキストが期待どおりになるまで読み直します。
- name: Order list
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
timeout: 30s
actions:
- name: navigate
url: "{{vars.url}}/orders"
- name: wait_text
id: status
selector: "#status"
matches: '^\d+ orders$'
test: res.code == 0
outputs:
status: res.results.status先にステップのtimeoutが尽きると、最後に読んだテキストを示すアクションのエラーとしてステップが失敗し、失敗時のページのとおりページを保存します。
wait_text "#status": the text was "Loading", want it to match "^\\d+ orders$": context deadline exceeded (page at failure: ...)ログイン状態の引き継ぎ
ログインするステップでsave_storage_stateを使ってログイン状態を保存し、後のステップでstorage_stateにそのファイルを指定すると、後のステップのブラウザはログインした状態で始まります。ステップごとにログインし直す必要はありません。
- name: Sign in
id: login
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
timeout: 30s
actions:
- name: navigate
url: "{{vars.url}}/login"
- name: type
selector: "#username"
value: "{{vars.user}}"
- name: type
selector: "#password"
value: "{{vars.password}}"
- name: click
selector: "button[type='submit']"
- name: wait_visible
selector: "#dashboard"
- name: save_storage_state
id: state
test: res.code == 0
outputs:
state: res.filepaths.state
- name: Orders
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
storage_state: "{{outputs.login.state}}"
actions:
- name: navigate
url: "{{vars.url}}/orders"
- name: wait_text
selector: "h1"
equals: "Orders"
test: res.code == 0ファイルには、ブラウザが持つすべてのサイトのクッキー(HttpOnlyのものも含む)と、保存したときに開いていたページのオリジンのlocalStorageが入ります。別のオリジンのlocalStorageも残すには、そのオリジンのページを開いた状態で保存します。ファイルはPlaywrightのstorage state と同じ形式なので、どちらのツールで書いたファイルももう一方で読めます。
storage_stateを指定したブラウザは、最初の操作の前にクッキーを読み込みます。localStorageは、項目のあるオリジンごとにそのオリジンのページを開いて書き込みます。このページへのリクエストにはブラウザの中で空のページを返すため、サーバーには届かず、アプリのスクリプトも動きません。最初の操作が始まるときには、ブラウザはabout:blankに戻っています。ファイルがない場合や形式が違う場合は、ブラウザを起動する前にステップが失敗します。ファイルを書くステップが失敗していた場合も同じです。
クッキーを使えばそのユーザーとしてログインできるため、ファイルはProbeを実行したユーザーだけが読めるようにします。pathを指定しなければ一時ディレクトリに書くため、リポジトリには入りません。pathを指定する場合は、バージョン管理の対象と、CIでアップロードするファイルから外してください。
コンソールの確認
クリックの処理で例外が起きるなど、ページは表示に出ないところで壊れていることがあります。res.console_errorsには、操作の実行中にページがコンソールにエラーとして書いたものが入ります。console.errorの呼び出し、失敗したconsole.assert、捕まえられなかった例外と、処理されなかったPromiseのrejectです。各要素は次のフィールドを持ちます。
| フィールド | 説明 |
|---|---|
type | error、assert、exceptionのいずれか |
text | DevToolsのコンソールに表示される内容。例外ではUncaughtで始まり、PromiseのrejectではUncaught (in promise)で始まる |
url、line、column | エラーが発生した場所。行と列はDevToolsと同じく1から数える。ブラウザが場所を返さない場合は空と0 |
画像の読み込みの失敗のようなブラウザ自身のメッセージや、ログと警告は入りません。エラーはページのタブから読むため、ポップアップや、別のサイトのiframeのエラーも入りません。
エラーが1つでもあればステップを失敗させるには、リストが空であることをテストします。
- name: Checkout page
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
actions:
- name: navigate
url: "{{vars.url}}/checkout"
- name: click
selector: "#pay"
- name: wait_visible
selector: "#receipt"
test: res.code == 0 && len(res.console_errors) == 0
echo: "{{res.console_errors}}"操作が失敗したときは、失敗のメッセージに最初のいくつかのエラーが入ります。操作が失敗した原因がそのエラーであることが多いためです。
... context deadline exceeded (console errors: "Uncaught TypeError: Cannot read properties of null (reading 'total') (http://localhost:8090/checkout.js:42:17)") (page at failure: ...)スクリーンショットを残す
保存した画像のパスはres.filepathsに入り、表示したり次に渡したりできます。
- name: Checkout page
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
window_w: 1280
window_h: 800
actions:
- name: navigate
url: "{{vars.url}}/checkout"
- name: full_screenshot
id: page
test: res.code == 0
echo: "Saved {{res.filepaths.page}}"エラーハンドリング
アクションが完了できないと、ステップ全体がアクションのエラーとして失敗し、実行は終了ステータス3で終わります。セレクタが何にも一致しなくても空の結果にはなりません。wait_visible、text、clickなど要素を探すアクションは、ステップのtimeoutまで待ち続けます。ナビゲーションは、接続の拒否のようにブラウザがネットワークのエラーを返すとすぐに失敗しますが、応答のないアドレスではtimeoutまで待ちます。いずれの場合もテストできるresはなく、失敗のメッセージにはブラウザのエラーと、コンソールの確認のとおりページがコンソールに書いたエラーが入ります。
失敗時のページ
アクションが失敗すると、Probeはその時点のページを保存します。ページ全体のPNGのスクリーンショットと、文書のHTMLです。2つのファイルにはprobe-browser-failure-1947177907.pngと.htmlのように同じ名前が付きます。失敗のメッセージには、ページのURLとあわせて両方のファイルのパスが入ります。
... context deadline exceeded (page at failure: url http://localhost:8090/, screenshot /tmp/probe-browser-failure-1947177907.png, html /tmp/probe-browser-failure-1947177907.html)--reportで書き出すファイルでも、このメッセージが失敗のmessageになります。テストがfalseになるのはアクションの失敗ではないため、そのときはページを保存しません。テキストが期待どおりでないときに保存させるには、testで確かめる代わりにwait_textにequals、contains、matchesを書きます。どんな場合でもページを残すにはcapture_screenshotアクションを加えます。
ファイルはシステムの一時ディレクトリに保存します。evidence_dirを指定するとそこに保存し、ディレクトリは必要に応じて作成します。CIのアーティファクトとしてアップロードする場合に便利です。ページには個人の情報が写りうるため、ファイルはProbeを実行したユーザーだけが読めるようにします。
- name: Checkout page
uses: github.com/mozership/probe-browser@634baa8dff2b273ed326996fec4d91c2dba9d38b # v0.2.0
with:
evidence_dir: out/browser
actions:
- name: navigate
url: "{{vars.url}}/checkout"
- name: wait_visible
selector: "#pay"ページを読み取れるように、ブラウザはタイムアウトのあとも最大10秒残します。ブラウザが起動しなかった場合や、時間切れの時点でページがまだ読み込み中だった場合など、読み取れなかったときはその旨をメッセージに書き、何も保存しません。
CIでの実行
CIのランナーには、ブラウザと、手元より少し長い時間が必要です。
- ランナーにChromeかChromiumを入れる。
headless: trueのままにするか、ウィンドウが必要ならXvfbなどのディスプレイを用意する。- コンテナで多く必要になるため、Chromeは
--no-sandboxで起動する。信頼できるサイトだけを開く。 - コールドスタートのランナーではChromeの起動が遅いため、
timeoutに余裕を持たせる。