Uploading files with PhantomJS, GhostDriver and Python

For a while, our Selenium tests could not upload files to PhantomJS. That's a problem, if you want to test a platform for making phone calls easy — you do need to upload mp3 files. The tests would be stuck, or simply fail to upload the file without complaint. Now, after some digging, the problems and workarounds are clearer. Since it's been quite a bit of detective work, I'm documenting it here. Might end up being helpful to other people.

What we started with:

Uploading files freezes the test

The freezing comes down to a defect in Webkit and hence PhantomJS: Processing of user-gestures messes up automated file uploads. (Corresponding Ghost Driver issue) burkestar identified the defect on November 14th of 2014 and proposed a hotfix: Skipping processing of user gestures. We don't test with gestures, so that's fine with us.

Unfortunately, so far this fix has not been released. In the GitHub issue, a fix is promised for 2.0.1 … that was half a year ago.

So, we forked PhantomJS, applied the patch by stevepm and deployed that.

Problem solved!

As luck would have it, I immediately ran into the next bug.

incompatible type of argument(s) in call to _uploadFile

For debugging, I ran PhantomJS standalone and enabled all debug options. To connect to it, I used the Remote webdriver and Ghost Driver. After deploying our custom PhantomJS, I ran into this:

E               WebDriverException: Message: Error - incompatible type of argument(s) in call to _uploadFile(); candidates were
E                   _uploadFile(QString,QStringList)

This did not happen when using the PhantomJS driver, that starts and manages the PhantomJS process itself. What's going on?

Googled the error, found other people running into this. But no fix, and no explanation that held up. More googling. Reading the relevant Ghost Driver and PhantomJS source. Searching git histories.

What is uploadFile good for?

In Selenium, uploadFile is used as part of sendKeys. When you upload a file to a browser locally, all you need to do is set the filepath on a file input field. The browser then will grab the file and upload it. This does not work with Selenium, if the browser is remote, because the file is not on the remote machine. Setting the path is meaningless.

Here, uploadFile comes to the rescue: It transmits files to the remote browser, stores them locally, and returns their path. sendKeys can now send that new path, which is valid on the remote machine, and the remote browser will now grab the correct file and upload it.

That is why it works locally: uploadFile is not needed and not being used.

The defect

In the end, it was this Ghost Driver PR that introduced the problem. As Whooaa512 explains, the PR implements uploadFile, an undocumented Selenium feature, following suit of other Web Drivers. Unfortunately, as the exception shows, the PR did not get it right exactly.

The reference implementation, arguably, is the Selenium server. The responsible handler is the UploadFile class. It expects one parameter, file, which should contain the bytes of a zipped file. It unzips it, stores it temporarily, and then returns the path.

On the client side of things, the Java Web Driver implements this behavior by zipping one file and sending it as file parameter. So does the Python Web Driver.

PhantomJS, implementing the server-side for this behavior in its session handler, differs from this: It's _postUpload function parses the request as JSON and extracts a CSS selector and a filePath attribute. None of which our Python remote driver supplied.

The fix

The fix of course would be to implement the correct behavior in Ghost Driver: unzip the file, store it temporarily and send back the filepath. Issues to solve are backwards-compatibility for people already using the wrong behavior and cleanup of this temporary file.

Unfortunately, Ghost Driver is at the moment a quasi-abandoned project, so there is little point in investing all this effort. Also, PhantomJS would have to integrate that new version, and the project feels rather volatile, too. Therefore, I merely mentioned the problem and solution in the PhantomJS issue.

The workaround

Since uploadFile is broken in PhantomJS, you need to avoid using it. How to do that depends on whether PhantomJS is running on the same machine as your tests.

If it's locally, you could use the PhantomJS driver directly. If you need to use the Remote driver, or if PhantomJS is running remotely, you need to patch the driver to behave as if running locally.

With the Python Web Driver, you do this by setting the attribute _is_remote on the web driver to False. See source. Your code then would look like this:

is_remote = webdriver._is_remote
webdriver._is_remote = False
webdriver.find_element_by_css_selector('input').send_keys(filepath)
webdriver._is_remote = is_remote

If you use this more often, a simulate_local_execution decorator or context manager might be appropriate.

In case PhantomJS is running on another machine than the tests, you also need to make the files available on that machine. In your tests, you need to specify the path to the file on the remote machine.

Clearly, none of this is beautiful. It has the merit of working, though. :)