1 | CukeFarm
|
2 | ===
|
3 |
|
4 | An opinionated template for writing Cucumber tests with Protractor.
|
5 |
|
6 | 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].
|
7 |
|
8 | [![Build Status](https://travis-ci.org/ReadyTalk/cukefarm.svg?branch=master)](https://travis-ci.org/ReadyTalk/cukefarm)
|
9 |
|
10 | # Getting Started
|
11 |
|
12 | To begin, install Protractor. Follow the instructions in the 'Prerequisites' and 'Setup' sections of the [Protractor Tutorial].
|
13 |
|
14 | Next, install Cucumber using the following command:
|
15 |
|
16 | npm install cucumber --save-dev
|
17 |
|
18 | # Installation
|
19 |
|
20 | Install CukeFarm by executing the following command from the root of your project:
|
21 |
|
22 | npm install cukefarm --save-dev
|
23 |
|
24 | # Set Up
|
25 |
|
26 | ## Config Object
|
27 |
|
28 | CukeFarm provides a generic Protractor config file. However, you must provide some additional options that are specific to your project.
|
29 |
|
30 | ### Necessary Options
|
31 |
|
32 | * Create file called `protractor.conf.js`
|
33 | * Use the `require` function to import CukeFarm
|
34 | * On the CukeFarm config object, create the following properties:
|
35 | * `specs = <path_to_your_feature_files>`
|
36 | * `capabilities.browserName = <protractor_browser_name>`
|
37 | * On the CukeFarm config object, push the path to your project specific World file (See 'World Object' below) onto the `cucumberOpts.require` property
|
38 | * Set the CukeFarm config object as the config property on the module exports object
|
39 |
|
40 | Below is a sample `protractor.conf.js` file that provides the minimum options necessary to run your tests:
|
41 |
|
42 | # protractor.conf.js
|
43 |
|
44 | var config = require('cukefarm').config;
|
45 |
|
46 | config.specs = '../features/**/*.feature';
|
47 | config.capabilities.browserName = 'chrome';
|
48 | config.cucumberOpts.require.push('../support/World.js');
|
49 |
|
50 | exports.config = config;
|
51 |
|
52 | ### Adding Step Definitions
|
53 |
|
54 | 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:
|
55 |
|
56 | # protractor.conf.js
|
57 |
|
58 | files = ['./support/World.js', './step_definitions/**/*.js'];
|
59 | for (i = 0; i < files.length; i++) {
|
60 | config.cucumberOpts.require.push(files[i]);
|
61 | }
|
62 |
|
63 | ### Additional Options
|
64 |
|
65 | 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.
|
66 |
|
67 | For a full list of what options can be passed to Protractor, see their [Reference Configuration File]
|
68 |
|
69 | ## World Object
|
70 |
|
71 | 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.
|
72 |
|
73 | ### What is a Page Object Map?
|
74 |
|
75 | A Page Object Map will map the Page Objects that you create to human language Strings that can be used when writing Gherkin.
|
76 |
|
77 | ### What does a Page Object Map look like?
|
78 |
|
79 | Here is a sample Page Object Map:
|
80 |
|
81 | # PageObjectMap.js
|
82 |
|
83 | module.exports = {
|
84 | "Page One" : require('./pages/PageOne'),
|
85 | "Page Two" : require('./pages/PageTwo'),
|
86 | "Page Three" : require('./pages/PageThree')
|
87 | };
|
88 |
|
89 | 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.
|
90 |
|
91 | ### Adding a Page Object Map to the World
|
92 |
|
93 | * Create a file called `World.js`
|
94 | * Use the `require` function to import CukeFarm
|
95 | * Use the `require` function to import your Page Object Map
|
96 | * Use the `require` function to import the `defineSupportCode` function from Cucumber
|
97 | * Set the pageObjectMap property of the CukeFarm World _prototype_ to your Page Object Map
|
98 | * You must set this on the prototype because Cucumber actually instantiates the World itself using a Constructor function
|
99 | * Call the `setWorldConstructor` function inside of `defineSupportCode` and pass it the `World` constructor
|
100 |
|
101 | Below is a sample `World.js` file:
|
102 |
|
103 | var World = require('cukefarm').World;
|
104 | var {defineSupportCode} = require('cucumber');
|
105 |
|
106 | World.prototype.pageObjectMap = require('./PageObjectMap');
|
107 |
|
108 | defineSupportCode(function({setWorldConstructor}) {
|
109 | setWorldConstructor(World);
|
110 | });
|
111 |
|
112 | ### Why use a Page Object Map?
|
113 |
|
114 | 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:
|
115 |
|
116 | # Search.feature
|
117 |
|
118 | Scenario: Clicking the "Foo" button on the Search Page will fill in the "Bar" field on the Results Page
|
119 | Given I am on the "Search" page
|
120 | When I type "Foo" in the "Search" field
|
121 | And I click the "Search" button
|
122 | Then I should be on the "Results" page
|
123 | And the "Showing Results For Field" should contain the text "Foo"
|
124 |
|
125 | # Writing Cucumber Scenarios with CukeFarm
|
126 |
|
127 | CukeFarm does force you to adhere to certain practices and conventions when writing Cucumber scenarios.
|
128 |
|
129 | ## Page Objects
|
130 |
|
131 | 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:
|
132 |
|
133 | ### WebElement Property Naming Conventions
|
134 |
|
135 | 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:
|
136 |
|
137 | * Every WebElement you intend to interact with on a page should be a property of that Page Object
|
138 | * Your key should be formatted [nameOfElement][TypeOfElement]. For instance `fooButton`
|
139 | * Your key should be camel case with the first letter being lower case.
|
140 |
|
141 | ### `waitForLoaded` Function
|
142 |
|
143 | 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:
|
144 |
|
145 | this.waitForLoaded = function() {
|
146 | return browser.wait((function(_this) {
|
147 | return function() {
|
148 | return _this.barField.isPresent();
|
149 | };
|
150 | })(this), 1000);
|
151 | };
|
152 |
|
153 | ### `get` Function
|
154 |
|
155 | To provide easy access for the 'Given I am on the "<something>" 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:
|
156 |
|
157 | this.get = function() {
|
158 | return browser.get('search');
|
159 | };
|
160 |
|
161 | ### Export the class
|
162 |
|
163 | Be sure to export the Page Object _class_ as opposed to an instance of it.
|
164 |
|
165 | ### Best Practices
|
166 |
|
167 | * Rather than simply exporting the class, export an object that has two properties: the class and a Gherkin name for your Page Object.
|
168 | * 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.
|
169 |
|
170 | ### Example Page Objects
|
171 |
|
172 | Below is the example Scenario from above along with the Page Objects and Page Object Map necessary to support it:
|
173 |
|
174 | # Search.feature
|
175 |
|
176 | Scenario: Clicking the "Foo" button on the Search Page will fill in the "Bar" field on the Results Page
|
177 | Given I am on the "Search" page
|
178 | When I type "Foo" in the "Search" field
|
179 | And I click the "Search" button
|
180 | Then I should be on the "Results" page
|
181 | And the "Showing Results For Field" should contain the text "Foo"
|
182 |
|
183 |
|
184 | # PageObjectMap.js
|
185 |
|
186 | var file, files, globule, i, len, page, path;
|
187 |
|
188 | globule = require('globule');
|
189 | path = require('path');
|
190 |
|
191 | files = globule.find('e2e/pages/**/*.js');
|
192 |
|
193 | for (i = 0; i < files.length; i++) {
|
194 | page = require(path.resolve(files[i]));
|
195 | module.exports[page.name] = page["class"];
|
196 | }
|
197 |
|
198 |
|
199 | # SearchPage.js
|
200 |
|
201 | var SearchPage = function SearchPage() {
|
202 |
|
203 | this.searchField = $('input.search-field');
|
204 | this.searchButton = $('button.search-button');
|
205 |
|
206 | this.get = function() {
|
207 | return browser.get('search');
|
208 | };
|
209 |
|
210 | this.waitForLoaded = function() {
|
211 | return browser.wait((function(_this) {
|
212 | return function() {
|
213 | return _this.searchButton.isPresent();
|
214 | };
|
215 | })(this), 30000);
|
216 | };
|
217 | }
|
218 |
|
219 | module.exports = {
|
220 | "class": SearchPage,
|
221 | name: 'Search'
|
222 | };
|
223 |
|
224 |
|
225 | # ResultsPage.js
|
226 |
|
227 | var ResultsPage = function ResultsPage() {
|
228 |
|
229 | this.showingResultsForField = $('span.results-for');
|
230 |
|
231 | this.get = function() {
|
232 | return browser.get('results');
|
233 | };
|
234 |
|
235 | this.waitForLoaded = function() {
|
236 | return browser.wait((function(_this) {
|
237 | return function() {
|
238 | return _this.showingResultsForField.isPresent();
|
239 | };
|
240 | })(this), 30000);
|
241 | };
|
242 | }
|
243 |
|
244 | module.exports = {
|
245 | "class": ResultsPage,
|
246 | name: 'Results'
|
247 | };
|
248 |
|
249 |
|
250 | # Search.html
|
251 |
|
252 | <html>
|
253 | <body>
|
254 | <input class="search-field" />
|
255 | <button class="search-button">Search</button>
|
256 | </body>
|
257 | </html>
|
258 |
|
259 |
|
260 | # Results.html
|
261 |
|
262 | <html>
|
263 | <body>
|
264 | <span class="results-for">Showing results for Foo</span>
|
265 | </body>
|
266 | </html>
|
267 |
|
268 | # Running Scenarios
|
269 |
|
270 | To run your scenarios, simply execute the following command:
|
271 |
|
272 | protractor path/to/your/protractor.conf.js
|
273 |
|
274 | # Helper Functions
|
275 |
|
276 | CukeFarm provides helper functions on the following objects that are defined on the World.
|
277 |
|
278 | ## `transform` Object
|
279 |
|
280 | 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.
|
281 |
|
282 | ## Custom Transforms
|
283 |
|
284 | 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.
|
285 |
|
286 | ## `elementHelper` Object
|
287 |
|
288 | The `elementHelper` object contains functions to interact with Protractor elements.
|
289 |
|
290 | # Contributing to CukeFarm
|
291 |
|
292 | Pull requests are always welcome. Please make sure to adhere to the following guidelines:
|
293 |
|
294 | ## Unit Test your code
|
295 |
|
296 | In particular, be sure to unit test your Step Definitions. This should be done in two ways:
|
297 | 1. Test that the name (regex) matches what you expect it to match.
|
298 | 2. Test that code within the Step Definition functions as you expect.
|
299 |
|
300 | 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.
|
301 |
|
302 | # Running CukeFarm Unit Tests
|
303 |
|
304 | * Install [Firefox]
|
305 | * Run `npm install` to download dependencies.
|
306 | * Run `npm --prefix ./spec/test_app/ install ./spec/test_app/` to download dependencies for the test app.
|
307 | * Run `npm test` to run the unit tests.
|
308 |
|
309 | [Cucumber]:https://www.npmjs.com/package/cucumber
|
310 | [Protractor]:http://angular.github.io/protractor
|
311 | [docs]:docs
|
312 | [docha]:https://github.com/tehsenaus/docha
|
313 | [Protractor Tutorial]:https://angular.github.io/protractor/#/tutorial
|
314 | [Reference Configuration File]:https://github.com/angular/protractor/blob/master/docs/referenceConf.js
|
315 | [WebDriver Page Object]:https://code.google.com/p/selenium/wiki/PageObjects
|
316 | [node-globules]:https://github.com/cowboy/node-globule
|
317 | [Firefox]:https://www.mozilla.org/en-US/
|