概要
Capybara(selenium-webdriver) + Turnip でテスト書いてて
- ファイルアップロードの試験やりたいな
- でもいちいちファイル作るのもめんどいし Tempfile でいいわ
みたいな状況になると、このようなステップが表れると思います。
require 'tempfile' step "ファイルを添付する" do tempfile = Tempfile.new tempfile.write 'My name is gongo!' tempfile.rewind attach_file('#upload_file', tempfile.path) end
概ね好調なのですが、ひとつだけ問題があります
リモートマシンのブラウザで実行すると失敗する
当然と言えば当然です。ローカルマシン(Ruby実行してるやつ)で作成した一時ファイルパスを
リモートマシンで起動したブラウザで input type="file"
に入力しても、
そのマシンにはそのファイルが存在しないので何もできません。
その解決方法です。実はちゃんと SeleniumWebDriver が標準で解決機能もってました。さすが。
そのまえに寄り道 (リモートマシンのブラウザで実行するための設定)
Capybara 実行時に起動するブラウザをローカル(ruby 実行している)マシンではなく、 ある特定のリモートマシンにインストールされているブラウザで行いたい時があると思います。
「ruby 実行するのは Ubuntu 12.04 だけど、検証したいブラウザは Windows の Firefox と IE9 なんだよー」みたいな時ですね。
そういう時はだいたい
# -*- coding: utf-8 -*- Capybara.register_driver :remote_windows do |app| # # Firefox を使いたい場合はこちら # caps = Selenium::WebDriver::Remote::Capabilities.firefox # caps = Selenium::WebDriver::Remote::Capabilities.internet_explorer # # host: リモートマシンのIPアドレス # port: リモートマシンで起動している selenium サーバのポート # url = "http://#{host}:#{port}}/wd/hub/" # # リモートマシンを使うぞ!という宣言の browser: :remote # opts = { desired_capabilities: caps, browser: :remote, url: url } Capybara::Selenium::Driver.new(app, opts) end Capybara.default_driver = :remote_windows
とかやるとよいでしょう。
本題 (解決策)
Selenium::WebDriver::Driver#file_detector
を使います。
素直にコメントの Example の通りにやると上手くいきます。
Capybara.register_driver :remote_windows do |app| caps = Selenium::WebDriver::Remote::Capabilities.internet_explorer url = "http://#{host}:#{port}}/wd/hub/" opts = { desired_capabilities: caps, browser: :remote, url: url } driver = Capybara::Selenium::Driver.new(app, opts) driver.browser.file_detector = lambda do |args| str = args.first.to_s str if File.exist? str end driver end
こうすることで attach_file
で指定したファイルが、
Selenium Server 経由でリモートマシンのブラウザからアクセスできるように
SeleniumWebDriver がはからってくれます。便利!!
もうちょい深く潜ってみる
driver_extensions/uploads_files.rb
の中身をもう一回みると、
bridge.file_detector = detector
となっています。detector ってのは str を返す lambda ですね。
この bridge.file_detector
は remote/bridge.rb で使用しています。
def sendKeysToElement(element, keys) if @file_detector && local_file = @file_detector.call(keys) keys = upload(local_file) end execute :sendKeysToElement, {:id => element}, {:value => Array(keys)} end def upload(local_file) unless File.file?(local_file) raise WebDriverError::Error, "you may only upload files: #{local_file.inspect}" end execute :uploadFile, {}, :file => Zipper.zip_file(local_file) end
Capybara::Node::Actions#attach_file
からどんどん下っていくと、最終的に sendKeysToElement()
に到達します。element は input type="file"
に対応するオブジェクトで、 keys というのが attach_file()
で指定したローカルファイルパスです。
さきほどの driver.file_detector
を呼び出さない場合、ローカルファイルパスをそのまま file field に打ち込むのですが、もし driver.file_detector
を呼び出していると
- 一旦
upload(local_file)
で指定したローカルファイルを(おそらく)selenium serverにアップロード - アップロードした時の返り値が、おそらくリモートマシンが認識できる、1 でアップロードしたファイルのパス
- ↑ のファイルパスを打ち込む
という流れですね。きっと。
逆に「リモートマシンのファイルをそのまま使いたいんだ!」という時は、file_detector
で与えた lambda さんが「ローカルマシンに御指定のファイルはねーぞ」って nil 返してくれるので、その場合はそのまま execute :sendKeysToElement
を呼び出してくれるでしょう。
総括
file_detector
便利!
おまけ
file_detector
の存在知るまでは
- あきらめてリモートマシンにもローカルマシンにも同じファイル設置しておく
- Capybara 起動時に Sinatra App 立ちあげて、リモートマシンからアクセスできるような Delivery server を設置
とかいろいろ考えてました。 特に 2 番は途中までうまくいってたんですけど、最終的に
以外は type="file"
に sendKeys しても無反応だったので諦めました。file detector があってよかった