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 | var done = arguments[0]; |
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 | void synchronize(WebDriver webDriver) { |
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 | public class SynchronizingWebDriverEventListener |
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.