(This post is a little code journey. If you just want to know how to enable remote uploads in Capybara, skip to the end) Usually a file upload is for a file on the same computer as the browser you're uploading with: User on System 1: Upload C:/files/selfie.jpg to Facebook Browser on System 1: Opening C:/files/selfie.jpg, Uploading.... Done! Nice haircut! When you're using a remote browser (say, when you're using one of our 157+ platforms), it still looks for files as though they were on the same system. However, the file you're trying to upload only exists on YOUR system, so it can't find it: User on System 1: Upload C:/files/dignity.jpg to Facebook Browser on System 2: Opening C:/fi...... Ack, dignity.jpg doesn't exist, WAT DO 0_0 Selenium 2 uses FileDetectors to fix this problem for us. When you've got a FileDetector set, any file path you pass to a file input element (with the "send_keys" method) is sent to the FileDetector. If it decides that file path is a (local) file, Selenium will upload the (local) file to the (remote) browser's server. It will then set the (remote) file path as the value of the file input field. Bang! Automagic remote file uploads. So how do the Ruby bindings work? Let's go check out the Selenium gem source!
# The detector is an object that responds to #call, and when called # will determine if the given string represents a file. If it does, # the path to the file on the local file system should be returned, # otherwise nil or false.
So, as long as the object we set as a file_detector can verify that a passed in string is a file path, and then return that path, we can add uploads to Capybara. Sweet! Kinda. See, there's a catch - Capybara doesn't provide direct access to the Selenium driver object, which is kinda the point of Capybara. We'll have to get access to it:
selenium_driver = page.object.browser
Let's create a file_detector object and pass it to selenium_driver. It needs to respond to 'call'. You know what responds to 'call' and doesn't require us lazy, lazy Ruby programmers to create (and then instantiate) a whole new class? Lambdas.
selenium_driver.file_detector = lambda do |args|
# Check that the first arg is really a file, for realz
Hmm, actually, we might be getting ahead of ourselves. It would suuuuuck to have to change all our tests to find file inputs and call send_keys and all that junk, so let's check out the Capybara DSL method "attach_file" to see what changes we have to make to it:
### lib/capybara/node/actions.rb
def attach_file(locator, path, options={})
Array(path).each do |p|
raise Capybara::FileNotFound, "cannot attach file, #{p} does not exist" unless File.exist?(p.to_s)
end
find(:file_field, locator, options).set(path)
end
### lib/capybara/selenium/node.rb
def set(value)
# SNIP #
elsif tag_name == 'input' and type == 'file'
path_names = value.to_s.empty? ? [] : value
native.send_keys(*path_names)
Oh, awesome! Check out the highlighted lines -- Capybara will already check that the file path we're providing exists, before it's even passed to the file_detector. That means we don't even need to *do* anything in our file_detector lambda! The entirety of what we need to do to give us remote file uploading in Capybara is below: The Solution
## Allows remote uploads. Totally awesomesauce (labs).
selenium_driver = page.object.browser
selenium_driver.file_detector = lambda {|args| args.first.to_s}
Isn't Ruby great?