Protractor

Protractor is a Node.js program to create BDD test cases in. To run Protractor, you will need to have Node.js installed.Protractor needs two files to run, the test or spec file, and the configuration file.

Syntax

Describe , it and expect are jasmine framework methods to write the tests easily. Read more about them here.

beforeEach function in the code above executes before all it blocks, however, you might not want to execute the code inbeforeEach for all it blocks.

Multiple ways to select elements:

by.css		 by.model	   by.repeater		by.id		by.binding	    by.xpath

Interacting with the DOM:

  • element: returns a single element
  • element.all: returns a collection of elements. Use get(index), first(), and last() functions to get a single element out of the collection.
  • filter : Similar to get(index), first() and last() functions, filter takes a collection of elements and returns a single element. The only difference is that the element can be selected based on the specified condition. This is useful when there is a dynamic/long list of elements having same selector path and you need to get an element using its text or any unique property.
  • page-objects: Page-objects is a commonly used practice across the industry while writing e2e tests. It enables you to write clean tests by listing all the information about the elements in a page-object file. This means that you only need to change the page object file, in case of any change in template of app.

 

 

 

Spec files

Protractor tests are written using the syntax of your test framework, for example Jasmine, and the Protractor API

describe('angularjs homepage', function() {
  it('should greet the named user', function() {
    // Load the AngularJS homepage.
    browser.get('http://www.angularjs.org');

    // Find the element with ng-model matching 'yourName' - this will
    // find the <input type="text" ng-model="yourName"/> element - and then
    // type 'Julie' into it.
    element(by.model('yourName')).sendKeys('Julie');

    // Find the element with binding matching 'yourName' - this will
    // find the <h1>Hello {{yourName}}!</h1> element.
    var greeting = element(by.binding('yourName'));

    // Assert that the text element has the expected value.
    // Protractor patches 'expect' to understand promises.

    expect(greeting.getText()).toEqual('Hello Julie!');
  });
});

This simple script (example_spec.js) tests the ‘The Basics’ example on the angularjs.org homepage.

 

Global Variables

Protractor exports these global variables to your spec (test) file:

  • browser – A wrapper around an instance of WebDriver, used for navigation and page-wide information. The browser.get method loads a page. Protractor expects Angular to be present on a page, so it will throw an error if the page it is attempting to load does not contain the Angular library. (If you need to interact with a non-Angular page, you may access the wrapped webdriver instance directly with browser.driver).
  • element – A helper function for finding and interacting with DOM elements on the page you are testing. The element function searches for an element on the page. It requires one parameter, a locator strategy for locating the element. See Using Locators for more information. See Protractor’s findelements test suite (elements_spec.js) for more examples.
  • by – A collection of element locator strategies. For example, elements can be found by CSS selector, by ID, or by the attribute they are bound to with ng-model. See Using Locators.
  • protractor – The Protractor namespace which wraps the WebDriver namespace. Contains static variables and classes, such as protractor.Key which enumerates the codes for special keyboard signals.

 

Config Files

The configuration file tells Protractor how to set up the Selenium Server, which tests to run, how to set up the browsers, and which test framework to use. The configuration file can also include one or more global settings. A simple configuration (conf.js) is shown below.

// An example configuration file
exports.config = {
  // The address of a running selenium server.
  seleniumAddress: 'http://localhost:4444/wd/hub',

  // Capabilities to be passed to the webdriver instance.
  capabilities: {
    'browserName': 'chrome'
  },

  // Spec patterns are relative to the configuration file location passed
  // to protractor (in this example conf.js).
  // They may include glob patterns.
  specs: ['example-spec.js'],

  // Options to be passed to Jasmine-node.
  jasmineNodeOpts: {
    showColors: true, // Use colors in the command line report.
  }
};

https://github.com/angular/protractor/blob/master/docs/referenceConf.js

 

 

Getting Started

Here is a simple Angular JS app to mimic a calculator.

Set Express to run on localhost port 3456 app/expressserver.js:

var express = require('express');
var app = express();
var util = require('util');

app.use(express.static(__dirname));
app.listen(3456);
console.log('Server running at http://localhost:3456');

Runnpm installwith the following dependencies in /package.json.

{
  "name": "protractor-demo",
  "version": "0.0.1",
  "description": "A demo app with Protractor tests",
  "scripts": {
    "start": "node app/expressserver.js",
    "test": "node_modules/.bin/protractor test/conf.js"
  },
  "dependencies": {
    "express": "~3.4.0",
    "protractor": "2.1.0",
    "mkdirp": "~0.3.5",
    "q": "1.0.0",
    "firefox-profile": "0.3.4"
  }
}

See Angular Javascript file and HTML Page source code below.

 

 

Setup

    1. Use npm to install Protractor globally (omit the -g if you’d prefer not to install globally):
      npm install -g protractor

      This will install two command line tools, protractor and webdriver-manager. Try running protractor --version to make sure it’s working.

    2. The webdriver-manager is a helper tool to easily get an instance of a Selenium Server running. Use it to download the necessary binaries with:
      webdriver-manager update
      webdriver-manager start

      This will start up a Selenium Server and will output a bunch of info logs. Protractor test will send requests to this server to control a local browser http://localhost:4444/wd/hub

    3. Protractor needs two files to run, a spec file and a configuration file.
      • /test/conf.js -This configuration tells Protractor where your test files (specs) are, and where to talk to your Selenium Server (seleniumAddress). It specifies that we will be using Jasmine for the test framework. It will use the defaults for all other configuration. Chrome is the default browser.
        // conf.js
        exports.config = {
          directConnect: true,
        
          framework: 'jasmine2',
        
          specs: [
            'spec.js'
          ],
        
          capabilities: {
            'browserName': 'chrome'
          },
        };
      • /test/spec.js – This configuration tells Protractor where your test files (specs) are, and where to talk to your Selenium Server (seleniumAddress). It specifies that we will be using Jasmine for the test framework. It will use the defaults for all other configuration. Chrome is the default browser.
        // spec.js
        describe('Protractor Demo App', function() {
          it('should have a title', function() {
            browser.get('http://localhost:3456');
        
            expect(browser.getTitle()).toEqual('Super Calculator');
          });
        });
    4. Now run the test withprotractor conf.jsYou should see a Chrome browser window open up and navigate to the Calculator, then close itself (this should be very fast!). The test output should be 1 tests, 1 assertion, 0 failures.

 

Interacting with elements

Interact with element in the HTML page (see index.js) below.

// spec.js
describe('Protractor Demo App', function() {
  it('should add one and two', function() {
    browser.get('http://localhost:3456');
    element(by.model('first')).sendKeys(1);
    element(by.model('second')).sendKeys(2);

    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).
        toEqual('5'); // This is wrong!
  });
});

The element function is used for finding HTML elements on your webpage. It returns an ElementFinder object, which can be used to interact with the element or get information from it. In this test, we use sendKeys to type into <input>s, click to click a button, and getText to return the content of an element.

      • by.model('first') to find the element with ng-model="first". If you inspect the Calculator page source, you will see this is <input type=text ng-model="first">.
      • by.id('gobutton') to find the element with the given id. This finds <button id="gobutton">.
      • by.binding('latest') to find the element bound to the variable latest. This finds the span containing {{latest}}Learn more about locators and ElementFinders.

Run the tests withprotractor conf.js

 

Writing multiple scenarios

Run the navigation out into a beforeEach function which is run before every it block. We’ve also stored the ElementFinders for the first and second input in nice variables that can be reused.

// spec.js
describe('Protractor Demo App', function() {
  var firstNumber = element(by.model('first'));
  var secondNumber = element(by.model('second'));
  var goButton = element(by.id('gobutton'));
  var latestResult = element(by.binding('latest'));

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

  it('should have a title', function() {
    expect(browser.getTitle()).toEqual('Super Calculator');
  });

  it('should add one and two', function() {
    firstNumber.sendKeys(1);
    secondNumber.sendKeys(2);

    goButton.click();

    expect(latestResult.getText()).toEqual('3');
  });

  it('should add four and six', function() {
    // Fill this in.
    expect(latestResult.getText()).toEqual('10');
  });
});

 

 

Changing the configuration

The configuration file lets you change things like which browsers are used and how to connect to the Selenium Server. The capabilities object describes the browser to be tested against. For a full list of options, see the reference config file.

// conf.js
exports.config = {
  framework: 'jasmine',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec.js'],
  multiCapabilities: [{
    browserName: 'firefox'
  }, {
    browserName: 'chrome'
  }]
}

 

 

Lists of elements

To deal with a list of multiple elements, use element.all, which returns an ElementArrayFinder.

  1. Create a helper function, add. We’ve added the variable history
  2. Use element.all with the by.repeater Locator to get an ElementArrayFinder.
  3. ElementArrayFinder has many methods in addition to count. Let’s use last to get an ElementFinder that matches the last element found by the Locator. Change the test to:
// spec.js
describe('Protractor Demo App', function() {
  var firstNumber = element(by.model('first'));
  var secondNumber = element(by.model('second'));
  var goButton = element(by.id('gobutton'));
  var latestResult = element(by.binding('latest'));
  var history = element.all(by.repeater('result in memory'));

  function add(a, b) {
    firstNumber.sendKeys(a);
    secondNumber.sendKeys(b);
    goButton.click();
  }

  beforeEach(function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
  });

  it('should have a history', function() {
    add(1, 2);
    add(3, 4);

    expect(history.count()).toEqual(2);

    add(5, 6);

    expect(history.count()).toEqual(0); // This is wrong!
  });
});
it('should have a history', function() {
  add(1, 2);
  add(3, 4);

  expect(history.last().getText()).toContain('1 + 2');
  expect(history.first().getText()).toContain('foo'); // This is wrong!
});

toContain Jasmine matcher to assert that the element text contains “1 + 2”. The full element text will also contain the timestamp and the result.

ElementArrayFinder also has methods each, map, filter, and reduce which are analogous to JavaScript Array methods. Read the API for more details.

 

 

Source code

/app/index.html

<!DOCTYPE HTML>
<html ng-app="calculator">
<head>
  <script src="./angular.min.js"></script>
  <script src="./calc.js"></script>
  <link href="./bootstrap.css" rel="stylesheet">
</head>
<body class="ng-cloak">
  <div ng-controller="CalcCtrl" class="container">
    <div>
      <h3>Super Calculator</h3>
      <form class="form-inline">
        <input ng-model="first" type="text" class="input-small"/>
        <select ng-model="operator" class="span1"
                ng-options="value for (key, value) in operators">
        </select>
        <input ng-model="second" type="text" class="input-small"/>
        <button ng-click="doAddition()" id="gobutton" class="btn">Go!</button>
        <h2>{{latest}}</h2>
      </form>
    </div>
    <h4>History</h4>
    <table class="table">
      <thead><tr>
        <th>Time</th>
        <th>Expression</th>
        <th>Result</th>
      </tr></thead>
      <tr ng-repeat="result in memory">
        <td>
          {{result.timestamp | date:'mediumTime'}}
        </td>
        <td>
          <span>{{result.first}}</span>
          <span>{{result.operator}}</span>
          <span>{{result.second}}</span>
        </td>
        <td>{{result.value}}</td>
      </tr>
    </table>
  </div>
</body>
</html>

 /app/calc.js

var calculator = angular.module('calculator', []).
    controller('CalcCtrl', CalcCtrl);

var CalcCtrl = function($timeout, $scope) {
  $scope.memory = [];
  $scope.latest = 0;
  $scope.operators = {
    ADDITION: '+',
    SUBTRACTION: '-',
    MULTIPLICATION: '*',
    DIVISION: '/',
    MODULO: '%'
  };
  $scope.operator = $scope.operators.ADDITION;

  $scope.doAddition = function() {
    var times = 5;
    $scope.latest = '. ';
    $timeout(function tickslowly() {
      if (times == 0) {
        var latestResult;
        var first = parseInt($scope.first);
        var second = parseInt($scope.second);
        switch ($scope.operator) {
          case '+':
            latestResult = first + second;
            break;
          case '-':
            latestResult = first - second;
            break;
          case '*':
            latestResult = first * second;
            break;
          case '/':
            latestResult = first / second;
            break;
          case '%':
            latestResult = first % second;
            break;
        }
        $scope.memory.unshift({
          timestamp: new Date(),
          first: $scope.first,
          operator: $scope.operator,
          second: $scope.second,
          value: latestResult
        });
        $scope.first = $scope.second = '';
        $scope.latest = latestResult;
      } else {
        $scope.latest += '. ';
        times--;
        $timeout(tickslowly, 300);
      }
    }, 300)
  };
};

/test/spec.js

describe('slow calculator', function() {
  beforeEach(function() {
    browser.get('http://localhost:3456');
  });

  it('should add numbers', function() {
    element(by.model('first')).sendKeys(4);
    element(by.model('second')).sendKeys(5);

    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).
        toEqual('9');
  });

  describe('memory', function() {
    var first, second, goButton;
    beforeEach(function() {
      first = element(by.model('first'));
      second = element(by.model('second'));
      goButton = element(by.id('gobutton'));
    });

    it('should start out with an empty memory', function () {
      var memory =
          element.all(by.repeater('result in memory'));

      expect(memory.count()).toEqual(0);
    });

    it('should fill the memory with past results', function() {
      first.sendKeys(1);
      second.sendKeys(1);
      goButton.click();

      first.sendKeys(10);
      second.sendKeys(20);
      goButton.click();

      var memory = element.all(by.repeater('result in memory').
          column('result.value'));
      memory.then(function (arr) {
        expect(arr.length).toEqual(2);
        expect(arr[0].getText()).toEqual('30'); // 10 + 20 = 30
        expect(arr[1].getText()).toEqual('2'); // 1 + 1 = 2
      });
    });
  });
});

 

Tutorials

 

References