Application testing
Angular BDD E2E Testing Jasmine Protractor SPA Typescript Visual Code

Using Protractor Locators for Advanced End to End Testing

Welcome to today’s blog.

In today’s blog I will be showing some advanced features of the Protractor API that can be used to create more detailed test scenarios in your end-to-end test scripts.

In a previous blog, I showed how to setup end to end testing using Protractor end to end test framework using the Selenium Web Driver and Jasmine BDD scripts to write basic test specs to test an Angular web application.

To start with let us define a test script:

  1. Open the list of books
  2. Select the first book
  3. Update the ISBN field with a value.
  4. Update the book.
  5. Re-open the list of books
  6. Select the first book.
  7. Check the ISBN value is the same as the updated value.

This seems quite complex sequence of tasks to apply to achieve a test, but with an automated end-end testing framework such as Protractor, it can be achieved with one script.

I will show how.

First, with we need to understand what we can achieve with the front-end UI we are using. In this case I am using Angular (a version beyond AngularJS). This means some of the Protractor locators (selectors) will not be available, such as by.model() and by.binding().

We can make use of the following locators:

1. Finding a DOM element by class name:

by.css('.myclass')

2. Finding a DOM element with the given id:

by.id('myid')

3. Find a DOM element using an input name selector:

by.name('field_name')

In our books list, we modify our Angular HTML template to support the use of CSS classes and / or HTML element identifiers:

<mat-card-content>
    <div id="heading"><b>{{heading}}</b></div>
    <div *ngIf="hasLoaded$ | async; else loading">
        <mat-list *ngFor="let book of books">
            <div style="display: flex; flex-wrap: wrap; 
                height: 20px; border-style: solid; 
                border-width: 1px;">
                <div style="flex: 0 50%; color: green; 
                    background-color:aquamarine;">
                    <mat-list-item id="book_{{book.id}}"  
                        class="clickLink" 
                        (click)="selectBookById(book.id)">
                        {{book.id}}
                    </mat-list-item>
                </div>    
                <div style="flex: 0 50%; color: blueviolet; 
                    background-color:aquamarine;">
                    <mat-list-item class="clickLink" 
                        (click)="selectBookById(book.id)">
                        {{book.title}}
                    </mat-list-item>
                </div>
            </div>
        </mat-list>
    </div>
    <br />
        …
</mat-card-content>

In an edit form, we similarly include an identifier for the HTML input controls we wish to use within our test scripts:

<div class="form-group">
    <label for="location">ISBN:</label>
    <input [(ngModel)]="book.isbn" name="isbn" id="isbn" 
        class="form-control col-sm-4" placeholder="ISBN" 
        placement="right" ngbTooltip="ISBN">
</div>

With controls such as buttons, we can also identify these with an id:

<div class="example-button-row">
    <button type="button" class="btn btn-primary" 
        id="btnUpdate" (click)="eventUpdateRecord($event.value)">
        Update
    </button>
</div>

You can see where we are getting at. By ensuring all of our HTML template script elements are all identified either by identifiers or CSS class names we can ensure our HTML form controls when running under an automated end-end UI test runner can be identified and actioned and / or compared against test data.

After the HTML templates has been standardized, we next implement a script that will run the tests.

The describe() function is used to group together specifications. Each spec (specification) is defined by the it() function.

Within each spec there are expectations, which are equivalent to assertions in unit tests. Depending on values within the UI they will either pass or fail an assertion test.

An example of an expectation is below:

expect(element(by.id('heading')).getText()).toEqual('Book List');

There are functions beforeEach(), afterEach(), beforeAll() and afterAll() that allow setup or teardown code to initialise variables and / or run tasks before or after a test run. An example of a setup is to open the browser at a particular URL:

beforeEach(function() {
    browser.get('http://localhost:4200/books');
});

For more details on Jasmine BDD test functions refer to the Jasmine site.

Below is the skeleton Jasmine test script:

describe('Protractor Test - Angular Record List Form', function() {
    var heading = element(by.id('heading'));
    var newValue = undefined;

    beforeEach(function() {
      browser.get('http://localhost:4200/books');
    });

    it('should have a title', function() {
        expect(element(by.id('heading'))
            .getText())
            .toEqual('Book List');
    });

    it('record can be updated', function() {
        browser.get('http://localhost:4200/books');
	    …
        // Select the first item

        // Test if the current form is named 

        // Change ISBN value..
      
        // update the record..
    });

    it('record update is consistent', function() {

        // reselect record and verify the ISBN field is the same 
        // as the updated value..
        browser.get('http://localhost:4200/books');

        element.all(by.css('.mat-list-item')).then(function(items) {
        // Select the first item
  
        // Test if the current form is named 

        // Compare ISBN value from data source against value 
        // updated on initial form. 
    });
});

Script for the specification ‘record can be updated’ is shown below:

element.all(by.css('.mat-list-item')).then(function(items) {
    expect(items.length).toBeGreaterThan(0);
    expect(items[1].getText()).toBe('The Lord of the Rings');

    // Select the first item
    var selectItem = element(by.id("book_1"));
    selectItem.click();
    browser.sleep(2000);

    // Test if the current form is named 
    expect(element(by.id('heading'))
        .getText())
        .toEqual('Book Details'); 

    // Change ISBN value..
    var today = new Date();
    var increment = today.getSeconds();

    var isbnField = element(by.id('isbn'));
    var isbnValue = undefined;
    var EC = protractor.ExpectedConditions;
    browser.wait(EC.visibilityOf(isbnField), 5000);

    isbnField.getAttribute('value').then(text => {
        isbnValue = text;
        console.log("original isbn=" + text);
        newValue = parseInt(isbnValue) + increment;
        console.log("new isbn=" + newValue);
        console.log("increment=" + increment);
        isbnField.clear();
        isbnField.sendKeys(newValue.toString());
        browser.sleep(2000);   
    });
      
     // update the record..
     console.log("updating record..");
     var btnUpdate = element(by.id('btnUpdate'));
     btnUpdate.click();
     console.log("record updated!");
     browser.sleep(2000);
});

With Jasmine you can use JavaScript within the specification script to store and manipulate variables. Also, because the Protractor API is asynchronous, all functions returning UI elements are promises (similar to observables), so to access a test property we have to use a then() function to extract the promises value of the variable asynchronously. For the ISBN field we do this as follows:

isbnField.getAttribute('value').then(text => {
    isbnValue = text;
	…

If you are familiar with Angular and RxJS, this is quite a familiar pattern, almost like subscribing to an observable, except we are receiving just one value from the control.

As I mentioned, because Protractor is asynchronous, some of the script can be run out of sequence!

So, we can use an API method to wait on a condition. In the above test, we wait for the ISBN field to be visible to the user:

var EC = protractor.ExpectedConditions;
browser.wait(EC.visibilityOf(isbnField), 5000);

This is not an indefinite wait. I have added enough time for the page to load and for the element to display. If there was no wait condition, then the variable isbnValue would end up undefined and lead to a test failure.

The script for the spec ‘record update is consistent’ is almost identical, except we don’t update, we compare the update value used in the previous spec:

it('record update is consistent', function() {

    // reselect record and verify the ISBN field is the same as 
    //  the updated value..
    browser.get('http://localhost:4200/books');

    element.all(by.css('.mat-list-item')).then(function(items) {
        expect(items.length).toBeGreaterThan(0);
        expect(items[1].getText()).toBe('The Lord of the Rings');
  
        // Select the first item
        var selectItem = element(by.id("book_1"));
        selectItem.click();
        browser.sleep(2000);
  
        // Test if the current form is named 
        expect(element(by.id('heading'))
            .getText())
            .toEqual('Book Details'); 

        // Compare ISBN value from data source against 
        // value updated on initial form. 
        var isbnField = element(by.id('isbn'));
        var EC = protractor.ExpectedConditions;
        browser.wait(EC.visibilityOf(isbnField), 5000);

        isbnField.getAttribute('value').then(text => {
            console.log("updated isbn=" + text);
            console.log("compare isbn=" + newValue);
        });
   
        expect(isbnField.getAttribute('value'))
            .toEqual(newValue.toString());
        browser.sleep(2000); 
    });
});  

Note: Using getText() on a UI field will not work (it will give undefined) with a HTML element of type INPUT. You will need to use getAttribute(‘value’) to obtain the entered input value. For a DIV or SPAN containing an inner static text, getText() will work.

In a future post I will discuss structuring our unit tests and using PageObjects.

That is all for today’s post.

I hope you found it useful.

Social media & sharing icons powered by UltimatelySocial