Selenium, jQuery and File uploads

Selenium

One of the contracts I’ve been working on recently is working with Gurock building a test automation system for a PHP application, their test management app TestRail. As well as building the instrastructure for the application testing and the API testing I’ve once again been involved in the nitty-gritty of testing a web application with Selenium and all the fun that involved.

And actually it has been fun. We’ve had a bunch of issues to overcome and despite the usual pain and trauma and running round in circles we seem to have overcome most of them and have a test suite that is robust against the three different platforms we’re testing against.

For those who don’t know Selenium WebDriver interface allows you to connect your Python tests, or just about any language you care to choose, and test you web application as a “black box” - interacting with it in the same way as your users do. These are “functional tests”, as opposed to unit tests”, that tests the whole application as a whole meets its specifications and rquirements. As will all testing you can’t guarantee that it makes your applcation bug-free, but you can eleminiate whole classes of bugs and gurarantee a minimum level of application quality.

This application is written in PHP, but we’re using Python, py.test and Selenium to automate the tests and the front end is built with jQuery. There are various fun aspects of testing this app that we’ve encountered. A couple of these stem from the fact that like any modern any web application much of the UI updates are done from AJAX calls. This means that there’s no global page state ready event to wait for to know that load has finished and the page is ready to interact with.

One of the plugins in use is the BlockUI plugin. This puts a semi-opaque overlay over the user interface in the browser whilst asynchronous AJAX requests are being made, to prevent other elements of the user interface being interacted with. As the request is an asynchronous one the browser isn’t blocked so our Selenium tests don’t know that the user interface is blocked and it should wait before attempting any more interactions. This causes tests to fail with the dreaded error:

Exception in thread "main" org.openqa.selenium.WebDriverException: unknown error: Element <input type="button" class="btn btn-default" data-toggle="modal" data-target="#adduser" data-localize="adduser" value="Add user"> is not clickable at point (1397, 97). Other element would receive the click: <div class="blockUI blockOverlay" style="z-index: 1000; border: none; margin: 0px; padding: 0px; width: 100%; height: 100%; top: 0px; left: 0px; background-color: rgb(0, 0, 0); cursor: wait; position: absolute; opacity: 0.304712;"></div>

The dreaded part is specifically is not clickable at point (1397, 97). Other element would receive the click: <div class="blockUI blockOverlay".

The “blockUI” element is intercepting the click because the AJAX request is not completed, or more to the point a blockUI element is intercepting it. The normal way round this would be to find the “blockU” element and wait for it to no longer be displayed. Unfortunately there’s more than one of them! So this is the code we came up with to wait until none of them are displayed:

from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.common.by import By

class GeneralLocators:

    blockUI = (By.CLASS_NAME, "blockUI")
    busy = (By.CLASS_NAME, "busy")


def any_elements_displayed(elements):
    for element in elements:
        try:
            if element.is_displayed():
                return True
        except StaleElementReferenceException:
            pass
    return False

class BasePageElement(object):

    def wait_for_blockui_to_close(self, seconds=5):
        self.driver.implicitly_wait(0)
        try:
            stop = time.time() + seconds
            while time.time() < stop:
                blockUIs = self.driver.find_elements(*GeneralLocators.blockUI)
                if not any_elements_displayed(blockUIs):
                    return
                time.sleep(0.1)
            raise TimeoutException("Timed out waiting for blockUI to go away")
        finally:
            self.driver.implicitly_wait(10)

We have a similar problem with AJAX elements that don’t block the page, but take several seconds to update, showing a busy indiciator whilst they’re updating. Again, we need to wait for the busy indicators to complete before we ineract with any of the elements. Thanksfully that is similarly easy. Note that we set the global implicitly_wait timeout to zero whilst we’re checking.

    def wait_until_not_busy(self, seconds=5):
        self.driver.implicitly_wait(0)
        try:
            stop = time.time() + seconds
            while time.time() < stop:
                busy = self.driver.find_elements(*GeneralLocators.busy)
                if not any_elements_displayed(busy):
                    return
                time.sleep(0.1)
            raise TimeoutException("Timed out waiting to not be busy")
        finally:
            self.driver.implicitly_wait(10)

It’s well worth noting that with the selenium library in Python, the implicitly_wait value is a global. Setting it anywhere sets it for the rest of the session.

We put all the element locators into classes, like GeneralLocators so that as locators change (inevitable in an evolving user interface) there is only one place to change the locators rather than having them scattered through out our code.

Here’s a few more tricks and trips we’ve discovered along the way. Whilst text boxes have a nice and straightforward .clear() method to clear existing text in them, this dioesn’;t work with a textarea (which confusingly enough has a .clear() method which apppears to do nothing. The right way to clear to a text box is to send a CTRL-A followed by a backspace:

        # CTRL-A plus BACKSPACE are needed for selenium to clear the textarea as .clear() doesn't work.
        self.send_keys_to_element(CustomizationsLocators.add_custom_field_description, Keys.CONTROL + "a")
        self.send_keys_to_element(CustomizationsLocators.add_custom_field_description, Keys.BACKSPACE)

If you want to provide a command line option to run the tests with a headless browser, this little function (firefox only) will do the trick. You could further customize is to switch between browers:

import pytest
from selenium import webdriver
from selenium.webdriver.firefox.options import Options

	def get_driver():
	    options = Options()
	    if pytest.config.getoption('headless'):
		options.headless = True
	    return webdriver.Firefox(options=options)

And these final two are interesting. Uploading files with Selenium. Because the file upload dialog is a native dialog it’s very hard to interact with Selenium (impossible I thin.). However it does come along with a hidden input field that you can enter file paths directly to. So for a normal file dialog this works fine:

    from selenium.webdriver.common.by import By
    file_inputs = driver.find_elements(By.CSS_SELECTOR, 'input.dz-hidden-input')
    input_element = file_inputs[input_index]
    driver.execute_script('arguments[0].style = ""; arguments[0].style.display = "block"; arguments[0].style.visibility = "visible";', input_element)
    time.sleep(0.1)
    input_element.send_keys(filename)

So long as you know, or work out by trial and error, which file input dialog to send the input to it will work fine. The useful thing is that it exposes all the hidden file inputs in the user interace so you can see what you’re interacting with.

This still unfortunately doesn’t work for file uploads by dropzone, some kind of javascript extension. For this you need to base64 encode the file yourself and attach it to the dropzone. Made all the more interesting by the fact that the driver.execute_script api will only take a single line of input. Still, it works!! As horrible as it is, this works!! It takes the base64 encoded version of the file and attaches it to the dropzone element as sa blob, with the filename attached as metadata.

    def add_dropzone_attachment(self, locator, attachment_path):
        filename = os.path.basename(attachment_path)
        with open(attachment_path, 'rb') as f:
            content = f.read()
        content = base64.b64encode(content).decode('ascii')

        script = (
            "var myZone, blob, base64Image; myZone = Dropzone.forElement('{}');"
            "base64content = '{}';"
            "function base64toBlob(r,e,n)var c=new Blob(a,);return c}}"
            "blob = base64toBlob(base64content, 'image/png');"
            "blob.name = '{}';"
            "myZone.addFile(blob);"
        ).format(locator, content, filename) 
        self.driver.execute_script(script)

The locator is the locator of the dropzone area itself, usually something like #attachmentDropzone.

Hopefully all this painfully won information proves useful to someone!

Written on November 26, 2018