alert/confirm/promptは「HTML要素」ではない
Seleniumで自動化スクリプトを書いていると、window.alertやwindow.confirmが出た瞬間にスクリプトが固まってしまう、という経験をすることがあります。原因は単純で、これらのJSダイアログは通常のDOM要素と違い、find_elementで探すこともclickすることもできないからです。ブラウザのレンダリング領域の外側、いわばブラウザ本体が描画する「モーダル」であり、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を指定しているのがポイントで、これによりダイアログがまだ表示されていない間の例外を「待機失敗」ではなく「まだ来ていないだけ」として扱い、タイムアウトまでポーリングを続けます。
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をあらかじめ指定しておくと、想定外のダイアログが出てもスクリプト全体が固まるのを防げる


コメント