alert/confirm/promptは「HTML要素」ではない
Seleniumで自動化スクリプトを書いていると、window.alertやwindow.confirmが出た瞬間にスクリプトが固まってしまうことがあります。原因はシンプルで、これらのJSダイアログは通常のDOM要素と違い、find_elementで探すこともクリックすることもできないからです。ブラウザのレンダリング領域の外側、いわばブラウザ本体が描画する「モーダル」であり、WebDriverからは専用のAPIを通してしか触れません。
これを知らずに、いつも通りfind_element(:css, "button")のようなコードを書いてしまうと、裏でダイアログが表示されたまま次のコマンドが永遠にタイムアウトする、という事故につながります。まずは「ダイアログはページの中の要素ではなく、別レイヤーの存在」という前提を押さえておきましょう。
switch_to.alertで扱う:accept/dismiss/send_keys
ダイアログを操作する窓口はdriver.switch_to.alertです。ダイアログが実際に表示されているときだけSelenium::WebDriver::Alertオブジェクトを返し、表示されていなければNoSuchAlertErrorを投げます。
alert = driver.switch_to.alert
alert.text # ダイアログの文言を取得
alert.accept # OK(確認)を押す
alert.dismiss # キャンセルを押す
promptのようにテキスト入力欄を持つダイアログでは、acceptする前にsend_keysで値を入力します。ここはコピペで大丈夫です。
def handle_prompt(driver, input_text)
alert = driver.switch_to.alert
alert.send_keys(input_text)
alert.accept
end
confirmを「キャンセルしたい」場合はacceptの代わりにdismissを呼ぶだけです。呼び分けを間違えると、本来キャンセルしたかった処理が実行されてしまいます。テストコードでは「OKを押したときの分岐」と「キャンセルを押したときの分岐」の両方を明示的に書いておくと事故が減ります。
ダイアログが出るまで待つ
厄介なのは、ダイアログを出すトリガー(ボタンのクリックなど)を実行した直後にswitch_to.alertを呼んでも、ブラウザ側の表示処理が終わっていないタイミングだとNoSuchAlertErrorになってしまう点です。逆に、ダイアログが出ないと思い込んでswitch_to.alertを呼ばずに次へ進むと、実際には出ていたダイアログに他の全コマンドがブロックされて固まります。
そこで、明示的な待機ヘルパーを用意しておきましょう。「ダイアログが出るまでポーリングし、出たらAlertオブジェクトを返す」という形です。
def wait_for_alert(driver, timeout = 5)
Selenium::WebDriver::Wait.new(
timeout: timeout,
ignore: Selenium::WebDriver::Error::NoSuchAlertError
).until { driver.switch_to.alert }
end
alert = wait_for_alert(driver)
puts alert.text
alert.accept
ignoreにNoSuchAlertErrorを指定しているのがポイントです。これによりダイアログがまだ表示されていない間の例外を「待機失敗」ではなく「まだ来ていないだけ」として扱い、タイムアウトまでポーリングを続けてくれます。alert.textで文言が拾えれば、ダイアログをちゃんと捕まえられている証拠です。
Basic認証はswitch_to.alertでは扱えない
Basic認証のログインダイアログも、見た目はalertと同じようにブラウザが出す入力用ポップアップです。ですが、これはHTTPレベルの認証チャレンジに対してブラウザ本体が表示するネイティブなダイアログで、JavaScriptのダイアログとは別物です。そのためdriver.switch_to.alertを呼んでも捕まえられず、NoSuchAlertErrorになるか、ブラウザによってはそもそもWebDriverから一切操作できません。
これを知らずに「Basic認証もswitch_to.alertでどうにかなるはず」と決め打ちして実装すると、いつまで経ってもダイアログを閉じられず、後続の操作がすべてタイムアウトするという詰み方をします。
URLに認証情報を埋め込んで突破する
実務でよく使われる回避策は、Basic認証のダイアログ自体を出さないようにすることです。https://user:pass@host/の形式でURLにユーザー名とパスワードを埋め込んでアクセスすると、多くのブラウザはダイアログを出さずに認証情報を送信してくれます。
require 'uri'
def navigate_with_basic_auth(driver, url, user, password)
uri = URI.parse(url)
uri.user = user
uri.password = password
driver.navigate.to(uri.to_s)
end
# navigate_with_basic_auth(driver, "https://example.com/secure/", "dummy_user", "dummy_pass")
注意点として、この方式はページ内でのリダイレクトを挟むと認証情報が引き継がれないことがあり、一部のブラウザ・バージョンでは同一オリジンへの再アクセス時に改めてダイアログが表示されることもあります(このあたりの挙動はブラウザのバージョンで変わるので、実際に対象環境で一度確認しておいたほうがいいです)。認証情報はコード中に直書きせず、環境変数など外から注入する形にしてください。テストコードとはいえ、ここは妥協しない方がいいと思っています。
出ないはずのダイアログで固まる事故を防ぐ
想定していないタイミングでconfirmやalertが出てしまい、スクリプト全体が固まる。これはSeleniumでよくあるトラブルです。防ぐ手段の1つが、ブラウザ起動時に「予期しないダイアログが出たときどう振る舞うか」を指定しておくことです。
options = Selenium::WebDriver::Options.chrome
options.unhandled_prompt_behavior = :dismiss # accept / dismiss / ignore などを指定可能
driver = Selenium::WebDriver.for(:chrome, options: options)
dismissを指定しておけば、想定外のダイアログが出ても自動的にキャンセルされて処理が先に進みます。固まる代わりに「想定外のダイアログが出た」という事実だけがログや例外として残るようになるので、これで大丈夫です。あわせて、重要な操作の周りは例外を明示的に拾っておくと原因の切り分けがしやすくなります。
def with_alert_guard(driver)
yield
rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError => e
warn "予期しないダイアログを検出しました: #{e.message}"
begin
driver.switch_to.alert.dismiss
rescue Selenium::WebDriver::Error::NoSuchAlertError
# 既に閉じられていた場合は何もしない
end
raise
end
本番で使う認証情報やアクセス先のURLは、コードやログに直接残さないことに注意してください。必ず環境変数や秘匿された設定ファイルから読み込むようにしましょう。
まとめ
window.alert/confirm/promptはDOM要素ではなく、find_elementでは触れない。driver.switch_to.alert経由でaccept/dismiss/send_keysを使う- ダイアログはトリガー直後にまだ表示されていないことがあるため、
NoSuchAlertErrorをignoreに指定したWaitでポーリングしてから操作する - Basic認証のダイアログはJSダイアログとは別物で、
switch_to.alertでは扱えない - Basic認証は
https://user:pass@host/形式でURLに埋め込むことで、ダイアログを出さずに突破できる(リダイレクトを挟むと引き継がれないことがある点に注意) unhandled_prompt_behaviorをあらかじめ指定しておくと、想定外のダイアログが出てもスクリプト全体が固まるのを防げる
Basic認証まわりはブラウザのバージョンで挙動が変わりやすいところなので、うちの環境ではこうだった、くらいの温度感で受け取ってもらえればと思います。もっとスマートなやり方があれば、コメントで教えてください。


コメント