This will be a really short article explaining how to download image from the browser side while running tests using the WebdriverIO library.

The problem of downloading images

I have a use case scenario, that requires to verify that a loaded page has the expected image. The main problem that in order to save image we have not so many options:

  • Save using the system save dialog. The problem is that we are not able to control system dialogs using the Webdriver API
  • Get the url and download it locally. This will not work if an image is protected and requires cookies or any other kind of authorization
  • Execute a script on the browser side that will download an image, converts it into a transportable format and finally sends the result back. However this method also not without its difficulties.

Async nature of Javascript

The only one method I have found working for this case is the third one. But the issue here is that network communication, IO operations are executed in an async manner in order not to block the UI thread. Let’s start by creating a simple script that will just download an image by a URL and convert the downloaded image into a Base64 string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
window.downloadImageToBase64 = function (url, callback) {
                        // Put these constants out of the function to avoid creation of objects.
                        var STATE_DONE = 4;
                        var HTTP_OK = 200;
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function () {
                            // Wait for valid response
                            if (xhr.readyState == STATE_DONE && xhr.status == HTTP_OK) {
                                var blob = new Blob([xhr.response], {
                                    type: xhr.getResponseHeader("Content-Type")
                                });
                                // Create file reader and convert blob array to Base64 string
                                var reader = new window.FileReader();
                                reader.readAsDataURL(blob);
                                reader.onloadend = function () {
                                    var base64data = reader.result;
                                    callback(base64data);
                                    console.log(base64data);
                                }

                            }
                        };
                        xhr.responseType = "arraybuffer";
                        // Load async
                        xhr.open("GET", url, true);
                        xhr.send();
                        return 0;
                    };

You can check this function, it should print a base64 string representing an image to the console and send the result back to the callback if it was provided.

As you can see there are two async calls in the function, the first is the XHR request and the second one is reading blob data using FileReader. That’s why we cannot get the response immediately.

The JSONWire protocol

The JSON Wire protocol

I think it is always worth knowing how do things work under the hood. All communication with between client libraries and browser drivers is done via the JSON Wire Protocol. Therefore all data is sent in the plain JSON format. This can help you to understand how all this works internally and debug issues. You can use the RequestDebug library to log all request coming back and forth.

The executeAsync call

The webdriverio library provides two methods for executing scripts on the browser side, execute and executeAsync. The former function is synchronous, so the result of evaluating the script is returned to the client after the script execution is finished. In our case it this function won’t work because of async calls in the downloadImageToBase64 function.

The executeAsync function is exactly what we are looking fore. The last argument of the function is a callback function that should be called from a script being executed on the browser side when it completes. Before we can use executeAsync we need to set the script execution timeout. According the documentation it should be set to 30 seconds by default, however in my case it doesn’t even wait for a couple seconds and the following error is thrown: Error: asynchronous script timeout: result was not received in 0 seconds.. You can set the timeout as follows.

1
client.timeouts('script', SCRIPT_EXECUTION_TIMEOUT)

We need to inject our function in the context of a browser used for running tests in order to use it in a script later. Of course you can just pass all functions in a single executeAsync call, but I would prefer a bit cleaner way. In my case I need several helper functions that are used on the browser side, so I have defined a custom command that sets all these utils/helpers functions into the global (window) context of a browser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  client.addCommand("injectHelperScripts", function () {
            var self = this;
            return self.execute(
                function injectScripts() {
                    window.addListener = function (element, eventName, handler) {
                        if (element.addEventListener) {
                            element.addEventListener(eventName, handler, false);
                        }
                        else if (element.attachEvent) {
                            element.attachEvent('on' + eventName, handler);
                        }
                        else {
                            element['on' + eventName] = handler;
                        }
                    };
                    window.removeListener = function (element, eventName, handler) {
                        if (element.addEventListener) {
                            element.removeEventListener(eventName, handler, false);
                        }
                        else if (element.detachEvent) {
                            element.detachEvent('on' + eventName, handler);
                        }
                        else {
                            element['on' + eventName] = null;
                        }
                    };
                    window.downloadImageToBase64 = function (url, callback) {
                        var STATE_DONE = 4;
                        var HTTP_OK = 200;
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function () {
                            // Wait for valid response
                            if (xhr.readyState == STATE_DONE && xhr.status == HTTP_OK) {
                                var blob = new Blob([xhr.response], {
                                    type: xhr.getResponseHeader("Content-Type")
                                });
                                // Create file reader and convert blob array to Base64 string
                                var reader = new window.FileReader();
                                reader.readAsDataURL(blob);
                                reader.onloadend = function () {
                                    var base64data = reader.result;
                                    callback(base64data);
                                    console.log(base64data);
                                }

                            }
                        };
                        xhr.responseType = "arraybuffer";
                        // Load async
                        xhr.open("GET", url, true);
                        xhr.send();
                        return 0;
                    };

                }
            );
        });

Finally let’s create a custom command for wrapping the downloadImageToBase64 function.

1
2
3
4
5
6
7
8
   client.addCommand('getBinaryImage', function (url) {
            var self = this;
            return self.executeAsync(
                function downloadImageBinary(url, callback) {
                    return downloadImageToBase64(url, callback);
                }
                , url);
        });

Downloading images using Webdriver

Now it is time to test our code. Personally I am using Selenium docker images, or rather the Selenium Hub image with multiple worker nodes. Docker always helps me to keep my machine clean. As a result in my case I am using remote connection. Here is the working example for downloading an image in the Base64 format from amazon website.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  static downloadImageFromAmazon(url, imageId) {
        let client = webdriverio.remote(TestRunner.generateWebDriverOptionsChrome())
            .init();
        client = TestRunner.setCustomCommandsForClient(client);
        let chain = client
            .timeouts('script', TestRunner.SCRIPT_TIMEOUT)
            .url(url)
            .waitForVisible('#' + imageId, TestRunner.DELAY_VISIBLE)
            .injectHelperScripts()
            .getAttribute('#' + imageId, "src");
        return chain.then((src) => {
            console.log(src);
            return chain.getBinaryImage(src);
        })
            .then((result) => {
                return result.value;
            })
            .catch(function (err) {
                console.log('Error ' + err);
            });
    }

Please note that this image is hosted on the other domain than amazon, therefore by default we are not able to execute the XHR request because of the Same-origin policy. Appropriately, you may need to disable security features of the browser. In the event of the chrome driver I have the following configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  static generateWebDriverOptionsChrome() {
        return {
            desiredCapabilities: {
                browserName: 'chrome',

                chromeOptions: {
                    args: ['disable-web-security']
                },
                loggingPrefs: {
                    'driver': 'INFO',
                    'browser': 'INFO'
                }
            },
            logLevel: 'verbose',
            host: 'localhost',
            port: 4444
        };
    }

Now you can try to download a book cover image by a url like that.

1
2
3
4
5
TestRunner.downloadImageFromAmazon("https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164", "imgBlkFront")
    .then((result) => {
        console.log("GOT BASE64 IMAGE");
        console.log(result);
    });

In my case I got the following base64 image. Open Developer Tools and get the src attribute of the image.

Downloaded Base64 image

Here is an utility class that you may need to save a base64 encoded image to a file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const isPng = require('is-png');
const fs = require('fs'),
    path = require('path');

class FileUtils {
    constructor() {

    }

    static isValidPngFile(filename) {
        let buffer = [];
        try {
            buffer = fs.readFileSync(filename);
        } catch (err) {
            return false;
        }
        return isPng(buffer);
    }

    static ensureDirectoryExistence(filePath) {
        let dirname = path.dirname(filePath);
        if (fs.existsSync(dirname)) {
            return true;
        }
        FileUtils.ensureDirectoryExistence(dirname);
        fs.mkdirSync(dirname);
    };

    static saveBase64Image(base64Image, filename) {
        var imageTypeRegularExpression = //(.*?)$/;
        let imageBuffer = FileUtils.decodeBase64Image(base64Image);
        var imageTypeDetected = imageBuffer
            .type
            .match(imageTypeRegularExpression);
        filename = filename + "." + imageTypeDetected[1];
        FileUtils.ensureDirectoryExistence(filename);
        // Save decoded binary image to disk
        fs.writeFileSync(filename, imageBuffer.data);
        return filename;
    }

    static decodeBase64Image(dataString) {
        var matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/);
        var response = {};

        if (matches.length !== 3) {
            return new Error('Invalid input string');
        }

        response.type = matches[1];
        response.data = new Buffer(matches[2], 'base64');

        return response;
    }

}

module.exports = FileUtils;

Conclusion

In this short how-to guide I shared my experience about dealing with image downloading using the WebdriverIO library. Nonetheless, the main intention of this article is to show how you can use custom commands and scripts execution on the browser side to get data from a page. If you have any troubles or suggestions please leave comments below.