Thanks Driven Life

日々是感謝

Capybara(SeleniumWebDriver)でローカルマシンのファイルをリモートマシンのブラウザで attach_file させてみせる

概要

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 の FirefoxIE9 なんだよー」みたいな時ですね。

そういう時はだいたい

# -*- 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 を使います。

実体はこちらです http://code.google.com/p/selenium/source/browse/rb/lib/selenium/webdriver/common/driver_extensions/uploads_files.rb

素直にコメントの 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_detectorremote/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 を呼び出していると

  1. 一旦 upload(local_file) で指定したローカルファイルを(おそらく)selenium serverにアップロード
  2. アップロードした時の返り値が、おそらくリモートマシンが認識できる、1 でアップロードしたファイルのパス
  3. ↑ のファイルパスを打ち込む

という流れですね。きっと。

逆に「リモートマシンのファイルをそのまま使いたいんだ!」という時は、file_detector で与えた lambda さんが「ローカルマシンに御指定のファイルはねーぞ」って nil 返してくれるので、その場合はそのまま execute :sendKeysToElement を呼び出してくれるでしょう。

総括

file_detector 便利!

おまけ

file_detector の存在知るまでは

  1. あきらめてリモートマシンにもローカルマシンにも同じファイル設置しておく
  2. Capybara 起動時に Sinatra App 立ちあげて、リモートマシンからアクセスできるような Delivery server を設置

とかいろいろ考えてました。 特に 2 番は途中までうまくいってたんですけど、最終的に

  • ローカルファイルの絶対パス ('/path/to' 形式)
  • File URI Schema ('file://path/to')

以外は type="file" に sendKeys しても無反応だったので諦めました。file detector があってよかった