Systemテストのドライバ事前読み込みでブラウザバイナリを明示的に設定
Rails のシステムテストで、Selenium 4.45 で削除された options.binary の自動設定を復元し、Chrome バイナリが見つからないエラーを解消します。
背景
Selenium 4.45 では DriverFinder がオプションオブジェクトを変更して options.binary を設定していた副作用が削除されました。その結果、Rails のシステムテストがドライバパスだけを取得し、ブラウザバイナリが未設定のまま Selenium が起動しようとするため、SessionNotCreatedError: cannot find Chrome binary が発生します。この regression は、Chrome が PATH に無く Selenium Manager のみがブラウザを提供している環境で顕在化します。同様の報告は SeleniumHQ の Issue #17698 でも取り上げられています。Rails では Browser#preload がこの副作用に依存していたため、バージョン更新でテストが失敗する状態になっていました。
Browser#preload の目的は、並列実行時のレースコンディションを防ぐためにドライバパスを事前に解決しキャッシュすることです。従来は DriverFinder.new(...).driver_path の呼び出しにより、内部で options.binary が暗黙的に設定されていました。副作用が除去されたことで、ドライバは取得できてもブラウザパスは残らず、テスト実行が止まっていました。この状況を解消するために、本 PR は resolve_driver_path に明示的なバイナリ設定を追加します。
技術的な変更
resolve_driver_path では Selenium 4.20.0 以降で DriverFinder のインスタンスを生成し、取得した driver_path と browser_path をそれぞれ Service.driver_path と options.binary に代入するよう変更されました。まず driver_finder = ::Selenium::WebDriver::DriverFinder.new(options, namespace::Service.new) でインスタンス化し、namespace::Service.driver_path = driver_finder.driver_path でドライバパスを設定します。続いて options.binary = driver_finder.browser_path if driver_finder.browser_path? により、ブラウザバイナリが利用可能な場合のみ明示的に設定されます。このロジックは Selenium バージョンに依存せず、Gem::Version の比較で分岐させているため、既存のコードフローを保持しつつ新機能を提供します。
@@
- namespace::Service.driver_path = ::Selenium::WebDriver::DriverFinder.new(options, namespace::Service.new).driver_path
+ driver_finder = ::Selenium::WebDriver::DriverFinder.new(options, namespace::Service.new)
+ namespace::Service.driver_path = driver_finder.driver_path
+ options.binary = driver_finder.browser_path if driver_finder.browser_path?
テストスイートには preloads browser's driver_path and binary with DriverFinder という新しいケースが追加され、DriverFinder が返す driver_path と browser_path が正しく設定されることを検証しています。スタブで Selenium::WebDriver::SeleniumManager.binary_paths を { "driver_path" => "chromedriver", "browser_path" => "chrome" } に置き換え、ActionDispatch::SystemTesting::Driver を生成して実際の Service.driver_path と browser.options.binary を取得します。アサーションでは chromedriver が Service.driver_path に、chrome がブラウザオプションの binary に設定されていることを確認します。既存の driver_path が設定済みの場合は上書きされないことを assert_no_changes で確認し、後方互換性が保たれていることを示しています。
@@
- test "preloads browser's driver_path with DriverFinder if a path isn't already specified" do
+ test "preloads browser's driver_path and binary with DriverFinder if a path isn't already specified" do
@@
- assert_equal found_executable, ::Selenium::WebDriver::Chrome::Service.driver_path
+ assert_equal "chromedriver", ::Selenium::WebDriver::Chrome::Service.driver_path
+ assert_equal "chrome", driver.instance_variable_get(:@browser).options.binary
設計判断
本修正の核心は、DriverFinder の副作用に依存せず、返却値を用いて設定を明示的に行う設計に変更した点です。副作用は内部実装の変更に脆弱であり、今回の Selenium 4.45 の変更で問題が顕在化したため、直接代入に置き換えることで安定性が向上します。resolve_driver_path 内のロジックは数行の追加で済むため、呼び出し元や公開インターフェースを変更する必要がなく、利用者への影響は最小限です。また、options.binary が既に設定されているケースでは条件分岐で上書きを防ぐため、既存環境の動作を維持します。
Selenium のバージョンチェック (Gem::Version.new(::Selenium::WebDriver::VERSION) >= Gem::Version.new("4.20.0")) により、古いバージョンでも従来の DriverFinder.path 呼び出しを保持しています。この分岐は既存コードの動作を保ちつつ、新しいバイナリ設定ロジックを安全に導入できるトレードオフです。結果として、現在サポートしている Selenium バージョン範囲内での動作が保証され、テストの追加により変更が意図した通りに機能することが CI で検証されています。
まとめ
Browser#preload が Selenium 4.45 の変更に追随し、ドライバパスとブラウザバイナリの両方を明示的に設定するようになったため、Chrome が PATH に無くてもシステムテストが正常に起動します。副作用依存を排除した設計により、既存のテスト環境への影響は最小限で、互換性が保たれています。