「参照…」ボタンを押すとSeleniumが止まる
Rubyでフォーム入力を自動化するSeleniumスクリプトを書いていて、最後の関門になりがちなのがファイルアップロードです。画面上の「ファイルを選択」ボタンをクリックすると、ブラウザではなくOS側のネイティブなファイル選択ダイアログが開きます。このダイアログはブラウザの外側で動作するウィンドウなので、SeleniumのDOM操作は一切届きません。button.clickまではエラーなく成功するのに、その先のダイアログでスクリプトが固まったように見えるのは、そもそもSeleniumの管轄外にある画面を相手にしてしまっているからです。
回避策としてOS標準のキーボード操作を自動化するツールを組み合わせる方法もありますが、OSごとにダイアログの見た目や操作手順が違ううえ、CI環境のヘッドレスブラウザでは動かせないことも多く、実行環境に強く依存してしまいます。もっと単純で環境を選ばない方法があります。
隠れたinput[type=file]要素に直接パスを渡す
多くのファイルアップロードUIは、見た目上は装飾されたボタンでも、裏側には標準のinput[type=file]要素が存在しています。「ファイルを選択」ボタンはこのinput要素をクリックさせるためのラッパーに過ぎません。そこでSeleniumからは、ボタンをクリックしてダイアログを開くのではなく、input[type=file]要素そのものを探し出し、send_keysでアップロードしたいファイルの絶対パスを直接渡します。
def upload_file(driver, file_path)
raise "ファイルが存在しません: #{file_path}" unless File.exist?(file_path)
file_input = driver.find_element(:css, "input[type='file']")
file_input.send_keys(File.expand_path(file_path))
end
send_keysにパスを渡すと、ブラウザはOSのダイアログでユーザーがファイルを選択したのと同じ状態をinput要素にセットします。ダイアログ自体は一度も開かれないため、ヘッドレス環境でもそのまま動作します。ポイントはFile.expand_pathで必ず絶対パスに変換しておくことで、相対パスのまま渡すとカレントディレクトリの解釈がドライバー側とスクリプト側でずれ、ファイルが見つからないエラーになることがあります。
displayなしの要素にはsend_keysが効かないことがある
ここで頻繁にはまるのが、input[type=file]要素が見た目を整えるためにdisplay: noneやvisibility: hiddenで隠されているケースです。ブラウザによっては、非表示の要素に対するsend_keysをそのまま黙って無視したり、ElementNotInteractableErrorを返したりします。
def upload_file_safely(driver, file_path)
file_input = driver.find_element(:css, "input[type='file']")
full_path = File.expand_path(file_path)
begin
file_input.send_keys(full_path)
rescue Selenium::WebDriver::Error::ElementNotInteractableError
# display:noneなどで操作不可の場合は、一時的に表示状態へ切り替えてから再試行する
driver.execute_script(<<~JS, file_input)
arguments[0].style.display = 'block';
arguments[0].style.visibility = 'visible';
arguments[0].style.opacity = '1';
JS
file_input.send_keys(full_path)
end
end
display: noneだけをblockに戻しても、親要素や祖先要素側にvisibility: hiddenやopacity: 0が指定されているとやはり操作できないままなので、代表的な3プロパティをまとめて上書きしています。見た目が一瞬崩れるのが気になる場合は、アップロード完了後に元のスタイルへ戻す処理を追加しておくと安全です。
複数ファイルをまとめてアップロードする
input要素にmultiple属性が付いている場合は、send_keysに渡す文字列を改行(\n)で連結することで、複数ファイルを一度に選択した状態にできます。
def upload_multiple_files(driver, file_paths)
file_input = driver.find_element(:css, "input[type='file'][multiple]")
paths = file_paths.map { |path| File.expand_path(path) }
file_input.send_keys(paths.join("\n"))
end
multiple属性が付いていないinputに対してこの方法を使うと、ブラウザによっては最後の1件しか反映されなかったり、エラーになったりするため、事前にelement.attribute("multiple")で属性の有無を確認しておくと事故を防げます。
アップロード後、本当に反映されたかを確認する
send_keysが例外を投げなかったからといって、サーバー側へのアップロードが完了したとは限りません。実際にはブラウザ内でファイルが選択された状態になっただけで、その後の非同期アップロード処理が別途走っているUIも多くあります。多くの画面では、アップロードが完了するとファイル名やサムネイル、進捗バーの完了表示が現れるので、それを待ち受けて初めて成功と判断します。
def wait_for_upload_complete(driver, file_name, timeout_seconds = 10)
deadline = Time.now + timeout_seconds
while Time.now < deadline
uploaded = driver.find_elements(:css, ".uploaded-file-name")
.any? { |el| el.text.include?(file_name) }
return true if uploaded
sleep 0.3
end
false
end
タイムアウトはファイルサイズや対象サービスの回線状況に依存するため、小さなテストファイルで実測しつつ、余裕を持たせた秒数を設定するのがおすすめです。
まとめ
- OSのファイル選択ダイアログはSeleniumの操作対象外なので、ボタンをクリックしてダイアログを開く発想自体を捨てる
- 裏側にある
input[type=file]要素を直接探し出し、send_keysに絶対パスを渡すことでダイアログを介さずアップロードできる display: noneなどで隠されたinput要素はsend_keysが効かないことがあり、その場合はJSで一時的にdisplay・visibility・opacityを書き換えてから再試行するmultiple属性があるinputには、複数の絶対パスを改行で連結して渡すことで一度に複数ファイルを選択できるsend_keysの成功はファイル選択の成功に過ぎず、サーバーへの反映が完了したかはファイル名表示などのUI変化をポーリングして別途確認する


コメント