One of the annoying things about writing Selenium tests is that you must ensure to only interact with the page at the right moments of time. Buttons tend to trigger asynchronous operations, so after you click a button, you should wait for the this operation to finish before you go any further.
The common practice is to poll the DOM – “as long as this spinner element is visible, operation is running” (
WebDriverWait). However, in different scenarios your spinners may be different DOM elements and sometimes you may not have a spinner at all, which makes this approach harder to implement.
Protractor somehow handles asynchronous operations transparently. How does it do it?
Both Angular and AngularJS know it, when they run an asynchronous operation. Both expose the special undocumented testability API, which Protractor uses when it wants to synchronize. This API allows one to provide a callback function which is going to be called once all asynchronous operations are completed. Let’s take a closer look to understand what happens behind the scenes.
Protractor injects a few functions on the page. One of those is waitForAngular(rootSelector, callback) function. Protractor calls this function every time it wants to synchronize.
This function serves as a synchronization facade – it expects caller to provide a callback function, which is going to be called once Angular says that there are no asynchronous operations running. Different version of Angular provide different testability API, so
waitForAngular() has to know how to work with all of them.
AngularJS registers a global
window.angular object, which has a method
getTestability(rootElement). Given the
rootElement, this method retrieves the injector and gets the
$$testability service has a
whenStable(callback) method, which is what Protractor’s
How does AngularJS keep track of all asynchronous operations?
$http are connected with this
$$testability service via
$browser service. Whenever they do something asynchronous, they report it to the
$browser then reports it to
Angular’s API is similar to one that AngularJS provides. Angular registers a global
window.getAngularTestability(element), which returns a
PublicTestability object. This object has a method
whenStable(callback), which is what Protractor’s
How does Angular keep track of all asynchronous operations? Its entire synchronization mechanism relies on Zone.js – a library that does some black magic to intercept all calls to low-level asynchronous mechanisms like
XMLHttpRequest and others.
Now that we know how Protractor synchronizes, we can implement a similar behavior for WebDriver. We’ll only consider Angular, but approach is the same for AngularJS.
executeAsyncScript() which allows one to submit an asynchronous script for execution and pause test execution until the script finishes. The script looks like this:
done() is a callback the script should call when execution is completed. The argument to this method is a flag indicating whether we actually waited for something (
didWork == true) or if we finished synchronously without any waiting (
didWork == false). This flag is going to be a return value of
executeAsyncScript() on the caller’s side.
Why is this flag important? If there were no asynchronous operations (
false), this means that we’re “stable” – no new asynchronous operations may appear unless with interact with the page. If there was at least one asynchronous operation and we had to wait for it to finish (
true), there are chances that more asynchronous operations could have appeared, so we’ll need to synchronize once again. Here’s what the calling code looks like:
If you manually call this
synchronize() method every time your test clicks a button, it will pause the test execution until the operation is finished. Obviously, you don’t want to call this method manually. Luckily, WebDriver provides the extension points to entirely hide this synchronization.
EventFiringWebDriver – a decorator around
WebDriver object that allows you to
WebDriverEventListener has a number of methods to be executed before and after a certain action. Among them, there is
afterClickOn() – a method that gets called after every
click(). Let’s use this method to inject our synchronization behavior:
Development teams can be more productive when developers pay attention to testability. The easier it is to write a test, the more chances the test will be written. Angular delivers the great testing experience by paying attention to testability, but while this post is primarily about Angular, same ideas work perfectly for many other front end web frameworks (a colleague of mine has recently built a similar synchronization mechanism for a React/Redux application).
You may find a self-sufficient sample project in this GitHub repository.