How do Protractor and Angular synchronize?

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.

What Protractor does

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 synchronization API

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. $$testability service has a whenStable(callback) method, which is what Protractor’s waitForAngular() uses.

How does AngularJS keep track of all asynchronous operations? $q, $timeout and $http are connected with this $$testability service via $browser service. Whenever they do something asynchronous, they report it to the $browser and $browser then reports it to $$testability.

Angular synchronization API

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 waitForAngular() uses.

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 setTimeout(), XMLHttpRequest and others.

How to make WebDriver do what Protractor does?

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.

First of all, any WebDriver implementation capable of executing JavaScript implements an interface JavascriptExecutor (e.g. ChromeDriver has it). This interface has a method executeAsyncScript() which allows one to submit an asynchronous script for execution and pause test execution until the script finishes. The script looks like this:

1
2
3
4
5
var done = arguments[0];
window.getAngularTestability(document.querySelector('app'))
.whenStable(function(didWork) {
done(didWork);
});

Here, 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:

1
2
3
4
5
6
7
8
9
10
11
12
void synchronize(WebDriver webDriver) {
for(int i = 0; i < MAX_ANGULAR_SYNC_ITERATIONS; ++i) {
JavascriptExecutor jsExecutor = (JavascriptExecutor)webDriver;
boolean shouldSyncOneMoreTime =
(Boolean)jsExecutor.executeAsyncScript(scriptContent);
if(shouldSyncOneMoreTime) {
continue;
}

break;
}
}

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.

WebDriver provides EventFiringWebDriver – a decorator around WebDriver object that allows you to register() a WebDriverEventListener. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SynchronizingWebDriverEventListener 
extends AbstractWebDriverEventListener {

@Override
public void afterClickOn(WebElement element, WebDriver driver) {
synchronize(driver);
}

void synchronize(WebDriver webDriver) {
...
}
}
...
ChromeDriver chromeDriver = new ChromeDriver();
EventFiringWebDriver eventFiringWebDriver =
new EventFiringWebDriver(chromeDriver);
eventFiringWebDriver.register(new SynchronizingWebDriverEventListener());
WebDriver webDriver = eventFiringWebDriver;
// Go ahead and use webDriver!

Conclusion

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.