An opinionated template for writing Cucumber tests with Protractor.
CukeFarm provides a set of Cucumber Steps that can be used to build feature files that are backed by automation using the Protractor framework. It also provides a set of helper functions that can be used when writing your own Step Definitions. Check out the docs directory for a full list of the Steps and helper functions. The docs are automatically generated using docha.
To begin, install Protractor. Follow the instructions in the 'Prerequisites' and 'Setup' sections of the Protractor Tutorial.
Next, install Cucumber using the following command:
npm install cucumber --save-dev
Install CukeFarm by executing the following command from the root of your project:
npm install cukefarm --save-dev
CukeFarm provides a generic Protractor config file. However, you must provide some additional options that are specific to your project.
- Create file called
protractor.conf.js
- Use the
require
function to import CukeFarm - On the CukeFarm config object, create the following properties:
specs = <path_to_your_feature_files>
capabilities.browserName = <protractor_browser_name>
- On the CukeFarm config object, push the path to your project specific World file (See 'World Object' below) onto the
cucumberOpts.require
property - Set the CukeFarm config object as the config property on the module exports object
Below is a sample protractor.conf.js
file that provides the minimum options necessary to run your tests:
# protractor.conf.js
var config = require('cukefarm').config;
config.specs = '../features/**/*.feature';
config.capabilities.browserName = 'chrome';
config.cucumberOpts.require.push('../support/World.js');
exports.config = config;
CukeFarm provides a set of general Step Definitions, but you will likely need to add more that are specific to your project. Simply push the path of your Step Definition files onto the CukeFarm config's cucumberOpts.require
property:
# protractor.conf.js
files = ['./support/World.js', './step_definitions/**/*.js'];
for (i = 0; i < files.length; i++) {
config.cucumberOpts.require.push(files[i]);
}
There are a number of different options that Protractor looks for when parsing the test config. You can add these additional options in the same way you added the properties above.
For a full list of what options can be passed to Protractor, see their Reference Configuration File
The CukeFarm World object give you access to a number of helper functions that will aid you in writing your Step Definitions. However, you must provide a Page Object Map specific to your project.
A Page Object Map will map the Page Objects that you create to human language Strings that can be used when writing Gherkin.
Here is a sample Page Object Map:
# PageObjectMap.js
module.exports = {
"Page One" : require('./pages/PageOne'),
"Page Two" : require('./pages/PageTwo'),
"Page Three" : require('./pages/PageThree')
};
Note: The above sample works, but it requires you to update the map every time you add or remove a Page Object. If you instead define your Page Objects using our Best Practices, you can dynamically create the Page Object Map.
- Create a file called
World.js
- Use the
require
function to import CukeFarm - Use the
require
function to import your Page Object Map - Use the
require
function to import thedefineSupportCode
function from Cucumber - Set the pageObjectMap property of the CukeFarm World prototype to your Page Object Map
- You must set this on the prototype because Cucumber actually instantiates the World itself using a Constructor function
- Call the
setWorldConstructor
function inside ofdefineSupportCode
and pass it theWorld
constructor
Below is a sample World.js
file:
var World = require('cukefarm').World;
var {defineSupportCode} = require('cucumber');
World.prototype.pageObjectMap = require('./PageObjectMap');
defineSupportCode(function({setWorldConstructor}) {
setWorldConstructor(World);
});
One of the guiding principles of CukeFarm is that steps should be reusable across multiple page objects wherever possible. This allows you to DRY up your code and prevent an explosion of Step Definitions. To enable this design, CukeFarm forces you to store what page you are on. Generally this is done by calling either the Given I am on the "<something>" page
or the Then I should be on the "<something>" page
step, which takes the captured Gherkin and instantiates the page it is mapped to. Later when you try to access a WebElement, the step may use the stored page object to access it, eliminating the need for separate steps per page object. For instance, a sample scenario might look like this:
# Search.feature
Scenario: Clicking the "Foo" button on the Search Page will fill in the "Bar" field on the Results Page
Given I am on the "Search" page
When I type "Foo" in the "Search" field
And I click the "Search" button
Then I should be on the "Results" page
And the "Showing Results For Field" should contain the text "Foo"
CukeFarm does force you to adhere to certain practices and conventions when writing Cucumber scenarios.
CukeFarm expects you to organize the representation of your system into objects similar to the WebDriver Page Object. CukeFarm has the following expectations of your Page Objects:
Adhering to the following conventions will allow you to use the stringToVariableName
function on the Transform object to convert captured Gherkin into Page Object keys:
- Every WebElement you intend to interact with on a page should be a property of that Page Object
- Your key should be formatted [nameOfElement][TypeOfElement]. For instance
fooButton
- Your key should be camel case with the first letter being lower case.
Each Page Object is expected to have a waitForLoaded
function that returns a promise. The promise should only resolve if the page successfully loads. A typical waitForLoaded
function will look something like this:
this.waitForLoaded = function() {
return browser.wait((function(_this) {
return function() {
return _this.barField.isPresent();
};
})(this), 1000);
};
To provide easy access for the 'Given I am on the "" page' step to reach your page, your page object should contain a get
function that somehow navigates to your page. A typical get
function will look something like this:
this.get = function() {
return browser.get('search');
};
Be sure to export the Page Object class as opposed to an instance of it.
- Rather than simply exporting the class, export an object that has two properties: the class and a Gherkin name for your Page Object.
- Why: This allows you to dynamically generate a Page Object Map by grabbing all Page Object files using a library like node-globules and accessing the Gherkin name from the Page Object export. See below for an example.
Below is the example Scenario from above along with the Page Objects and Page Object Map necessary to support it:
# Search.feature
Scenario: Clicking the "Foo" button on the Search Page will fill in the "Bar" field on the Results Page
Given I am on the "Search" page
When I type "Foo" in the "Search" field
And I click the "Search" button
Then I should be on the "Results" page
And the "Showing Results For Field" should contain the text "Foo"
# PageObjectMap.js
var file, files, globule, i, len, page, path;
globule = require('globule');
path = require('path');
files = globule.find('e2e/pages/**/*.js');
for (i = 0; i < files.length; i++) {
page = require(path.resolve(files[i]));
module.exports[page.name] = page["class"];
}
# SearchPage.js
var SearchPage = function SearchPage() {
this.searchField = $('input.search-field');
this.searchButton = $('button.search-button');
this.get = function() {
return browser.get('search');
};
this.waitForLoaded = function() {
return browser.wait((function(_this) {
return function() {
return _this.searchButton.isPresent();
};
})(this), 30000);
};
}
module.exports = {
"class": SearchPage,
name: 'Search'
};
# ResultsPage.js
var ResultsPage = function ResultsPage() {
this.showingResultsForField = $('span.results-for');
this.get = function() {
return browser.get('results');
};
this.waitForLoaded = function() {
return browser.wait((function(_this) {
return function() {
return _this.showingResultsForField.isPresent();
};
})(this), 30000);
};
}
module.exports = {
"class": ResultsPage,
name: 'Results'
};
# Search.html
<html>
<body>
<input class="search-field" />
<button class="search-button">Search</button>
</body>
</html>
# Results.html
<html>
<body>
<span class="results-for">Showing results for Foo</span>
</body>
</html>
To run your scenarios, simply execute the following command:
protractor path/to/your/protractor.conf.js
CukeFarm provides helper functions on the following objects that are defined on the World.
The transform
object contains functions to transform strings that were captured by Step Names into other data types to be used in the Step Definition.
All functions provided by the transform
object are also provided as custom transforms so that they can be direcly applied to capture groups when using Cucumber Expressions.
The elementHelper
object contains functions to interact with Protractor elements.
Pull requests are always welcome. Please make sure to adhere to the following guidelines:
In particular, be sure to unit test your Step Definitions. This should be done in two ways:
- Test that the name (regex) matches what you expect it to match.
- Test that code within the Step Definition functions as you expect.
Note: The unit tests are the contract for the Step Definition names. Any changes to Step Definition names that do not break any unit tests are considered to be backward compatible and may occur at any time in a minor version or patch. IT IS YOUR RESPONSIBILITY TO SAFEGUARD YOUR FEATURE FILES.
- Install Firefox
- Run
npm install
to download dependencies. - Run
npm --prefix ./spec/test_app/ install ./spec/test_app/
to download dependencies for the test app. - Run
npm test
to run the unit tests.