Meet Protractor

A speed date with end to end testing

What is Protractor?

  • Wraps WebDriver/Selenium 
  • Utilizes Jasmine, Mocha or Cucumber 
  • Makes hooking everything together easier
  • Replaces Karma for Angular e2e testing
  • Made for Angular, but not required

An end to end testing framework of frameworks

Why do we need e2e testing?

  • Running real interactions in a real browser in a real deployment scenario with real* data
  • Validate components actually work together
  • Write a single test that runs against local and prod
    • Meaning you know they behave the same

Do what the users do

​* The realness of the data obviously depends on the enviornment

Doesn't e2e testing suck?

  • Selenium Grids for faster parallel testing 
  • You already know the syntax
    (Jasmine, Mocha, etc)
  • Better browser drivers
  • TheIntern.io? (Maybe not the best choice for Angular sites)

Yes. But it's gotten better. 

Why Protractor?

You could use WebDriver JS, but…

For Angular sites

  • DOM access helpers
    • By: models, bindings, ng-options, ng-repeat items
  • waitForAngular
    • Page load
    • Binding updates
  • DOM access helpers
    • By: Button text (incl partial),
             cssContainingText
  • Custom locators
    • e.g. Handlebars
  • Globals: element, by, $, $$

For non-Angular sites

  • Protractor config helps simply a lot of setup.
  • Jasmine 1.3 didn't easily support async tests.

Not to mention...

Additional globals

The protractor global wraps the webdriver namespace.

elem.sendKeys(protractor.Key.ENTER);
browser.driver.manage().deleteAllCookies();

The browser global extends / contains webdriver.WebDriver. The raw driver is accessible through browser.driver.

Comparison

Protractor vs WebDriverJS

describe('angularjs demo page', function() {
   it('should add one and two', function() {

      //Global browser wraps already built WD
      browser.get('http://bit.ly/1tUzecs');

      //Global 'by' means shorter syntax
      element(by.model('first')).sendKeys(1);
      element(by.id('gobutton')).click();

      //Global 'expect' unpacks Promises
      expect(element(by.binding('latest'))
            .getText()).toEqual('3'); 
   }); 
});
var assert = require('assert'), 
    test = require('selenium-webdriver/testing'), 
    wd = require('selenium-webdriver'); 

test.describe('Google Search', function() { 
   test.it('should work', function() { 
      var driver = new wd.Builder()
          .withCapabilities(
            wd.Capabilities.chrome())
          .build(); //Manual WD build

      driver.get('http://www.google.com');
      driver.findElement(wd.By.name('q'))
            .sendKeys('foo');
      driver.findElement(wd.By.name('btnG'))
            .click();

      driver.getTitle().then( function(title) {
         assert.equal(title, 'foo - Google');
      }); 
      driver.quit(); //Note manual quit
   }); 
});

Async Commands

WebDriverJS Control Flow + Protractor upgrades

To avoid callback hell, WebDriver uses Control Flow to queue commands.

element.click()
element.sendKeys('foo')
//is the same as 
element.click().then(function() {element.sendKeys('foo') })
//Jasmine expect has been modified (jasminewd) to unwrap promises
expect(browser.getTitle()).toEqual('Protractor ');

What you get out of the box

  • Jasmine (1.3) Other frameworks supported, but not Jasmine 2.0. Soon to be Jasmine 2.x after Protractor 1.5
  • WebDriver Manager Install & Update Selenium Servers and browser drivers
  • Selenium Standalone Server So you don't need to run a remote server
  • Chrome Driver Get testing right away. IE is a short command away
  • elementexplorer.js Debugging element locators interactively

node_modules/protractor/[bin|selenium]

Protractor

Selenium Server

Web Server

localhost

localhost

localhost

CI server

Where does everything run?

Dev, QA, Stage, Prod

Another way to look at it

Test

Server

Browser

Or

Getting Started

npm install -g protractor
describe('angularjs homepage', function() {
  it('should have a title', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
    expect(browser.getTitle()).toEqual('Super Calculator');
  });
});
exports.config = {
  directConnect: true,
  specs: ['spec.js']
}

Write a test spec  //spec.js

Install with npm

Configure your setup  //protractor.config.js

Text

Text

protractor protractor.config.js

Text

Run

Configuration

  • shardTestFiles
    files for capability will run in parallel
  • onPrepare
    Setup Jasmine reporters,
    Screen size, take screenshots
  • params
    Global vars avail in all tests
    params: { user:'foo', pwd:'bar' }

Highlights

Seriously. The reference config is great.

Okay, maybe a little more down below

Config for non-Angular apps

//config.js
onPrepare: function(){
   browser.ignoreSynchronization = true;
}

//Otherwise in your specs
beforeEach(function() {
   return browser.ignoreSynchronization = true;
});
//config.js
onPrepare: function(){
   global.isAngularSite = function(flag){
      browser.ignoreSynchronization = !flag;
   };
}

//*.spec.js
beforeEach(function() {
   isAngularSite(false);
});

Text

//config.js
onPrepare: function(){
   browser.ignoreSynchronization = true;
}

//Otherwise in your specs
beforeEach(function() {
   return browser.ignoreSynchronization = true;
});

Basic setup

Gettin' fancy

Sharing Config

//base.config.js
exports.config = {
  baseUrl: http://nebraskajs.com/
  multiCapabilities: [{"browserName": "internet explorer"},{'browserName': 'chrome'}]
}

//local.chromeOnly.config.js
exports.config = (function(){
  var r = require('./protractor.base.config.js');  //Use require to pull in your base config
  var tmpConfig = r.config;

  tmpConfig.chromeOnly = true;
  tmpConfig.chromeDriver = './node_modules/protractor/selenium/chromedriver';
  tmpConfig.baseUrl = 'https://localhost:3000/';

  //Use capabilities instead of multiCapabilities for grunt reasons
  tmpConfig.capabilities = {
     'browserName': 'chrome',
     'chromeOptions': { 'args': ['show-fps-counter=true'] }
  };

  /* Null or clear out fields that might conflict with the above statements */
  tmpConfig.multiCapabilities = [];

  return tmpConfig;
})();

onPrepare is your friend

 //Just to give you some ideas

  onPrepare: function () {
    var jasmineReporters = require('jasmine-reporters');
    var fs = require('fs-extra');

    browser.driver.manage().window().setSize(1024, 768);
    browser.manage().timeouts().pageLoadTimeout(5000);
    browser.manage().timeouts().implicitlyWait(3000);

    // The IE driver doesn't like get('#/foo') 
    // This allows us to call getRoute('#/foo');
    global.getRoute = function(route) { 
        return browser.get('/appContext/' + route);
    };


    var dir = 'out/tests/protractor/xmloutput';
    fs.ensureDir(dir, function (err) {
      if (err) {
        console.log(err);
      }
      //dir has now been created, including the directory it is to be placed in
    });


    var capsPromise = browser.getCapabilities();
    capsPromise.then(function (caps) {
      var browserName = caps.caps_.browserName.toUpperCase();
      var browserVersion = caps.caps_.version;
      var prePendStr = browserName + '-' + browserVersion + '-';
      //Do something cool
   });

    jasmine.getEnv().addReporter(new jasmine.ConsoleReporter());

     /**
       * NOTE: Changing versions of protractor or jasmine might require this to change.
       * https://github.com/larrymyers/jasmine-reporters#protractor
       */
      jasmine.getEnv().addReporter(
        new jasmine.JUnitXmlReporter(dir, true, true)
      );

      
}

Grunt + Protractor

  • Avoid multiple config files

        grunt protractor:full:prod:ie
  • Cannot override multiCapabilities
  • keepAlive: false //Needed for Jenkins

Overriding your config file

Page Objects

  • Separates how something is done vs what it does
  • Reduces code duplication
  • Makes your tests more readable

An e2e testing best practice

Example Page Object

var AngularDemoPage = function() {
  this.firstInput = element(by.model('first'));
  this.secondInput = element(by.model('second'));
  this.goButton = element(by.id('gobutton'));
  this.latest = element(by.binding('latest'));

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

  this.setInputs = function(val1, val2) {
    this.firstInput.sendKeys(val1);
    this.secondInput.sendKeys(val2);
  }

  this.clickGoButton = function() {
    return this.goButton.click();
  }

  /** @return Promise resolves answer value **/
  this.getLatestText = function() {
    return this.latest.getText();
  }
};
module.exports = AngularDemoPage;

IDE autocomplete based on JSDocs makes Page Objects even better.

Testing with Page Objects

var AngularDemoPage = require('./demo.po.js');
var errorUtils = require('./../common/errors.util.js');

describe('angularjs demo page', function() {
   it('should add one and two', function() {

      AngularDemoPage.get();
      AngularDemoPage.setInputs(1,2);
      AngularDemoPage.clickGoButton();

      expect(AngularDemoPage.getLatestText()).toEqual('3'); 
      expect(errorUtils.hasErrors()).toBe(false);  //Global page utils
   }); 
});

Project Structure

|-- e2e/  
|    |-- components/
|    |     |-- datepicker.component.js
|    |-- homepage/
|    |     |-- homepage.po.js
|    |     |-- *.spec.js
|    |-- profile/
|    |     |-- profile.po.js
|    |     |-- *.spec.js
|-- e2e/  
|    |-- components/
|    |     |-- datepicker.component.js
|    |-- pages/
|    |     |-- homepage.page.js
|    |     |-- profile.page.js
|    |-- specs/
|    |     |-- homepage/
|    |     |       |-- hompage.login.spec.js
|    |     |-- profile/
|    |     |       |-- profile.social.spec.js
|    |     |       |-- profile.pwdChange.spec.js
|    |     |-- *.spec.js

Pick one and stick with it

But keep your specs grouped by area.  It helps in defining test suites.

Nitty Gritty of locators

Locator <= by.*(string)
ElementFinder <= element[.all](locator)
Promise <= foundElem.takeSomeAction()


An ElementFinder will not contact the browser until an action method has been called. Since all actions are asynchronous, all action methods return a promise.

element(by.css('some-css')).element(by.tagName('tag-within-css')).click();
element.all(by.css('some-css')).first().element(by.tagName('tag-within-css'));

ElementFinders can be chained

Just instructions until you pull the trigger

Debugging

  • Wait vs Sleep vs Debugger vs Pause
    • Wait - Loop wait until a condition is true
    • Sleep - Make the driver sleep for x millis
    • Debugger - Pauses test & injects helper functions into browser
    • Pause (BETA) - Inject protractor debugger in the control flow
      • This is currently consider the best option
  • No support (yet) for getting browser console
  • Docs (kinda) cover...

The thing that might drive you mad

GOTCHAS

What still sucks

  • Still tough to debug
    • "But WHY did my test fail?"
  • On screen visibility - "Element not clickable"
  • Driver differences (See leadfoot)
  • No speed throttle
  • Keeping on top of changes/failures

e2e testing - better, but not awesome

Still true

Learn More

Other Cool Stuff

var testability = angular.getTestability(document.body);
testability.allowAnimations(false); //Faster tests
//Get notified when pending async ops known to Angular have been completed.  
testability.whenStable(myCallback); //This is what protractor expect() uses
$compileProvider.debugInfoEnabled(false) //prod - significant performance boost

Thank You

Laptop designed by B. Agustín Amenábar Larraín from the Noun Project
Server designed by aLf from the Noun Project
Server designed by Norbert Kucsera from the Noun Project
Website designed by Max Miner from the Noun Project
Banana Peel designed by Liliane Lass Erbe from the Noun Project

Ramon Victor - Protractor for AngularJS

Credits

The Noun Project for kick ass icons

Meet Protractor

By Derek Eskens