Working with AngularJS apps in Serenity BDD

John Ferguson Smart | Mentor | Author | Speaker - Author of 'BDD in Action'.
Helping teams deliver more valuable software sooner15th December 2017

AngularJS | Requirements Discovery | Xscale |

AngularJS applications have their own challenges when it comes to WebDriver test automation. The asycnhronous nature of Angular apps makes testing these applications particularly tricky when using traditional WebDriver-based technologies.

In the JavaScript world, Protractor provides an elegant solution to the problem of testing AngularJS apps. Protractor supports angular-specific locators and knows how to wait for Angular to finish processing pending tasks before proceeding to the next step of the test.

Serenity BDD integrates with ngWebDriver to leverage the power of Protractor from within your Serenity BDD test suites.

Waiting for Angular to finish asynchronous activities

Sometimes you need to wait until Angular has finished it’s application processing activities before interacting with an element or moving to the next step of a test. You can do this in a Serenity Page Object by calling the waitForAngularRequestsToFinish() method, like this:

	waitForAngularRequestsToFinish()

If you are using Screenplay, you can use the WaitUntil class (from the net.serenitybdd.screenplay.waits package), as shown here:

    actor.attemptsTo(
        Click.on(ADD_TO_CART),
        WaitUntil.angularRequestsHaveFinished(),
        Click.on(PURCHASE)
    )

Working with AngularJS locators

Protractor provides a number of Angular-specific locators, such as by.model, by.binding and by.repeater (note that the binding and model locators are not supported in Angular 2). The ngWebDriver library from Paul Hammant allows you to call Protractor locators from your Java test code.

ngWebDriver provides a special ByAngular class that you can use in the place of the Selenium Byclass, and that provides access to a number of Angular-specific locators.

In Serenity BDD, you can use the ngWebDriver locators anywhere you would use normal Webdriver locators. This includes the find() and findAll() methods of the Serenity PageObject base class, but also in any Screenplay target locator.

Some examples are shown in the following sections.

Binding and Model

In Angular 1.x apps, you can use Protractor’s binding and model locators using the ByAngular.binding() and ByAngular.model() methods. In a Serenity Page Object, you could retrieve an element using the find() method:

public class TodoListApp extends PageObject {
    public void addTodo(String item) {
        find(ByAngular.model("todo")).sendKeys(item, Keys.ENTER);
    }
}

Or you could define a Screenplay `Target`like this:

    Target TODO_INPUT_FIELD = Target.the("Todo input field")
                                    .located(ByAngular.model("todo"));

Note that these won’t work with Angular 2.x or above.

Button text

Often it is useful to identify button by it’s text label. Traditionally this calls for XPath. But Protractor gives us a more elegant approach. Suppose you have the following HTML element on your page.

<button>Save</button>

You can locate this element using the buttonText() method:

find(ByAngular.buttonText("Save")).click();

For more flexibility, you can also use the partialButtonText() method.

CSS Containing Text

Another useful trick is to fine elements that match a given CSS selector, and which contain a specific string. This is not usually possible with CSS in WebDriver, so you need to resort to chained selectors or cumbersome XPath expressions.

Suppose you have an Angular app with the following code:

<div id="product">
  <ul>
    <li class="colour">Red</li>
    <li class="colour">Blue</li>
  </ul>
</div>

You could locate the Red entry in this list by using the following code:

    find(ByAngular.cssContainingText("#product .colour", "Red"))

Using repeaters

You can use ByAngular.repeater() to retrieve elements that have been implemented on the page using ng-repeat. For example, in the TodoMVC App, a list of todo items is represented using the ng-repeat attribute:

<li ng-repeat="todo in todos"...>

We could retrieve these items using the following method in a PageObject class:

public class TodoListApp extends PageObject {
    public List<String> visibleTodoItems() {
        return findAll(ByAngular.repeater("todo in todos"))
                  .stream()
                  .map(WebElement::getText)
                  .collect(Collectors.toList());
    }
}

In Screenplay, we could write a Target object that uses the ByAngular locator to find the list of todo items:

public class TodoList {
    public static Target ITEMS
        = Target.the("List of todo items")
                .located(ByAngular.repeater("todo in todos"));
}

You can learn more about the available locators on the ngWebDriver website.

© 2019 John Ferguson Smart