Back to Resources


Posted December 15, 2013

Remote file uploads with Selenium & Capybara


(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

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?

Dec 15, 2013
Share this post
Copy Share Link
© 2023 Sauce Labs Inc., all rights reserved. SAUCE and SAUCE LABS are registered trademarks owned by Sauce Labs Inc. in the United States, EU, and may be registered in other jurisdictions.