When using Selenium in automated tests, sometimes it becomes necessary to execute Javascript. Whether that’s due to the presence of a canvas that has no obvious click zones, or because of important information attached to the page that’s not visible from the DOM, here’s a trick that may help you out along the way.

All code is done in Java, but this should still apply to whichever Selenium client you may be using.

There are two modes of running scripts from Selenium. The first flavor involves running a standard synchronous script:

String pageTitle = "My Sync Page Title";
String format = "json";
String pageBody = (String) driver.executeScript(
    "let pageId = Framework.getPageId(arguments[0]);\n" +
    "let pageContent = Framework.getPageContent(pageId, arguments[1]);\n" +
    "let pageBody = pageContent['page-body'];\n" +
    "return pageBody;",
    pageTitle, format
);

Calling the above, you would expect the pageBody string in the Java code to be set to the page body we just retrieved in Javascript. Arguments [0] and [1] in the Javascript code are set to the two string values we passed in with the splat that driver.executeScript(String script, Object... args) takes.

What if the framework was asynchronous and involved promises? In that case, we have to pass in a callback to use the value. We won’t be able to return the value synchronously. Selenium provides another method for this, driver.executeAsyncScript(String script, Object... args):

String pageTitle = "My Async Page Title";
String format = "json";
String pageBody = (String) driver.executeAsyncScript(
    "Framework.getPageId(arguments[1]).then((pageId) => {\n" +
    "    Framework.getPageContent(pageId, arguments[2]).then((pageContent) => {\n" +
    "        let pageBody = pageContent['page-body'];\n" +
    "        let done = arguments[0];\n" +
    "        done(pageBody);\n" +
    "    });\n" +
    "});",
    pageTitle, format
);

In this case, the first argument [0] is now a return function that we have to call. In fact, the Java code will hang if our Javascript code never calls the return function.

ECMAScript 2017 introduced async/await syntax to combat code complexity. Instead of passing a callback into a “then” function, we await the promise for the expected value and continue on in a linear fashion. The catch is that awaiting a promise is only valid in an async function, which itself returns a promise of the expected result. In async/await terms, the above Javascript code might look like:

let tempFunc = async () => {
    let pageId = await Framework.getPageId(arguments[1]);
    let pageContent = await Framework.getPageContent(pageId, arguments[2]);
    let pageBody = pageContent['page-body'];
    return pageBody
};

tempFunc().then(arguments[0]);

The way in which the Selenium executeAsyncScript method executes our script in isn’t async itself, but we can still create an async function and execute it immediately to take advantage of the async/await syntax. With all the above in mind, we can circle back with a new approach to our above Java code:

String pageTitle = "My Async Page Title";
String format = "json";
String pageBody = (String) driver.executeAsyncScript(
    "(async () => {\n" +
    "    let pageId = await Framework.getPageId(arguments[1]);\n" +
    "    let pageContent = await Framework.getPageContent(pageId, arguments[2]);\n" +
    "    let pageBody = pageContent['page-body'];\n" +
    "    return pageBody;\n" +
    "})().then(arguments[0]);",
    pageTitle, format
);

This looks more like the synchronous script we got to use at the top! With a helper function, we can finally clean this up to its simplest form:

Object executePromiseScript(String script, Object... args)
{
    return driver.executeAsyncScript(
        "(async () => {\n" +
        script + "\n" +
        "})().then(arguments[0]);",
        args
    );
}
String pageTitle = "My Async Page Title";
String format = "json";
String pageBody = (String) executePromiseScript(
    "let pageId = await Framework.getPageId(arguments[1]);\n" +
    "let pageContent = await Framework.getPageContent(pageId, arguments[2]);\n" +
    "let pageBody = pageContent['page-body'];\n" +
    "return pageBody;",
    pageTitle, format
);

Happy testing!