UI Automation Testing of Angular apps using Protractor / Jasmine

Automation testing is usually considered to be a separate process which requires the automation QA team to write tests in a separate solution with their own build process and workflow. But I will show how we can easily build an automated test suite within any Angular CLI project.
Protractor is configured by default in any Angular CLI project. We will use a demo app that I’ve created and we will cover it with automated tests step-by-step. At the end you will be able to download the complete project.
This is basic tutorial. You don’t need any previous knowledge of Protractor or automation testing. Knowledge of HTML/CSS is required. Latest Chrome browser should be used. Git is required to pull sources, however a link to download a .zip version will be included as well.

Prerequisites

  1. NodeJS. You can download latest Node from https://nodejs.org/en/download/
  2. Angular CLI. Once you have installed Node, open the terminal and run 
    npm install angular-cli -g

    If you are getting access errors on Mac run the same script with sudo. 

  3. Protractor.
    npm install protractor -g

    If you are getting access errors on Mac run the same script with sudo. 

  4. Demo App.  Pull latest version from our repository: https://github.com/trailheadtechnology/protractor-testing-demo-app or download the zip: https://github.com/trailheadtechnology/protractor-testing-demo-app/archive/master.zip

Now let’s complete our setup and verify that we are ready to move forward. Open the terminal and navigate to the folder where the demo app is located. Run the following commands:

npm install
webdriver-manager update
ng serve

This will start a simple development server with the app. Open https://localhost:4200 in your browser and verify that you see a simple login screen. Let’s inspect our app now.

Demo Application

Our demo application consists of 2 pages:

Login page.

A simple form with 2 fields for username and password and a sign in button. An error message will be displayed to the user in case of incorrect credentials. This page specifically simulates an async request to the server to look for common problems when writing automated tests and the way to solve them. You can sign in to the app with username: correct / password: correct.

Dashboard page.

This page is very simple. It has title saying Welcome to the Dashboard and Logout link.

Test cases

We will cover these test cases in our test application:

  1. Login page should have correct titles and button text.
  2. Login page should display an error message to the user if they provided incorrect credentials.
  3. Login page should redirect the user to the dashboard page if they provided correct credentials.

 
Let’s translate those test cases into code now.

First Suite and Specs

Protractor tests are described in specs which are grouped into suites. The most common framework used to describe those is Jasmine. You can read more about Jasmine on their website: https://jasmine.github.io/
Angular CLI creates a single suite with a spec by default, but we will remove it and create our own specs from scratch. Locate the e2e folder under the root folder of your project and remove the e2e/app.e2e-spec.ts and e2e/app.po.ts files completely.
Now let’s create login.e2e-spec.ts file under the e2e folder and add the following code:

describe('Login page', () => {
});

describe(..) block is used to group a set of specs into suite. We will write our login page specs inside this function. To describe specs we will use it(…) blocks. The great thing about Jasmine that specs are described in the same way we’ve declared them earlier:

  1. Login page should have correct titles and button text.
  2. Login page should display an error message to the user if they provided incorrect credentials.
  3. Login page should redirect the user to the dashboard page if they provided correct credentials.

The text highlighted in bold will become name for each spec. Let’s add 3 specs to our login.e2e-spec.ts:

describe('Login page', () => {
 it('should have correct titles and button text', () => {
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
 });
});

Let’s try to run our test suite now. There are 2 ways to do it: with the Angular CLI ng e2e command or the protractor command.  We will use ng e2e at the moment, but we will look at the differences later. ng e2e will build the latest version of the application, serve the application on the localhost and run our test suite against it. Open terminal and run: 

ng e2e

You should see this output:

All specs passed because we haven’t provided any assertions yet. Let’s start by declaring a global  beforeEach(…) hook and navigate to the /login URL before each test in our suite (https://jasmine.github.io/api/edge/global.html#beforeEach). There are 4 global hooks you can use to setup some preconditions for the whole suite / test (beforeAll, beforeEach) as well as cleanup after the suite / test has run (afterAll, afterEach). 
Update login.e2e-spec.ts with the following code:

import {browser} from 'protractor';
describe('Login page', () => {
 beforeEach(() => {
   browser.get('/login');
 });
 it('should have correct titles and button text', () => {
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
 });
});

Now let’s work on the first spec which is:

it('should have correct titles and button text', () => {
});

To complete this spec we need to locate elements on the page and compare their text to what is expected. We will encapsulate locators for elements inside classes called Page Objects. Let’s look into what a Page Object is and create our login page Page Object class.

Page Object

Page Object is a Design Pattern which has become popular in test automation for enhancing test maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your AUT. The tests then use the methods of this page object class whenever they need to interact with the UI of that page. The benefit is that if the UI changes for the page, the tests themselves don’t need to change, only the code within the page object needs to change. Subsequently all changes to support that new UI are located in one place. (https://www.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern)
Create a login.po.ts file under the e2e folder and paste the following code there:

import {browser, element, by} from 'protractor';
export class LoginPage {
}

Now let’s set up locators for the elements on the login page. We will combine element(…) https://www.protractortest.org/#/api?view=ElementFinder with by https://www.protractortest.org/#/api?view=ProtractorBy to find the elements we need.

As you can see from the Protractor API reference there are numerous ways to find elements on the page. The most common ones are by.id() and by.css(). Update your login.po.ts file with the following selectors:

import {browser, element, by} from 'protractor';
export class LoginPage {
 get usernameLabel() {
   return element(by.css('.login-field:nth-child(1) label'));
 }
 get username() {
   return element(by.id('username'));
 }
 get passwordLabel() {
   return element(by.css('.login-field:nth-child(2) label'));
 }
 get password() {
   return element(by.id('password'));
 }
 get errorMessage() {
   return element(by.className('login-error'));
 }
 get signIn() {
   return element(by.className('login__button'));
 }
}

You can figure out those selectors by right-clicking element in Chrome and selecting Inspect element option:

The element will be highlighted in the DOM tree and you will be able to find out which selector will be better to use in each case. There are also options to Copy XPath or Copy Selector , but they are not recommended to use, because any change to the DOM tree will break the generated selectors.

 
We can now complete our first spec. Go back to the login.e2e-spec.ts file and update it with the following code:

import {browser} from 'protractor';
import {LoginPage} from './login.po';
describe('Login page', () => {
 let page: LoginPage;
 beforeEach(() => {
   page = new LoginPage();
   browser.get('/login');
 });
 it('should have correct titles and button text', () => {
   expect(page.usernameLabel.getText()).toEqual('Username');
   expect(page.passwordLabel.getText()).toEqual('Password');
   expect(page.signIn.getText()).toEqual('Sign in');
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
 });
});

Here we used Jasmine expect(…) calls with .toEqual(…) matchers. You can read more about matchers in Jasmine from their API docs: https://jasmine.github.io/api/edge/matchers.html. Each expect statement is assertion. Meaning if it fails, the spec will be considered as failed.
Let’s work on the next spec now:

it ('should display an error message to the user if they provided incorrect credentials', () => {
});

Here we will utilize the .sendKeys(…) method on input to fill out username and password and also use protractor.ExpectedConditions to force the browser to wait for the API response. Add the following code to the login.e2e-spec.ts file: 

import {browser, protractor} from 'protractor';
import {LoginPage} from './login.po';
describe('Login page', () => {
 let page: LoginPage;
 const EC = protractor.ExpectedConditions;
 beforeEach(() => {
   page = new LoginPage();
   browser.get('/login');
 });
 it('should have correct titles and button text', () => {
   expect(page.usernameLabel.getText()).toEqual('Username');
   expect(page.passwordLabel.getText()).toEqual('Password');
   expect(page.signIn.getText()).toEqual('Sign in');
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
   page.username.sendKeys('123');
   page.password.sendKeys('123');
   page.signIn.click();
   browser.wait(EC.visibilityOf(page.errorMessage));
   expect(page.errorMessage.getText()).toEqual('Incorrect username or password');
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
 });
});

Note browser.wait(EC.visibilityOf(page.errorMessage));. This line will force Protractor to stop executing statements until the provided condition is fulfilled. In our case we wait for the element that contains an error message to become visible. You can read more about methods of this class in Protractor API docs: https://www.protractortest.org/#/api?view=ProtractorExpectedConditions
To finish our login specs we will need to check if the user is redirected to the dashboard. The best way to do this is to check that some dashboard element is present on the screen. Let’s create another Page Object class for the dashboard page in the file dashboard.po.ts under the e2e folder with the following contents:

import {by, element} from 'protractor';
export class Dashboard {
 get title() {
   return element(by.css('h1'));
 }
 get logoutLink() {
   return element(by.css('.header__logout'));
 }
}

Now let’s complete our login spec:

import {browser, protractor} from 'protractor';
import {LoginPage} from './login.po';
import {DashboardPage} from './dashboard.po';
describe('Login page', () => {
 let page: LoginPage;
 const EC = protractor.ExpectedConditions;
 beforeEach(() => {
   page = new LoginPage();
   browser.get('/login');
 });
 it('should have correct titles and button text', () => {
   expect(page.usernameLabel.getText()).toEqual('Username');
   expect(page.passwordLabel.getText()).toEqual('Password');
   expect(page.signIn.getText()).toEqual('Sign in');
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
   page.username.sendKeys('123');
   page.password.sendKeys('123');
   page.signIn.click();
   browser.wait(EC.visibilityOf(page.errorMessage));
   expect(page.errorMessage.getText()).toEqual('Incorrect username or password');
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
   const dashboardPage = new DashboardPage();
   page.username.sendKeys('correct');
   page.password.sendKeys('correct');
   page.signIn.click();
   browser.wait(EC.visibilityOf(dashboardPage.title));
   expect(dashboardPage.title.isPresent()).toBeTruthy();
 });
});

Let’s run ng e2e again and verify that all our specs passed. We now have fully functional test suite. However there is some duplicated code in our specs. Let’s move it into Page Object.
login.po.ts

import {browser, element, by} from 'protractor';
export class LoginPage {
 get usernameLabel() {
   return element(by.css('.login-field:nth-child(1) label'));
 }
 get username() {
   return element(by.id('username'));
 }
 get passwordLabel() {
   return element(by.css('.login-field:nth-child(2) label'));
 }
 get password() {
   return element(by.id('password'));
 }
 get errorMessage() {
   return element(by.className('login-error'));
 }
 get signIn() {
   return element(by.className('login__button'));
 }
 trySignIn(username: string, password: string) {
   this.username.sendKeys(username);
   this.password.sendKeys(password);
   this.signIn.click();
 }
}

login.e2e-spec.ts

import {browser, protractor} from 'protractor';
import {LoginPage} from './login.po';
import {DashboardPage} from './dashboard.po';
describe('Login page', () => {
 let page: LoginPage;
 const EC = protractor.ExpectedConditions;
 beforeEach(() => {
   page = new LoginPage();
   browser.get('/login');
 });
 it('should have correct titles and button text', () => {
   expect(page.usernameLabel.getText()).toEqual('Username');
   expect(page.passwordLabel.getText()).toEqual('Password');
   expect(page.signIn.getText()).toEqual('Sign in');
 });
 it ('should display an error message to the user if they provided incorrect credentials', () => {
   page.trySignIn('123','123');
   browser.wait(EC.visibilityOf(page.errorMessage));
   expect(page.errorMessage.getText()).toEqual('Incorrect username or password');
 });
 it ('should redirect the user to the dashboard page if they provided correct credentials', () => {
   const dashboardPage = new DashboardPage();
   page.trySignIn('correct', 'correct');
   browser.wait(EC.visibilityOf(dashboardPage.title));
   expect(dashboardPage.title.isPresent()).toBeTruthy();
 });
});

Now we have cleaner code and if something changes in our login process, we only need to change it in one place without modifying our tests.

Test run: protractor vs ng e2e

It was mentioned before that there 2 main ways to start your protractor automation suite: the ng e2e command and the protractor command. The main difference between those is that ng e2e will build the current version of the application and run tests against it, while the protractor command will execute tests against the baseUrl defined in protractor.conf.js. 

const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
 allScriptsTimeout: 11000,
 specs: [
   './e2e/**/*.e2e-spec.ts'
 ],
 capabilities: {
   'browserName': 'chrome'
 },
 directConnect: true,
 baseUrl: 'https://localhost:4200/',
 framework: 'jasmine',
 jasmineNodeOpts: {
   showColors: true,
   defaultTimeoutInterval: 30000,
   print: function() {}
 },
 onPrepare() {
   require('ts-node').register({
     project: 'e2e/tsconfig.e2e.json'
   });
   jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
 }
};

By default this url will be https://localhost:4200 because that’s Angular default URL for ng serve. Open another instance of the terminal and run ng serve in it. Now from the first terminal window instead of running ng e2e run protractor. You will see the same result with 3 specs being executed and passed. This variant is more flexible as it gives you an ability to execute tests locally against any environment by simply changing the baseUrl in protractor.conf.js. Finally let’s look into how to get a simple HTML report after test execution.

Reporting

There are numerous reporting libraries that you can find for protractor. We will integrate the HTML report generator protractor-html-reporter-2 (https://github.com/abhishekkyd/protractor-html-reporter-2). First we need to install it in our project. Run the following command in the terminal:

npm install protractor-html-reporter-2 --save-dev

Now open protractor.conf.js and replace it’s contents with the following code:

// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
const jasmineReporters = require('jasmine-reporters');
const HTMLReport = require('protractor-html-reporter-2');
const fs = require('fs-extra');
exports.config = {
 allScriptsTimeout: 11000,
 specs: [
   './e2e/**/*.e2e-spec.ts'
 ],
 capabilities: {
   'browserName': 'chrome'
 },
 directConnect: true,
 baseUrl: 'https://localhost:4200/',
 framework: 'jasmine',
 jasmineNodeOpts: {
   showColors: true,
   defaultTimeoutInterval: 30000,
   print: function() {}
 },
 onPrepare() {
   require('ts-node').register({
     project: 'e2e/tsconfig.e2e.json'
   });
   fs.emptyDir('e2e/_report', (err) => { err && console.log(err); });
   const jasmineEnv = jasmine.getEnv();
   const specReporter = new SpecReporter({ spec: { displayStacktrace: true } });
   const xmlReporter = new jasmineReporters.JUnitXmlReporter({
     consolidateAll: true,
     savePath: './e2e/_report',
     filePrefix: 'e2exmlresults'
   });
   const screenshotReporter = {
     specDone: function (result) {
       if (result.status === 'failed') {
         browser.getCapabilities().then(function (caps) {
           const browserName = caps.get('browserName');
           browser.takeScreenshot().then(function (png) {
             const stream = fs.createWriteStream('e2e/_report/' + browserName + '-' + result.fullName + '.png');
             stream.write(new Buffer(png, 'base64'));
             stream.end();
           });
         });
       }
     }
   };
   jasmineEnv.addReporter(specReporter);
   jasmineEnv.addReporter(xmlReporter);
   jasmineEnv.addReporter(screenshotReporter);
 },
 //HTMLReport called once tests are finished
 onComplete: function() {
   let browserName, browserVersion, platform;
   const capsPromise = browser.getCapabilities();
   capsPromise.then(function (caps) {
     browserName = caps.get('browserName');
     browserVersion = caps.get('version');
     platform = caps.get('platform');
     const testConfig = {
       reportTitle: 'Protractor Test Report',
       outputPath: './e2e/_report',
       outputFilename: 'ProtractorTestReport',
       screenshotPath: '.',
       testBrowser: browserName,
       browserVersion: browserVersion,
       modifiedSuiteName: false,
       screenshotsOnlyOnFailure: true,
       testPlatform: platform
     };
     new HTMLReport().from('e2e/_report/e2exmlresults.xml', testConfig);
   });
 }
};

The main idea here is to configure protractor to generate an XML report as an output and do a screenshot when the spec is done and if it has failed. After test completion we generate the HTML report providing a path to an XML report generated after the test execution.
Now run your test suite again. After running you will be able to find the generated HTML report under the e2e/_report folder.

This is what the report looks like:

Now let’s simulate a failed condition in our tests to see how it will look when some tests fail. Open login.e2e-spec.ts and modify our first spec in the following way:

it('should have correct titles and button text', () => {
 expect(page.usernameLabel.getText()).toEqual('Username1');
 expect(page.passwordLabel.getText()).toEqual('Password1');
 expect(page.signIn.getText()).toEqual('Sign in');
});

Run the test suite. You should now see failures in the terminal:

If you open newly generated HTML report now you should see a failed spec and screenshot of the app on failure:

Those screenshots can be very informative and help to debug failures.

Conclusion

Today we learned how to create tests for any Angular CLI project. You can continue to expand this test suite with your own tests, for instance for logout functionality or dashboard elements. You can get the final code by pulling the e2e branch from the same repo: https://github.com/trailheadtechnology/protractor-testing-demo-app/tree/e2e or by downloading a .zip from https://github.com/trailheadtechnology/protractor-testing-demo-app/archive/e2e.zip
 

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Manage Your Windows Applications With Winget

Winget, Microsoft’s native package manager for Windows 10 (version 1709 and later) and Windows 11, offers a streamlined CLI for efficient application management. This blog post introduces Winget’s installation and basic commands for installing, updating, and removing software. It highlights the tool’s ability to manage non-Winget-installed apps and explores curated package lists for batch installations. The post also recommends top Winget packages, noting some may require a paid subscription.

Read More

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.