Protractor end to end testing with Gherkin syntax and Typescript decorators

Creating the base project structure

Create a new folder and add the following folders:

  • build
  • build\tasks
  • features
  • page-objects
  • reports
  • steps
  • support

Create a default package file by performing:

npm init -y  

Install all the packages that are required:

npm i chai cucumber cucumber-tsflow gulp gulp-clean gulp-protractor gulp-protractor-cucumber-html-report gulp-typescript protractor protractor-cucumber-framework require-dir typings --save-dev  

This will install the following npm packages:

Setup gulp

In the root folder create a file named gulpfile.ts and add the following content:

// all gulp tasks are located in the ./build/tasks directory
// gulp configuration is in files in ./build directory
require('require-dir')('build/tasks');  

Setup typescript transpile

Create a build.js file in the directory build\tasks and add the following content:

var gulp = require("gulp");  
var ts = require("gulp-typescript");  
var tsProject = ts.createProject('tsconfig.json');  
var clean = require("gulp-clean");  
var paths = require('../paths');

gulp.task("clean", function () {  
    return gulp.src(paths.dist, { read: false }).pipe(clean());
});

gulp.task("build", ["clean"], function () {  
    var tsResult = tsProject.src().pipe(ts(tsProject));
    return tsResult.js.pipe(gulp.dest(paths.dist));
});

In the build folder add a file named paths.js and add the following content:

module.exports = {  
    dist: "dist/"
};

This will be the location where you store all the paths to the resources used in your build scripts. Next add the configuration settings for Typescript by adding a file named tsconfig.json in the root folder of your project with the following content:

{
    "compilerOptions": {
        "moduleResolution": "node",
        "module": "umd",
        "target": "es5",
        "declaration": true,
        "sourceMap": true,
        "removeComments": false,
        "experimentalDecorators": true
    },
    "exclude": [
        "node_modules"
    ],
    "filesGlob": [
        "steps/**/*.ts"
    ]
}

Add typings definitions for the javascript libs we are using:

typings i dt~chai dt~angular-protractor dt~selenium-webdriver --save --global  

Setup Protractor

In the build folder add a file named test.js and add the following content:

var gulp = require("gulp");  
var webdriver_update = require("gulp-protractor").webdriver_update;  
var protractor = require("gulp-protractor").protractor;  
var reporter = require("gulp-protractor-cucumber-html-report");  
var paths = require('../paths');

gulp.task("webdriver_update", webdriver_update);

gulp.task("e2e", ["build"], function () {  
    return gulp.src(paths.features)
      .pipe(protractor({
        configFile: "protractor.conf.js"
      }))
      .on("error", function (e) { throw e; });
});

gulp.task("e2e-report", function () {  
    gulp.src(paths.testResultJson)
        .pipe(reporter({
            dest: paths.e2eReports
        }));
});

This will allow you to use gulp to start the running the protractor test. Extend your paths.js file to include the new paths used above:

module.exports = {  
    dist: "dist/",
    features: "features/**/*.feature",
    testResultJson: "./reports/cucumber-test-results.json",
    e2eReports: "reports/e2e"
};

In the root folder of your project add the file protractor.conf.js and add the following content:

var paths = require('build/paths');  
exports.config = {  
    directConnect: true,
    multiCapabilities: [
     {'browserName': 'firefox'},
     {'browserName': 'chrome'},
     {'browserName': 'internet explorer'}
    ]
    seleniumServerJar: './node_modules/gulp-protractor/node_modules/protractor/selenium/selenium-server-standalone-2.53.1.jar',
    framework: 'custom',
    frameworkPath: 'node_modules/protractor-cucumber-framework',
    specs: [
      paths.features
    ],
    jasmineNodeOpts: {
        showColors: true,
        defaultTimeoutInterval: 30000
    },
    cucumberOpts: {
        require: [paths.distFiles, paths.support],
        format: "json"
    }
};

The above setup will run your end to end tests in Chrome, Firefox, and Internet Explorer 11 in parallel. You can remove browsers if for example it is okey to only test your tests in Chrome.

You can also replace the multiCapabilities property with the text below to only test in Chrome.

capabilities: {  
   'browserName': 'chrome'
},

Extend your build\paths.js file once more, to include the new paths:

module.exports = {  
    dist: "dist/",
    distFiles: "dist/**/*.js",
    testResultJson: "./reports/cucumber-test-results.json",
    e2eReports: "reports/e2e"
    features: "features/**/*.feature",
    support: "support/*.js"
};

Setup create screenshot on failure

It would be nice to see a screenshot of the current view if the test fails. The script below will add a screenshot to a failed step.

Create a new file named TakeScreenshot.js in the support folder with the following content:

module.exports = function TakeScreenshot() {  
    this.After(function (scenario, callback) {
       if (scenario.isFailed()) {
            browser.takeScreenshot().then(function (png) {
                var decodedImage = new Buffer(png.replace(/^data:image\/(png|gif|jpeg);base64,/,''), 'base64');
                scenario.attach(decodedImage, 'image/png');
                callback();
            });
        }
        else {
            callback();
        }
    });
};

Add another new file with the name jsonOutputHook.js (also in the support folder) and add the content:

var paths = require("../build/paths");

module.exports = function JsonOutputHook() {  
    var Cucumber = require('cucumber');
    var JsonFormatter = Cucumber.Listener.JsonFormatter();
    var fs = require('fs');
    var path = require('path');

    JsonFormatter.log = function (json) {
        fs.writeFile(path.join(__dirname, '../'+ paths.testResultJson), json, function (err) {
            if (err) throw err;
            console.log('json file location: ' + path.join(__dirname, '../' + paths.testResultJson));
        });
    };

    this.registerListener(JsonFormatter);
};

When this is in place gulp e2e will run your end to end test and gulp e2e-report will generate a report based on the results from the last gulp e2e run.

Example of a failing scenario

Example of a feature

A feature is defined in a .feature file and can have multiple scenarios defined in a language called Gherkin. Gherkin is a language that allows you to keep a definition of the business logic that can be shared with the stakeholders of the project.

A .feature file (stored in the features folder) can, for example, contain the following scenario:

Feature: Create a new teamie  
    In order to start a new teamie
    As an team member that is the organizer of a teamie survey for my team
    I want to be able to create a teamie so that my team can improve

    Background:
        Given I received an link to create a new Teamie
        And I opened the link in my browser

    Scenario: Team name must be set
        Given I don't enter a team name for my team
        Then A error message should occur 'The team name cannot be empty'

To implement the logic behind the above story we implement a step definition (in the steps folder) and page objects (in the page-objects folder).

A page object is a reusable structure of the page itself. This allows us to write this logic once and use it in any step definition that requires it. Which means a page object is the object that contains all our Selenium like syntax to access objects on our page.

export default class NewTeamiePageObject {  
    public static setTeamName(name: string): webdriver.promise.Promise<void> {
        return element(by.id("lblName")).sendKeys(name);
    }
}

Next we have a steps file, this file will has the methods that will be called by CucumberJS if they match regex pattern defined in the decorator. And will in turn call the methods defined on our page object.

Below is the sample code to handle the step Given I don't enter a team name for my team. Each class that contains steps must be decorated with the @bindings decorator and each method must be decorated with the @given (or @when, @then) decorator.

CucumberJS will test all the regular expressions defined on the step definitions and execute the step definition if it's a match with the step text.

As you can see below there is also a method with the regular expression /^I enter the team name '(.*)' for my team$/. This regex will handle strings that match I enter the team name 'team 1' for my team and supply the value team 1 to the method to make your code more flexible.

import { binding, given, then, when } from "cucumber-tsflow";  
import { expect } from "chai";  
import { newTeamiePageObject } from "./../../page-objects/NewTeamiePageObject";

@binding()
class NewTeamieSteps {

    @given(/^I don't enter a team name for my team$/)
    public GivenIDontEnterATeamNameForMyTeam (callback): void {
        newTeamiePageObject.setTeamName("").then(callback);
    }

    @given(/^I enter the team name '(.*)' for my team$/)
    public GivenEnterATeamNameForMyTeam (teamName, callback): void {
        newTeamiePageObject.setTeamName(teamName).then(callback);
    }    
}

export = NewTeamieSteps;  

Note: the last line (the export) is required to be written in this way to get things to work, using export default class won't work :-(