Understanding execute async script in Selenium

33,494

Solution 1

Here's the reference to the two APIs (well it's Javadoc, but the functions are the same), and here's an excerpt from it that highlights the difference

[executeAsyncScript] Execute an asynchronous piece of JavaScript in the context of the currently selected frame or window. Unlike executing synchronous JavaScript, scripts executed with this method must explicitly signal they are finished by invoking the provided callback. This callback is always injected into the executed function as the last argument.

Basically, execSync blocks further actions being performed by the selenium browser, while execAsync does not block and calls on a callback when it's done.


Since you've worked with protractor, I'll use that as example. Protractor uses executeAsyncScript in both get and waitForAngular

In waitForAngular, protractor needs to wait until angular announces that all events settled. You can't use executeScript because that needs to return a value at the end (although I guess you can implement a busy loop that polls angular constantly until it's done). The way it works is that protractor provides a callback, which Angular calls once all events settled, and that requires executeAsyncScript. Code here

In get, protractor needs to poll the page until the global window.angular is set by Angular. One way to do it is driver.wait(function() {driver.executeScript('return window.angular')}, 5000), but that way protractor would pound at the browser every few ms. Instead, we do this (simplified):

functions.testForAngular = function(attempts, callback) {
  var check = function(n) {
    if (window.angular) {
      callback('good');
    } else if (n < 1) {
      callback('timedout');
    } else {
      setTimeout(function() {check(n - 1);}, 1000);
    }
  };
  check(attempts);
};

Again, that requires executeAsyncScript because we don't have a return value immediately. Code here


All in all, use executeAsyncScript when you care about a return value in a calling script, but that return value won't be available immediately. This is especially necessary if you can't poll for the result, but must get the result using a callback or promise (which you must translate to callback yourself).

Solution 2

When should I use execute_async_script() instead of the regular execute_script()?

When it comes to checking conditions on the browser side, all checks you can perform with execute_async_script can be performed with execute_script. Even if what you are checking is asynchronous. I know because once upon a time there was a bug with execute_async_script that made my tests fail if the script returned results too quickly. As far as I can tell, the bug is gone now so I've been using execute_async_script but for months beforehand, I used execute_script for tasks where execute_async_script would have been more natural. For instance, performing a check that requires loading a module with RequireJS to perform the check:

driver.execute_script("""
// Reset in case it's been used already.
window.__selenium_test_check = undefined;
require(["foo"], function (foo) {
    window.__selenium_test_check = foo.computeSomething();
});
""")

result = driver.wait(lambda driver: 
    driver.execute_script("return window.__selenium_test_check;"))

The require call is asynchronous. The problem with this though, besides leaking a variable into the global space, is that it multiplies the network requests. Each execute_script call is a network request. The wait method works by polling: it runs the test until the returned value is true. This means one network request per check that wait performs (in the code above).

When you test locally it is not a big deal. If you have to go through the network because you are having the browsers provisioned by a service like Sauce Labs (which I use, so I'm talking from experience), each network request slows down your test suite. So using execute_async_script not only allows writing a test that looks more natural (call a callback, as we normally do with asynchronous code, rather than leak into the global space) but it also helps the performance of your tests.

result = driver.execute_async_script("""
var done = arguments[0];
require(["foo"], function (foo) {
    done(foo.computeSomething());
});
""")

The way I see it now is that if a test is going to hook into asynchronous code on the browser side to get a result, I use execute_async_script. If it is going to do something for which there is no asynchronous method available, I use execute_script.

Share:
33,494

Related videos on Youtube

alecxe
Author by

alecxe

"I am a soldier, at war with entropy itself" I am a Software Developer and generalist who is in love with the Python language and community. I greatly value clean and maintainable code, great software, but I know when I need to be a perfectionist and when it stands in a way of product delivery. I like to break things, to find new ways to break things, to solve hard problems, to put things under test and stress, and to have my mind blown by an interesting question. Some of my interests: Learning, Productivity, AI, Space Exploration, Internet of Things. "If you change the way you look at things, the things you look at change." - Wayne Dyer If you are looking for a different way to say "Thank you": Amazon wish list Pragmatic wish list Today I left my phone at home And went down to the sea. The sand was soft, the ocean glass, But I was still just me. Then pelicans in threes and fours, Glided by like dinosaurs, An otter basked upon its back, And dived to find another snack. The sun corpuscular and bright, Cast down a piercing shaft, And conjured an inspiring sight On glinting, bobbing craft. Two mermaids rose up from the reef, Out of the breaking waves. Their siren song was opium grief, Their faces from the grave. The mermaids asked a princely kiss To free them from their spell. I said to try a poet’s bliss. They shrugged and bid farewell. The sun grew dark and sinister, In unscheduled eclipse. As two eight-headed aliens Descended in their ships. They said the World was nice enough But didn’t like our star. And asked the way to Betelgeuse, If it wouldn’t be too far. Two whales breached far out to sea, And flew up to the sky, The crowd was busy frolicking, And didn’t ask me why. Today I left my phone at home, On the worst day, you’ll agree. If only I had pictures, If only you could see. Not everything was really there, I’m happy to confess, But I still have the memories, Worth more than tweets and stress. Today I left my phone at home, I had no shakes or sorrow. If that is what my mind can do, It stays at home tomorrow. Gavin Miller

Updated on July 09, 2022

Comments

  • alecxe
    alecxe almost 2 years

    I've been using selenium (with python bindings and through protractor mostly) for a rather long time and every time I needed to execute a javascript code, I've used execute_script() method. For example, for scrolling the page (python):

    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    

    Or, for infinite scrolling inside an another element (protractor):

    var div = element(by.css('div.table-scroll'));
    var lastRow = element(by.css('table#myid tr:last-of-type'));
    
    browser.executeScript("return arguments[0].offsetTop;", lastRow.getWebElement()).then(function (offset) {
        browser.executeScript('arguments[0].scrollTop = arguments[1];', div.getWebElement(), offset).then(function() {
            // assertions
    
        });
    });
    

    Or, for getting a dictionary of all element attributes (python):

    driver.execute_script('var items = {}; for (index = 0; index < arguments[0].attributes.length; ++index) { items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value }; return items;', element)
    

    But, WebDriver API also has execute_async_script() which I haven't personally used.

    What use cases does it cover? When should I use execute_async_script() instead of the regular execute_script()?

    The question is selenium-specific, but language-agnostic.

  • alecxe
    alecxe over 9 years
    Thank you for the link to the javaDoc - there are some samples mentioned. Have you personally had a need to execute an async script? If yes, could you describe the use case(s)? That would be really helpful.
  • hankduan
    hankduan over 9 years
    Added some examples. Hopefully that makes sense.
  • alecxe
    alecxe over 9 years
    Great, this was an interesting journey into protractor's client side scripts and other internals! Thanks again. Just in theory let's imagine: if I would test an angular app with selenium python bindings - I would need to load the similar (or same) client-side scripts and call testForAngular() and waitForAngular() functions asynchronously in order to check and wait for Angular to "become stable". Correct?
  • hankduan
    hankduan over 9 years
    Well, testForAngular just waits for 'window.angular' to be present, and waitForAngular uses 'window.angular' to check if the app is stable, so you definitely need them both, but you'll need a lot of other code too =)
  • alecxe
    alecxe over 9 years
    Yeah, thanks :) I'm just trying to think of a valid real-world non-protractor/angular related example usage of the executeAsyncScript which could not be solved with anything else as easily as with it. FYI, I'll keep this topic open for a while (we'll also start a bounty in a couple of days to draw attention).
  • GrayedFox
    GrayedFox about 8 years
    Superb. +1 for going into detail about when it is still appropriate to use execute over executeAsync. I would add the caveat: this bug has, for me (using selenium web driver and javascript) seemingly returned.
  • pguardiario
    pguardiario over 5 years
    What is the callback though and why aren't you passing it?
  • Louis
    Louis over 5 years
    @pguardiario The callback is created and passed by Selenium itself. When you use execute_async_script, the last argument available on the JavaScript side is the callback created by Selenium. Suppose you call driver.execute_async_script(script, "foo", "bar"). In the code that runs in script, arguments[0] is the value "foo", arguments[1] is the value "bar" and arguments[2] is the callback that Selenium created. Your script must call the callback when it is done to signal to Selenium that it is done. Otherwise, Selenium cannot know that it is done.
  • pguardiario
    pguardiario over 5 years
    Thanks, that makes sense. I thought I was supposed to be passing a callback.