├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.MD ├── features ├── buy-workwear.feature └── google-search.feature ├── img └── cucumber-html-report.png ├── index.js ├── junit └── junit-report.xml ├── package.json ├── page-objects ├── google-search.js └── mammoth-workwear.js ├── reports ├── .gitignore └── .gitkeep ├── runtime ├── chromeDriver.js ├── electronDriver.js ├── firefoxDriver.js ├── helpers.js ├── phantomDriver.js └── world.js ├── shared-objects └── test-data.js ├── shippable.yml └── step-definitions ├── buy-workwear-steps.js └── google-search-steps.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # TODO: move from shippable to github actions -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | package-lock.json 4 | .idea/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, John Doherty 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # selenium-cucumber-js 2 | 3 | [![npm](https://img.shields.io/npm/dt/selenium-cucumber-js.svg)](https://www.npmjs.com/package/selenium-cucumber-js) 4 | 5 | JavaScript browser automation framework using official [selenium-webdriver](http://seleniumhq.github.io/selenium/docs/api/javascript/ "view webdriver js documentation") and [cucumber-js](https://github.com/cucumber/cucumber-js "view cucumber js documentation"). 6 | 7 | If you prefer to work with puppeteer check out [puppeteer-cucumber-js](https://github.com/orca-scan/puppeteer-cucumber-js) 8 | 9 | **Table of Contents** 10 | 11 | * [Installation](#installation) 12 | * [Usage](#usage) 13 | * [Options](#options) 14 | * [Configuration file](#configuration-file) 15 | * [Feature files](#feature-files) 16 | * [Step definitions](#step-definitions) 17 | * [Page objects](#page-objects) 18 | * [Shared objects](#shared-objects) 19 | * [Helpers](#helpers) 20 | * [Visual Comparison](#visual-comparison) 21 | * [Before/After hooks](#beforeafter-hooks) 22 | * [Reports](#reports) 23 | * [How to debug](#how-to-debug) 24 | * [Directory structure](#directory-structure) 25 | * [Demo](#demo) 26 | * [Bugs](#bugs) 27 | * [Contributing](#contributing) 28 | * [Troubleshooting](#troubleshooting) 29 | * [IntelliJ Cucumber Plugin](#intellij-cucumber-plugin) 30 | 31 | ## Installation 32 | 33 | ```bash 34 | npm install selenium-cucumber-js --save-dev 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```bash 40 | node ./node_modules/selenium-cucumber-js/index.js -s ./step-definitions 41 | ``` 42 | 43 | ### Options 44 | 45 | ```bash 46 | -h, --help output usage information 47 | -V, --version output the version number 48 | -s, --steps path to step definitions. defaults to ./step-definitions 49 | -p, --pageObjects path to page objects. defaults to ./page-objects 50 | -o, --sharedObjects [paths] path to shared objects (repeatable). defaults to ./shared-objects 51 | -b, --browser name of browser to use. defaults to chrome 52 | -k, --browser-teardown browser teardown strategy after every scenario (always, clear, none). defaults to "always" 53 | -r, --reports output path to save reports. defaults to ./reports 54 | -d, --disableLaunchReport disable the auto opening the browser with test report 55 | -j, --junit output path to save junit-report.xml defaults to ./reports 56 | -t, --tags name of tag to run 57 | -f, --featureFile a specific feature file to run 58 | -x, --timeOut steps definition timeout in milliseconds. defaults to 10 seconds 59 | -n, --noScreenshot disable auto capturing of screenshots when an error is encountered 60 | ``` 61 | 62 | By default tests are run using Google Chrome, to run tests using another browser supply the name of that browser along with the `-b` switch. Available options are: 63 | 64 | Browser | Example 65 | ---------- | --------------- 66 | Chrome | `-b chrome` 67 | Firefox | `-b firefox` 68 | Phantom JS | `-b phantomjs` 69 | Electron | `-b electron` 70 | Custom | `-b customDriver.js` 71 | 72 | To use your own driver, create a customDriver.js file in the root of your project and provide the filename with the `-b` switch. 73 | 74 | #### Configuration file 75 | 76 | Configuration options can be set using a `selenium-cucumber-js.json` file at the root of your project. The JSON keys use the "long name" from the command line options. For example the following duplicates default configuration: 77 | 78 | ```json 79 | { 80 | "steps": "./step-definitions", 81 | "pageObjects": "./page-objects", 82 | "sharedObjects": "./shared-objects", 83 | "reports": "./reports", 84 | "browser": "chrome", 85 | "timeout": 10000 86 | } 87 | ``` 88 | 89 | Whereas the following would set configuration to match the expected directory structure of IntelliJ's Cucumber plugin, and make default timeout one minute. _Note that the default browser has not been overridden and will remain 'chrome'._ 90 | 91 | ```json 92 | { 93 | "steps": "./features/step_definitions", 94 | "pageObjects": "./features/page_objects", 95 | "sharedObjects": "./features/shared_objects", 96 | "reports": "./features/reports", 97 | "timeout": 60000 98 | } 99 | ``` 100 | 101 | ### Feature files 102 | 103 | A feature file is a [Business Readable, Domain Specific Language](http://martinfowler.com/bliki/BusinessReadableDSL.html) file that lets you describe software’s behavior without detailing how that behavior is implemented. Feature files are written using the [Gherkin syntax](https://github.com/cucumber/cucumber/wiki/Gherkin) and must live in a folder named **features** within the root of your project. 104 | 105 | ```gherkin 106 | # ./features/google-search.feature 107 | 108 | Feature: Searching for vote cards app 109 | As an internet user 110 | In order to find out more about the itunes vote cards app 111 | I want to be able to search for information about the itunes vote cards app 112 | 113 | Scenario: Google search for vote cards app 114 | When I search Google for "itunes vote cards app" 115 | Then I should see some results 116 | ``` 117 | 118 | ### Browser teardown strategy 119 | 120 | The browser automatically closes after each scenario to ensure the next scenario uses a fresh browser environment. But 121 | you can change this behavior with the "-k" or the "--browser-teardown" parameter. 122 | 123 | Value | Description 124 | ---------- | --------------- 125 | `always` | the browser automatically closes (default) 126 | `clear` | the browser automatically clears cookies, local and session storages 127 | `none` | the browser does nothing 128 | 129 | ### Step definitions 130 | 131 | Step definitions act as the glue between features files and the actual system under test. 132 | 133 | _To avoid confusion **always** return a JavaScript promise your step definition in order to let cucumber know when your task has completed._ 134 | 135 | ```javascript 136 | // ./step-definitions/google-search-steps.js 137 | 138 | module.exports = function () { 139 | 140 | this.Then(/^I should see some results$/, function () { 141 | 142 | // driver wait returns a promise so return that 143 | return driver.wait(until.elementsLocated(by.css('div.g')), 10000).then(function(){ 144 | 145 | // return the promise of an element to the following then. 146 | return driver.findElements(by.css('div.g')); 147 | }) 148 | .then(function (elements) { 149 | 150 | // verify this element has children 151 | expect(elements.length).to.not.equal(0); 152 | }); 153 | }); 154 | }; 155 | ``` 156 | 157 | The following variables are available within the ```Given()```, ```When()``` and ```Then()``` functions: 158 | 159 | | Variable | Description | 160 | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 161 | | `driver` | an instance of [selenium web driver](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html) (_the browser_) 162 | | `selenium` | the raw [selenium-webdriver](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/) module, providing access to static properties/methods 163 | | `page` | collection of **page** objects loaded from disk and keyed by filename 164 | | `shared` | collection of **shared** objects loaded from disk and keyed by filename 165 | | `helpers` | a collection of [helper methods](runtime/helpers.js) _things selenium does not provide but really should!_ 166 | | `by` | the selenium [By](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_By.html) class used to locate elements on the page 167 | | `until` | the selenium [until](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/until.html) class used to wait for elements/events 168 | | `expect` | instance of [chai expect](http://chaijs.com/api/bdd/) to ```expect('something').to.equal('something')``` 169 | | `assert` | instance of [chai assert](http://chaijs.com/api/assert/) to ```assert.isOk('everything', 'everything is ok')``` 170 | | `trace` | handy trace method to log console output with increased visibility 171 | 172 | ### Page objects 173 | 174 | Page objects are accessible via a global ```page``` object and are automatically loaded from ```./page-objects``` _(or the path specified using the ```-p``` switch)_. Page objects are exposed via a camel-cased version of their filename, for example ```./page-objects/google-search.js``` becomes ```page.googleSearch```. You can also use subdirectories, for example ```./page-objects/dir/google-search.js``` becomes ```page.dir.googleSearch```. 175 | 176 | Page objects also have access to the same runtime variables available to step definitions. 177 | 178 | An example page object: 179 | 180 | ```javascript 181 | // ./page-objects/google-search.js 182 | 183 | module.exports = { 184 | 185 | url: 'http://www.google.co.uk', 186 | 187 | elements: { 188 | searchInput: by.name('q'), 189 | searchResultLink: by.css('div.g > h3 > a') 190 | }, 191 | 192 | /** 193 | * enters a search term into Google's search box and presses enter 194 | * @param {string} searchQuery 195 | * @returns {Promise} a promise to enter the search values 196 | */ 197 | performSearch: function (searchQuery) { 198 | 199 | var selector = page.googleSearch.elements.searchInput; 200 | 201 | // return a promise so the calling function knows the task has completed 202 | return driver.findElement(selector).sendKeys(searchQuery, selenium.Key.ENTER); 203 | } 204 | }; 205 | ``` 206 | 207 | And its usage within a step definition: 208 | 209 | ```js 210 | // ./step-definitions/google-search-steps.js 211 | this.When(/^I search Google for "([^"]*)"$/, function (searchQuery) { 212 | 213 | return helpers.loadPage('http://www.google.com').then(function() { 214 | 215 | // use a method on the page object which also returns a promise 216 | return page.googleSearch.performSearch(searchQuery); 217 | }) 218 | }); 219 | ``` 220 | 221 | ### Shared objects 222 | 223 | Shared objects allow you to share anything from test data to helper methods throughout your project via a global ```shared``` object. Shared objects are automatically loaded from ```./shared-objects``` _(or the path specified using the ```-o``` switch)_ and made available via a camel-cased version of their filename, for example ```./shared-objects/test-data.js``` becomes ```shared.testData```. You can also use subdirectories, for example ```./shared-objects/dir/test-data.js``` becomes ```shared.dir.testData```. 224 | 225 | 226 | Shared objects also have access to the same runtime variables available to step definitions. 227 | 228 | An example shared object: 229 | 230 | ```javascript 231 | // ./shared-objects/test-data.js 232 | 233 | module.exports = { 234 | username: "import-test-user", 235 | password: "import-test-pa**word" 236 | } 237 | ``` 238 | 239 | And its usage within a step definition: 240 | 241 | ```js 242 | module.exports = function () { 243 | 244 | this.Given(/^I am logged in"$/, function () { 245 | 246 | driver.findElement(by.name('usn')).sendKeys(shared.testData.username); 247 | driver.findElement(by.name('pass')).sendKeys(shared.testData.password); 248 | }); 249 | }; 250 | ``` 251 | 252 | ### Helpers 253 | 254 | `selenium-cucumber-js` contains a few helper methods to make working with selenium a bit easier, those methods are: 255 | 256 | ```js 257 | // Load a URL, returning only when the tag is present 258 | helpers.loadPage('http://www.google.com'); 259 | 260 | // get the value of a HTML attribute 261 | helpers.getAttributeValue('body', 'class'); 262 | 263 | // get a list of elements matching a query selector who's inner text matches param. 264 | helpers.getElementsContainingText('nav[role="navigation"] ul li a', 'Safety Boots'); 265 | 266 | // get first elements matching a query selector who's inner text matches textToMatch param 267 | helpers.getFirstElementContainingText('nav[role="navigation"] ul li a', 'Safety Boots'); 268 | 269 | // click element(s) that are not visible (useful in situations where a menu needs a hover before a child link appears) 270 | helpers.clickHiddenElement('nav[role="navigation"] ul li a','Safety Boots'); 271 | 272 | // wait until a HTML attribute equals a particular value 273 | helpers.waitUntilAttributeEquals('html', 'data-busy', 'false', 5000); 274 | 275 | // wait until a HTML attribute exists 276 | helpers.waitUntilAttributeExists('html', 'data-busy', 5000); 277 | 278 | // wait until a HTML attribute no longer exists 279 | helpers.waitUntilAttributeDoesNotExists('html', 'data-busy', 5000); 280 | 281 | // get the content value of a :before pseudo element 282 | helpers.getPseudoElementBeforeValue('body header'); 283 | 284 | // get the content value of a :after pseudo element 285 | helpers.getPseudoElementAfterValue('body header'); 286 | 287 | // clear the cookies 288 | helpers.clearCookies(); 289 | 290 | // clear both local and session storages 291 | helpers.clearStorages(); 292 | 293 | // clear both cookies and storages 294 | helpers.clearCookiesAndStorages('body header'); 295 | 296 | // waits until an element to exist and returns it 297 | helpers.waitForCssXpathElement('#login-button', 5000); 298 | 299 | // scroll until element is visible 300 | helpers.scrollToElement(webElement); 301 | 302 | // select a value inside a dropdown list by its text 303 | helpers.selectByVisibleText('#country', 'Brazil'); 304 | 305 | // waits and returns an array of all windows opened 306 | helpers.waitForNewWindows(); 307 | ``` 308 | 309 | ### Visual Comparison 310 | 311 | The `selenium-cucumber-js` framework uses [Applitools Eyes](https://applitools.com/) to add visual checkpoints to your JavaScript Selenium tests. It takes care of getting screenshots of your application from the underlying WebDriver, sending them to the Applitools Eyes server for validation and failing the test when differences are detected. To perform visual comparisons within your tests, obtain an [Applitools Eyes](https://applitools.com/) API Key and assign it to the `eye_key` property of the `selenium-cucumber-js.json` config file in the root of your project. 312 | 313 | For example the following configuration could be used with an increased timeout which allows enough time for visual checks: 314 | 315 | ```json 316 | { 317 | "eye_key": "Your_Api_Key", 318 | "timeout": 50000 319 | } 320 | ``` 321 | 322 | And its usage within page Objects: 323 | 324 | ```js 325 | module.exports = { 326 | 327 | url: 'https://applitools.com/helloworld', 328 | 329 | elements: { 330 | clickme: by.tagName('button'), 331 | searchResultLink: by.css('div.g > h3 > a') 332 | }, 333 | 334 | applitools_Eyes_Example: function () { 335 | 336 | // Start the test and set the browser's viewport size to 800x600. 337 | eyes.open(driver, 'Hello World!', 'My first Javascript test!', 338 | {width: 800, height: 600}); 339 | 340 | // Navigate the browser to the "hello world!" web-site. 341 | driver.get(page.HelloWorld.elements.url); 342 | 343 | // Visual checkpoint #1. 344 | eyes.checkWindow('Main Page'); 345 | 346 | // Click the "Click me!" button. 347 | driver.findElement(page.HelloWorld.elements.clickme).click(); 348 | 349 | // Visual checkpoint #2. 350 | eyes.checkWindow('Click!'); 351 | 352 | // End the test. 353 | eyes.close(); 354 | } 355 | }; 356 | ``` 357 | 358 | ### Before/After hooks 359 | 360 | You can register before and after handlers for features and scenarios: 361 | 362 | | Event | Example 363 | | -------------- | ------------------------------------------------------------ 364 | | BeforeFeature | ```this.BeforeFeatures(function(feature, callback) {})``` 365 | | AfterFeature | ```this.AfterFeature(function(feature, callback) {});``` 366 | | BeforeScenario | ```this.BeforeScenario(function(scenario, callback) {});``` 367 | | AfterScenario | ```this.AfterScenario(function(scenario, callback) {});``` 368 | 369 | ```js 370 | module.exports = function () { 371 | 372 | // add a before feature hook 373 | this.BeforeFeature(function(feature, done) { 374 | console.log('BeforeFeature: ' + feature.getName()); 375 | done(); 376 | }); 377 | 378 | // add an after feature hook 379 | this.AfterFeature(function(feature, done) { 380 | console.log('AfterFeature: ' + feature.getName()); 381 | done(); 382 | }); 383 | 384 | // add before scenario hook 385 | this.BeforeScenario(function(scenario, done) { 386 | console.log('BeforeScenario: ' + scenario.getName()); 387 | done(); 388 | }); 389 | 390 | // add after scenario hook 391 | this.AfterScenario(function(scenario, done) { 392 | console.log('AfterScenario: ' + scenario.getName()); 393 | done(); 394 | }); 395 | }; 396 | ``` 397 | 398 | ### Reports 399 | 400 | HTML and JSON reports are automatically generated and stored in the default `./reports` folder. This location can be changed by providing a new path using the `-r` command line switch: 401 | 402 | ![Cucumber HTML report](img/cucumber-html-report.png) 403 | 404 | ### How to debug 405 | 406 | Most selenium methods return a [JavaScript Promise](https://spring.io/understanding/javascript-promises "view JavaScript promise introduction") that is resolved when the method completes. The easiest way to step in with a debugger is to add a ```.then``` method to a selenium function and place a ```debugger``` statement within it, for example: 407 | 408 | ```js 409 | module.exports = function () { 410 | 411 | this.When(/^I search Google for "([^"]*)"$/, function (searchQuery, done) { 412 | 413 | driver.findElement(by.name('q')).then(function(input) { 414 | expect(input).to.exist; 415 | debugger; // <<- your IDE should step in at this point, with the browser open 416 | return input; 417 | }) 418 | .then(function(input){ 419 | input.sendKeys(searchQuery); 420 | input.sendKeys(selenium.Key.ENTER); 421 | 422 | done(); // <<- let cucumber know you're done 423 | }); 424 | }); 425 | }; 426 | ``` 427 | 428 | ### Directory structure 429 | 430 | You can use the framework without any command line arguments if your application uses the following folder structure: 431 | 432 | ```bash 433 | . 434 | ├── features 435 | │ └── google-search.feature 436 | ├── step-definitions 437 | │ └── google-search-steps.js 438 | ├── page-objects 439 | │ └── google-search.js 440 | └── shared-objects 441 | │ ├── test-data.js 442 | │ └── stuff.json 443 | └── reports 444 | ├── cucumber-report.json 445 | └── cucumber-report.html 446 | ``` 447 | 448 | ## Demo 449 | 450 | This project includes an example to help you get started. You can run the example using the following command: 451 | 452 | ```bash 453 | node ./node_modules/selenium-cucumber-js/index.js 454 | ``` 455 | 456 | ## Bugs 457 | 458 | Please raise bugs via the [selenium-cucumber-js issue tracker](https://github.com/john-doherty/selenium-cucumber-js/issues) and, if possible, please provide enough information to allow the bug to be reproduced. 459 | 460 | ## Contributing 461 | 462 | Everyone is very welcome to contribute to this project. You can contribute just by submitting bugs or suggesting improvements by [opening an issue on GitHub](https://github.com/john-doherty/selenium-cucumber-js/issues). 463 | 464 | ## Troubleshooting 465 | 466 | ### IntelliJ Cucumber Plugin 467 | 468 | IntelliJ based IDE's have a plugin that allows the tester to control click on a `Given`, `When`, `Then` statement within a Cucumber feature file and have the user taken to the associated step definition. This plugin relies on your project having the following folder structure: 469 | 470 | ```bash 471 | . 472 | └── features 473 | │ google-search.feature 474 | └── step_definitions 475 | │ └── google-search-steps.js 476 | └── page_objects 477 | │ └── google-search.js 478 | └── shared_objects 479 | │ ├── test-data.js 480 | │ └── stuff.json 481 | └── reports 482 | ├── cucumber-report.json 483 | └── cucumber-report.html 484 | ``` 485 | 486 | This can be achieved by restructuring your project to match the layout above _(notice the underscores)_, and running your tests with the following switches: 487 | 488 | ```bash 489 | node ./node_modules/selenium-cucumber-js/index.js -s ./features/step_definitions -p ./features/page_objects -o ./features/shared_objects -r ./features/reports 490 | ``` 491 | 492 | ### VSCode Cucumber Plugin 493 | 494 | Visual Studio Code has also an extension for Cucumber (Gherkin) Language Support + Format + Steps/PageObjects Autocomplete. You can find how to install and use at [Cucumber (Gherkin) Full Support](https://marketplace.visualstudio.com/items?itemName=alexkrechik.cucumberautocomplete). 495 | 496 | Following the default structure, the `settings.json` should look like this: 497 | 498 | ```json 499 | { 500 | "cucumberautocomplete.steps": [ 501 | "step-definitions/*.js" 502 | ], 503 | "cucumberautocomplete.syncfeatures": "features/*.feature", 504 | "cucumberautocomplete.strictGherkinCompletion": false, 505 | "cucumberautocomplete.onTypeFormat": true, 506 | "editor.quickSuggestions": { 507 | "comments": false, 508 | "strings": true, 509 | "other": true 510 | }, 511 | "cucumberautocomplete.gherkinDefinitionPart": "(Given|When|Then)\\(", 512 | } 513 | ``` 514 | 515 | ## License 516 | 517 | Licensed under [ISC License](LICENSE) © [John Doherty](https://twitter.com/mrjohndoherty) 518 | -------------------------------------------------------------------------------- /features/buy-workwear.feature: -------------------------------------------------------------------------------- 1 | @workwear 2 | Feature: Shop for Workwear 3 | I can search for and buy workwear 4 | 5 | Scenario: View product detail 6 | Given I am on the Mammoth Workwear home page 7 | When I click navigation item "Safety Boots" 8 | And I click product item "Timberland Pro Euro Hiker 2G Safety Boots" 9 | Then I should see product detail with title "Timberland Pro Euro Hiker 2G Safety Boots" -------------------------------------------------------------------------------- /features/google-search.feature: -------------------------------------------------------------------------------- 1 | @search 2 | Feature: Searching for vote cards app 3 | As an internet user 4 | In order to find out more about the itunes vote cards app 5 | I want to be able to search for information about the itunes vote cards app 6 | 7 | Scenario: Google search for vote cards app 8 | When I search Google for "itunes vote cards app" 9 | Then I should see "Vote Cards" in the results 10 | 11 | Scenario: Google search for course of life app 12 | When I search Google for "CourseOf.Life" 13 | Then I should see some results -------------------------------------------------------------------------------- /img/cucumber-html-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-doherty/selenium-cucumber-js/89d52b2aa787d6c8a61ba066a2325a045095436e/img/cucumber-html-report.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs-plus'); 6 | var path = require('path'); 7 | var program = require('commander'); 8 | var pjson = require('./package.json'); 9 | var cucumber = require('cucumber'); 10 | 11 | function collectPaths(value, paths) { 12 | paths.push(value); 13 | return paths; 14 | } 15 | 16 | function coerceInt(value, defaultValue) { 17 | 18 | var int = parseInt(value); 19 | 20 | if (typeof int === 'number') return int; 21 | 22 | return defaultValue; 23 | } 24 | 25 | var config = { 26 | steps: './step-definitions', 27 | pageObjects: './page-objects', 28 | sharedObjects: './shared-objects', 29 | featureFiles: './features', 30 | reports: './reports', 31 | browser: 'chrome', 32 | browserTeardownStrategy: 'always', 33 | timeout: 15000 34 | }; 35 | 36 | var configFileName = path.resolve(process.cwd(), 'selenium-cucumber-js.json'); 37 | 38 | if (fs.isFileSync(configFileName)) { 39 | config = Object.assign(config, require(configFileName)); 40 | } 41 | 42 | program 43 | .version(pjson.version) 44 | .description(pjson.description) 45 | .option('-s, --steps ', 'path to step definitions. defaults to ' + config.steps, config.steps) 46 | .option('-p, --pageObjects ', 'path to page objects. defaults to ' + config.pageObjects, config.pageObjects) 47 | .option('-o, --sharedObjects [paths]', 'path to shared objects (repeatable). defaults to ' + config.sharedObjects, collectPaths, [config.sharedObjects]) 48 | .option('-b, --browser ', 'name of browser to use. defaults to ' + config.browser, config.browser) 49 | .option('-k, --browser-teardown ', 'browser teardown strategy after every scenario (always, clear, none). defaults to "always"', config.browserTeardownStrategy) 50 | .option('-r, --reports ', 'output path to save reports. defaults to ' + config.reports, config.reports) 51 | .option('-d, --disableLaunchReport [optional]', 'Disables the auto opening the browser with test report') 52 | .option('-j, --junit ', 'output path to save junit-report.xml defaults to ' + config.reports) 53 | .option('-t, --tags ', 'name of tag to run', collectPaths, []) 54 | .option('-f, --featureFiles ', 'comma-separated list of feature files to run or path to directory defaults to ' + config.featureFiles, config.featureFiles) 55 | .option('-x, --timeOut ', 'steps definition timeout in milliseconds. defaults to ' + config.timeout, coerceInt, config.timeout) 56 | .option('-n, --noScreenshot [optional]', 'disable auto capturing of screenshots when an error is encountered') 57 | .option('-w, --worldParameters ', 'JSON object to pass to cucumber-js world constructor. defaults to empty', config.worldParameters) 58 | .parse(process.argv); 59 | 60 | program.on('--help', function () { 61 | console.log(' For more details please visit https://github.com/john-doherty/selenium-cucumber-js#readme\n'); 62 | }); 63 | 64 | // store browserName globally (used within world.js to build driver) 65 | global.browserName = program.browser; 66 | global.browserTeardownStrategy = program.browserTeardown; 67 | 68 | // store Eyes Api globally (used within world.js to set Eyes) 69 | global.eyesKey = config.eye_key; 70 | 71 | // used within world.js to import page objects 72 | global.pageObjectPath = path.resolve(program.pageObjects); 73 | 74 | // used within world.js to output reports 75 | global.reportsPath = path.resolve(program.reports); 76 | if (!fs.existsSync(program.reports)) { 77 | fs.makeTreeSync(program.reports); 78 | } 79 | 80 | // used within world.js to decide if reports should be generated 81 | global.disableLaunchReport = (program.disableLaunchReport); 82 | 83 | // used with world.js to determine if a screenshot should be captured on error 84 | global.noScreenshot = (program.noScreenshot); 85 | 86 | // used within world.js to output junit reports 87 | global.junitPath = path.resolve(program.junit || program.reports); 88 | 89 | // set the default timeout to 10 seconds if not already globally defined or passed via the command line 90 | global.DEFAULT_TIMEOUT = global.DEFAULT_TIMEOUT || program.timeOut || 10 * 1000; 91 | 92 | // used within world.js to import shared objects into the shared namespace 93 | global.sharedObjectPaths = program.sharedObjects.map(function (item) { 94 | return path.resolve(item); 95 | }); 96 | 97 | // rewrite command line switches for cucumber 98 | process.argv.splice(2, 100); 99 | 100 | // allow specific feature files to be executed 101 | if (program.featureFiles) { 102 | var splitFeatureFiles = program.featureFiles.split(','); 103 | 104 | splitFeatureFiles.forEach(function (feature) { 105 | process.argv.push(feature); 106 | }); 107 | } 108 | 109 | // add switch to tell cucumber to produce json report files 110 | process.argv.push('-f'); 111 | process.argv.push('pretty'); 112 | process.argv.push('-f'); 113 | process.argv.push('json:' + path.resolve(__dirname, global.reportsPath, 'cucumber-report.json')); 114 | 115 | // add cucumber world as first required script (this sets up the globals) 116 | process.argv.push('-r'); 117 | process.argv.push(path.resolve(__dirname, 'runtime/world.js')); 118 | 119 | // add path to import step definitions 120 | process.argv.push('-r'); 121 | process.argv.push(path.resolve(program.steps)); 122 | 123 | // add tag 124 | if (program.tags) { 125 | program.tags.forEach(function (tag) { 126 | process.argv.push('-t'); 127 | process.argv.push(tag); 128 | }); 129 | } 130 | 131 | if (program.worldParameters){ 132 | process.argv.push('--world-parameters'); 133 | process.argv.push(program.worldParameters); 134 | } 135 | 136 | // add strict option (fail if there are any undefined or pending steps) 137 | process.argv.push('-S'); 138 | 139 | // 140 | // execute cucumber 141 | // 142 | var cucumberCli = cucumber.Cli(process.argv); 143 | 144 | global.cucumber = cucumber; 145 | 146 | cucumberCli.run(function (succeeded) { 147 | 148 | var code = succeeded ? 0 : 1; 149 | 150 | function exitNow() { 151 | process.exit(code); 152 | } 153 | 154 | if (process.stdout.write('')) { 155 | exitNow(); 156 | } 157 | else { 158 | // write() returned false, kernel buffer is not empty yet... 159 | process.stdout.on('drain', exitNow); 160 | } 161 | }); 162 | -------------------------------------------------------------------------------- /junit/junit-report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selenium-cucumber-js", 3 | "version": "1.8.1", 4 | "description": "JavaScript browser automation framework using official selenium-webdriver and cucumber-js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "author": { 10 | "name": "John Doherty", 11 | "email": "contact@johndoherty.info", 12 | "url": "https://courseof.life/johndoherty" 13 | }, 14 | "license": "ISC", 15 | "keywords": [ 16 | "selenium", 17 | "selenium bdd", 18 | "cucumber", 19 | "cucumber-js", 20 | "webdriver", 21 | "selenium-webdriver", 22 | "chromedriver", 23 | "phantomjs", 24 | "testing", 25 | "junit", 26 | "bdd" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/john-doherty/selenium-cucumber-js.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/john-doherty/selenium-cucumber-js/issues" 34 | }, 35 | "engines": { 36 | "node": ">=12.19.0" 37 | }, 38 | "engineStrict": true, 39 | "homepage": "https://github.com/john-doherty/selenium-cucumber-js#readme", 40 | "dependencies": { 41 | "chai": "3.5.0", 42 | "chalk": "1.1.3", 43 | "chromedriver": "^86.0.0", 44 | "commander": "2.9.0", 45 | "cucumber": "1.3.3", 46 | "cucumber-html-reporter": "4.0.4", 47 | "cucumber-junit": "1.6.0", 48 | "electron": "^9.1.0", 49 | "electron-chromedriver": "^1.7.1", 50 | "electron-packager": "^9.1.0", 51 | "electron-prebuilt": "^1.4.13", 52 | "eyes.selenium": "0.0.72", 53 | "fs-plus": "2.9.1", 54 | "geckodriver": "^1.16.2", 55 | "merge": "^1.2.1", 56 | "phantomjs-prebuilt": "2.1.12", 57 | "require-dir": "0.3.2", 58 | "selenium-webdriver": "3.5.0" 59 | }, 60 | "devDependencies": { 61 | "eslint": "^3.19.0", 62 | "eslint-config-airbnb-base": "^11.2.0", 63 | "eslint-plugin-import": "^2.2.0" 64 | }, 65 | "eslintConfig": { 66 | "extends": "airbnb-base", 67 | "env": { 68 | "es6": false, 69 | "browser": true 70 | }, 71 | "globals": { 72 | "selenium": true, 73 | "helpers": true, 74 | "page": true, 75 | "driver": true, 76 | "until": true, 77 | "by": true, 78 | "expect": true, 79 | "Promise": true, 80 | "browserName": true, 81 | "DEFAULT_TIMEOUT": true 82 | }, 83 | "rules": { 84 | "brace-style": [ 85 | "error", 86 | "stroustrup" 87 | ], 88 | "comma-dangle": [ 89 | "error", 90 | "never" 91 | ], 92 | "func-names": 0, 93 | "indent": [ 94 | "error", 95 | 4, 96 | { 97 | "SwitchCase": 1 98 | } 99 | ], 100 | "max-len": [ 101 | 2, 102 | 180, 103 | 4, 104 | { 105 | "ignoreUrls": true, 106 | "ignoreComments": false 107 | } 108 | ], 109 | "new-cap": [ 110 | "error", 111 | { 112 | "capIsNewExceptions": [ 113 | "Router", 114 | "ObjectId", 115 | "DEBUG" 116 | ], 117 | "properties": false 118 | } 119 | ], 120 | "no-underscore-dangle": 0, 121 | "no-unused-vars": [ 122 | "warn" 123 | ], 124 | "no-use-before-define": [ 125 | "error", 126 | { 127 | "functions": false 128 | } 129 | ], 130 | "no-var": [ 131 | "off" 132 | ], 133 | "one-var": [ 134 | "off" 135 | ], 136 | "vars-on-top": [ 137 | "off" 138 | ], 139 | "no-param-reassign": [ 140 | "off" 141 | ], 142 | "no-lone-blocks": [ 143 | "off" 144 | ], 145 | "padded-blocks": 0, 146 | "prefer-template": [ 147 | "off" 148 | ], 149 | "prefer-arrow-callback": [ 150 | "off" 151 | ], 152 | "default-case": [ 153 | "off" 154 | ], 155 | "wrap-iife": [ 156 | 2, 157 | "inside" 158 | ], 159 | "no-plusplus": [ 160 | "off" 161 | ], 162 | "require-jsdoc": [ 163 | "warn", 164 | { 165 | "require": { 166 | "FunctionDeclaration": true, 167 | "MethodDefinition": true, 168 | "ClassDeclaration": true 169 | } 170 | } 171 | ], 172 | "object-shorthand": [ 173 | "error", 174 | "never" 175 | ], 176 | "space-before-function-paren": "off", 177 | "strict": "off", 178 | "valid-jsdoc": [ 179 | "error" 180 | ] 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /page-objects/google-search.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | url: 'http://www.google.co.uk', 4 | 5 | elements: { 6 | searchInput: by.name('q'), 7 | searchResultLink: by.css('div.g > h3 > a') 8 | }, 9 | 10 | /** 11 | * enters a search term into Google's search box and presses enter 12 | * @param {string} searchQuery 13 | * @returns {Promise} a promise to enter the search values 14 | */ 15 | preformSearch: function (searchQuery) { 16 | 17 | var selector = page.googleSearch.elements.searchInput; 18 | 19 | // return a promise so the calling function knows the task has completed 20 | return driver.findElement(selector).sendKeys(searchQuery, selenium.Key.ENTER); 21 | } 22 | }; -------------------------------------------------------------------------------- /page-objects/mammoth-workwear.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | url: 'http://mammothworkwear.com', 4 | 5 | elements: { 6 | menuItem: 'nav[role="navigation"] ul li a', 7 | productItem: 'main .pitem a' 8 | }, 9 | 10 | clickNavigationItem: function(containingText) { 11 | 12 | return helpers.clickHiddenElement(page.mammothWorkwear.elements.menuItem, containingText); 13 | }, 14 | 15 | clickProductItem: function(containingText) { 16 | 17 | return helpers.clickHiddenElement(page.mammothWorkwear.elements.productItem, containingText); 18 | }, 19 | 20 | titleContains: function(expectedTitle) { 21 | 22 | return driver.getTitle().then(function(pageTitle) { 23 | expect(pageTitle).to.contain(expectedTitle); 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /reports/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-doherty/selenium-cucumber-js/89d52b2aa787d6c8a61ba066a2325a045095436e/reports/.gitkeep -------------------------------------------------------------------------------- /runtime/chromeDriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chromedriver = require('chromedriver'); 4 | var selenium = require('selenium-webdriver'); 5 | 6 | /** 7 | * Creates a Selenium WebDriver using Chrome as the browser 8 | * @returns {ThenableWebDriver} selenium web driver 9 | */ 10 | module.exports = function() { 11 | 12 | var driver = new selenium.Builder().withCapabilities({ 13 | browserName: 'chrome', 14 | javascriptEnabled: true, 15 | acceptSslCerts: true, 16 | chromeOptions: { 17 | args: ['start-maximized', 'disable-extensions'] 18 | }, 19 | path: chromedriver.path 20 | }).build(); 21 | 22 | driver.manage().window().maximize(); 23 | 24 | return driver; 25 | }; 26 | -------------------------------------------------------------------------------- /runtime/electronDriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var selenium = require('selenium-webdriver'); 4 | var path = require('path'); 5 | 6 | var myapp = path.resolve(process.cwd(), 'MyApp.app/Contents/MacOS/MyApp'); 7 | /** 8 | * Creates a Selenium WebDriver using Firefox as the browser 9 | * @returns {ThenableWebDriver} selenium web driver 10 | */ 11 | module.exports = function () { 12 | 13 | var driver = new selenium.Builder() 14 | .withCapabilities({ 15 | chromeOptions: { 16 | // Here is the path to your Electron binary. 17 | binary: myapp 18 | } 19 | }) 20 | .forBrowser('electron') 21 | .build(); 22 | 23 | return driver; 24 | }; 25 | -------------------------------------------------------------------------------- /runtime/firefoxDriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var firefox = require('geckodriver'); 4 | var selenium = require('selenium-webdriver'); 5 | 6 | /** 7 | * Creates a Selenium WebDriver using Firefox as the browser 8 | * @returns {ThenableWebDriver} selenium web driver 9 | */ 10 | module.exports = function() { 11 | 12 | var driver = new selenium.Builder().withCapabilities({ 13 | browserName: 'firefox', 14 | javascriptEnabled: true, 15 | acceptSslCerts: true, 16 | 'webdriver.firefox.bin': firefox.path 17 | }).build(); 18 | 19 | driver.manage().window().maximize(); 20 | 21 | return driver; 22 | }; 23 | -------------------------------------------------------------------------------- /runtime/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | /** 4 | * returns a promise that is called when the url has loaded and the body element is present 5 | * @param {string} url - url to load 6 | * @param {integer} waitInSeconds - number of seconds to wait for page to load 7 | * @returns {Promise} resolved when url has loaded otherwise rejects 8 | * @example 9 | * helpers.loadPage('http://www.google.com'); 10 | */ 11 | loadPage: function(url, waitInSeconds) { 12 | 13 | // use either passed in timeout or global default 14 | var timeout = (waitInSeconds) ? (waitInSeconds * 1000) : DEFAULT_TIMEOUT; 15 | 16 | // load the url and wait for it to complete 17 | return driver.get(url).then(function() { 18 | 19 | // now wait for the body element to be present 20 | return driver.wait(until.elementLocated(by.css('body')), timeout); 21 | }); 22 | }, 23 | 24 | /** 25 | * returns the value of an attribute on an element 26 | * @param {string} htmlCssSelector - HTML css selector used to find the element 27 | * @param {string} attributeName - attribute name to retrieve 28 | * @returns {string} the value of the attribute or empty string if not found 29 | * @example 30 | * helpers.getAttributeValue('body', 'class'); 31 | */ 32 | getAttributeValue: function (htmlCssSelector, attributeName) { 33 | 34 | // get the element from the page 35 | return driver.findElement(by.css(htmlCssSelector)).then(function(el) { 36 | return el.getAttribute(attributeName); 37 | }); 38 | }, 39 | 40 | /** 41 | * returns list of elements matching a query selector who's inner text matches param. 42 | * WARNING: The element returned might not be visible in the DOM and will therefore have restricted interactions 43 | * @param {string} cssSelector - css selector used to get list of elements 44 | * @param {string} textToMatch - inner text to match (does not have to be visible) 45 | * @returns {Promise} resolves with list of elements if query matches, otherwise rejects 46 | * @example 47 | * helpers.getElementsContainingText('nav[role="navigation"] ul li a', 'Safety Boots') 48 | */ 49 | getElementsContainingText: function(cssSelector, textToMatch) { 50 | 51 | // method to execute within the DOM to find elements containing text 52 | function findElementsContainingText(query, content) { 53 | 54 | var results = []; // array to hold results 55 | 56 | // workout which property to use to get inner text 57 | var txtProp = ('textContent' in document) ? 'textContent' : 'innerText'; 58 | 59 | // get the list of elements to inspect 60 | var elements = document.querySelectorAll(query); 61 | 62 | for (var i = 0, l = elements.length; i < l; i++) { 63 | if (elements[i][txtProp].trim() === content.trim()) { 64 | results.push(elements[i]); 65 | } 66 | } 67 | 68 | return results; 69 | } 70 | 71 | // grab matching elements 72 | return driver.findElements(by.js(findElementsContainingText, cssSelector, textToMatch)); 73 | }, 74 | 75 | /** 76 | * returns first elements matching a query selector who's inner text matches textToMatch param 77 | * @param {string} cssSelector - css selector used to get list of elements 78 | * @param {string} textToMatch - inner text to match (does not have to be visible) 79 | * @returns {Promise} resolves with first element containing text otherwise rejects 80 | * @example 81 | * helpers.getFirstElementContainingText('nav[role="navigation"] ul li a', 'Safety Boots').click(); 82 | */ 83 | getFirstElementContainingText: function(cssSelector, textToMatch) { 84 | 85 | return helpers.getElementsContainingText(cssSelector, textToMatch).then(function(elements) { 86 | return elements[0]; 87 | }); 88 | }, 89 | 90 | /** 91 | * clicks an element (or multiple if present) that is not visible, useful in situations where a menu needs a hover before a child link appears 92 | * @param {string} cssSelector - css selector used to locate the elements 93 | * @param {string} textToMatch - text to match inner content (if present) 94 | * @returns {Promise} resolves if element found and clicked, otherwise rejects 95 | * @example 96 | * helpers.clickHiddenElement('nav[role="navigation"] ul li a','Safety Boots'); 97 | */ 98 | clickHiddenElement: function(cssSelector, textToMatch) { 99 | 100 | // method to execute within the DOM to find elements containing text 101 | function clickElementInDom(query, content) { 102 | 103 | // get the list of elements to inspect 104 | var elements = document.querySelectorAll(query); 105 | 106 | // workout which property to use to get inner text 107 | var txtProp = ('textContent' in document) ? 'textContent' : 'innerText'; 108 | 109 | for (var i = 0, l = elements.length; i < l; i++) { 110 | 111 | // if we have content, only click items matching the content 112 | if (content) { 113 | 114 | if (elements[i][txtProp] === content) { 115 | elements[i].click(); 116 | } 117 | } 118 | // otherwise click all 119 | else { 120 | elements[i].click(); 121 | } 122 | } 123 | } 124 | 125 | // grab matching elements 126 | return driver.findElements(by.js(clickElementInDom, cssSelector, textToMatch)); 127 | }, 128 | 129 | /** 130 | * Waits until a HTML attribute equals a particular value 131 | * @param {string} elementSelector - HTML element CSS selector 132 | * @param {string} attributeName - name of the attribute to inspect 133 | * @param {string} attributeValue - value to wait for attribute to equal 134 | * @param {integer} waitInMilliseconds - number of milliseconds to wait for page to load 135 | * @returns {Promise} resolves if attribute eventually equals, otherwise rejects 136 | * @example 137 | * helpers.waitUntilAttributeEquals('html', 'data-busy', 'false', 5000); 138 | */ 139 | waitUntilAttributeEquals: function(elementSelector, attributeName, attributeValue, waitInMilliseconds) { 140 | 141 | // use either passed in timeout or global default 142 | var timeout = waitInMilliseconds || DEFAULT_TIMEOUT; 143 | 144 | // readable error message 145 | var timeoutMessage = attributeName + ' does not equal ' + attributeValue + ' after ' + waitInMilliseconds + ' milliseconds'; 146 | 147 | // repeatedly execute the test until it's true or we timeout 148 | return driver.wait(function() { 149 | 150 | // get the html attribute value using helper method 151 | return helpers.getAttributeValue(elementSelector, attributeName).then(function(value) { 152 | 153 | // inspect the value 154 | return value === attributeValue; 155 | }); 156 | 157 | }, timeout, timeoutMessage); 158 | }, 159 | 160 | /** 161 | * Waits until a HTML attribute exists 162 | * @param {string} elementSelector - HTML element CSS selector 163 | * @param {string} attributeName - name of the attribute to inspect 164 | * @param {integer} waitInMilliseconds - number of milliseconds to wait for page to load 165 | * @returns {Promise} resolves if attribute exists within timeout, otherwise rejects 166 | * @example 167 | * helpers.waitUntilAttributeExists('html', 'data-busy', 5000); 168 | */ 169 | waitUntilAttributeExists: function(elementSelector, attributeName, waitInMilliseconds) { 170 | 171 | // use either passed in timeout or global default 172 | var timeout = waitInMilliseconds || DEFAULT_TIMEOUT; 173 | 174 | // readable error message 175 | var timeoutMessage = attributeName + ' does not exists after ' + waitInMilliseconds + ' milliseconds'; 176 | 177 | // repeatedly execute the test until it's true or we timeout 178 | return driver.wait(function() { 179 | 180 | // get the html attribute value using helper method 181 | return helpers.getAttributeValue(elementSelector, attributeName).then(function(value) { 182 | 183 | // attribute exists if value is not null 184 | return value !== null; 185 | }); 186 | 187 | }, timeout, timeoutMessage); 188 | }, 189 | 190 | /** 191 | * Waits until a HTML attribute no longer exists 192 | * @param {string} elementSelector - HTML element CSS selector 193 | * @param {string} attributeName - name of the attribute to inspect 194 | * @param {integer} waitInMilliseconds - number of milliseconds to wait for page to load 195 | * @returns {Promise} resolves if attribute is removed within timeout, otherwise rejects 196 | * @example 197 | * helpers.waitUntilAttributeDoesNotExists('html', 'data-busy', 5000); 198 | */ 199 | waitUntilAttributeDoesNotExists: function(elementSelector, attributeName, waitInMilliseconds) { 200 | 201 | // use either passed in timeout or global default 202 | var timeout = waitInMilliseconds || DEFAULT_TIMEOUT; 203 | 204 | // readable error message 205 | var timeoutMessage = attributeName + ' still exists after ' + waitInMilliseconds + ' milliseconds'; 206 | 207 | // repeatedly execute the test until it's true or we timeout 208 | return driver.wait(function() { 209 | 210 | // get the html attribute value using helper method 211 | return helpers.getAttributeValue(elementSelector, attributeName).then(function(value) { 212 | 213 | // attribute exists if value is not null 214 | return value === null; 215 | }); 216 | 217 | }, timeout, timeoutMessage); 218 | }, 219 | 220 | /** 221 | * Waits until an css element exists and returns it 222 | * @param {string} elementSelector - HTML element CSS selector 223 | * @param {integer} waitInMilliseconds - (optional) number of milliseconds to wait for the element 224 | * @returns {Promise} a promisse that will resolve if the element is found within timeout 225 | * @example 226 | * helpers.waitForCssXpathElement('#login-button', 5000); 227 | */ 228 | waitForCssXpathElement: function (elementSelector, waitInMilliseconds){ 229 | // use either passed in timeout or global default 230 | var timeout = waitInMilliseconds || DEFAULT_TIMEOUT; 231 | 232 | // if the locator starts with '//' assume xpath, otherwise css 233 | var selector = (localizador.indexOf('//') === 0) ? "xpath" : "css"; 234 | 235 | // readable error message 236 | var timeoutMessage = attributeName + ' still exists after ' + waitInMilliseconds + ' milliseconds'; 237 | 238 | // wait until the element exists 239 | return driver.wait(selenium.until.elementLocated({ [selector]: elementSelector }), timeout, timeoutMessage); 240 | }, 241 | 242 | /** 243 | * Scroll until element is visible 244 | * @param {WebElement} elemento - selenium web element 245 | * @returns {Promise} a promise that will resolve to the scripts return value. 246 | * @example 247 | * helpers.scrollToElement(webElement); 248 | */ 249 | scrollToElement: function (element) { 250 | return driver.executeScript('return arguments[0].scrollIntoView(false);', element); 251 | }, 252 | 253 | /** 254 | * Select a value inside a dropdown list by its text 255 | * @param {string} elementSelector - css or xpath selector 256 | * @param {string} optionName - name of the option to be chosen 257 | * @param {Promise} a promise that will resolve when the click command has completed 258 | * @example 259 | * helpers.selectByVisibleText('#country', 'Brazil'); 260 | */ 261 | selectDropdownValueByVisibleText: async function (elementSelector, optionName) { 262 | var select = await helpers.waitForCssXpathElement(elementSelector); 263 | var selectElements = await select.findElements({ css: 'option' }); 264 | var options = []; 265 | 266 | for (var option of selectElements) { 267 | options.push((await option.getText()).toUpperCase()); 268 | } 269 | optionName = optionName.toUpperCase(); 270 | 271 | return selectElements[options.indexOf(optionName)].click(); 272 | }, 273 | 274 | /** 275 | * Awaits and returns an array of all windows opened 276 | * @param {integer} waitInMilliseconds - (optional) number of milliseconds to wait for the result 277 | * @returns {Promise} a promise that will resolve with an array of window handles. 278 | * @example 279 | * helpers.waitForNewWindows(); 280 | */ 281 | waitForNewWindows: async function (waitInMilliseconds) { 282 | // use either passed in timeout or global default 283 | var timeout = waitInMilliseconds || DEFAULT_TIMEOUT; 284 | 285 | var windows = []; 286 | for (var i = 0; i < timeout; i += 1000) { 287 | windows = await driver.getAllWindowHandles(); // procura por todas as windows abertas 288 | if (windows.length > 1) return windows; 289 | 290 | await driver.sleep(1000); 291 | } 292 | }, 293 | 294 | /** 295 | * Get the content value of a :before pseudo element 296 | * @param {string} cssSelector - css selector of element to inspect 297 | * @returns {Promise} executes .then with value 298 | * @example 299 | * helpers.getPseudoElementBeforeValue('body header').then(function(value) { 300 | * console.log(value); 301 | * }); 302 | */ 303 | getPseudoElementBeforeValue: function(cssSelector) { 304 | 305 | function getBeforeContentValue(qs) { 306 | 307 | var el = document.querySelector(qs); 308 | var styles = el ? window.getComputedStyle(el, ':before') : null; 309 | 310 | return styles ? styles.getPropertyValue('content') : ''; 311 | } 312 | 313 | return driver.executeScript(getBeforeContentValue, cssSelector); 314 | }, 315 | 316 | /** 317 | * Get the content value of a :after pseudo element 318 | * @param {string} cssSelector - css selector of element to inspect 319 | * @returns {Promise} executes .then with value 320 | * @example 321 | * helpers.getPseudoElementAfterValue('body header').then(function(value) { 322 | * console.log(value); 323 | * }); 324 | */ 325 | getPseudoElementAfterValue: function(cssSelector) { 326 | 327 | function getAfterContentValue(qs) { 328 | 329 | var el = document.querySelector(qs); 330 | var styles = el ? window.getComputedStyle(el, ':after') : null; 331 | 332 | return styles ? styles.getPropertyValue('content') : ''; 333 | } 334 | 335 | return driver.executeScript(getAfterContentValue, cssSelector); 336 | }, 337 | 338 | clearCookies: function() { 339 | return driver.manage().deleteAllCookies(); 340 | }, 341 | 342 | clearStorages: function() { 343 | return driver.executeScript('window.localStorage.clear(); window.sessionStorage.clear();') 344 | }, 345 | 346 | clearCookiesAndStorages: function() { 347 | return helpers.clearCookies().then(helpers.clearStorages()); 348 | } 349 | }; 350 | -------------------------------------------------------------------------------- /runtime/phantomDriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var phantomjs = require('phantomjs-prebuilt'); 4 | var selenium = require('selenium-webdriver'); 5 | 6 | /** 7 | * Creates a Selenium WebDriver using PhantomJS as the browser 8 | * @returns {ThenableWebDriver} selenium web driver 9 | */ 10 | module.exports = function() { 11 | 12 | var driver = new selenium.Builder().withCapabilities({ 13 | browserName: 'phantomjs', 14 | javascriptEnabled: true, 15 | acceptSslCerts: true, 16 | 'phantomjs.binary.path': phantomjs.path, 17 | 'phantomjs.cli.args': '--ignore-ssl-errors=true' 18 | }).build(); 19 | 20 | driver.manage().window().maximize(); 21 | 22 | return driver; 23 | }; 24 | -------------------------------------------------------------------------------- /runtime/world.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * world.js is loaded by the cucumber framework before loading the step definitions and feature files 5 | * it is responsible for setting up and exposing the driver/browser/expect/assert etc required within each step definition 6 | */ 7 | 8 | var fs = require('fs-plus'); 9 | var path = require('path'); 10 | var requireDir = require('require-dir'); 11 | var merge = require('merge'); 12 | var chalk = require('chalk'); 13 | var selenium = require('selenium-webdriver'); 14 | var expect = require('chai').expect; 15 | var assert = require('chai').assert; 16 | var reporter = require('cucumber-html-reporter'); 17 | var cucumberJunit = require('cucumber-junit'); 18 | 19 | // Initialize the eyes SDK and set your private API key. 20 | var Eyes = require('eyes.selenium').Eyes; 21 | 22 | // drivers 23 | var FireFoxDriver = require('./firefoxDriver.js'); 24 | var PhantomJSDriver = require('./phantomDriver.js'); 25 | var ElectronDriver = require('./electronDriver.js'); 26 | var ChromeDriver = require('./chromeDriver'); 27 | 28 | /** 29 | * create the selenium browser based on global var set in index.js 30 | * @returns {ThenableWebDriver} selenium web driver 31 | */ 32 | function getDriverInstance() { 33 | 34 | var driver; 35 | 36 | switch (browserName || '') { 37 | 38 | case 'firefox': { 39 | driver = new FireFoxDriver(); 40 | } 41 | break; 42 | 43 | case 'phantomjs': { 44 | driver = new PhantomJSDriver(); 45 | } 46 | break; 47 | 48 | case 'electron': { 49 | driver = new ElectronDriver(); 50 | } 51 | break; 52 | 53 | case 'chrome': { 54 | driver = new ChromeDriver(); 55 | } 56 | break; 57 | 58 | // try to load from file 59 | default: { 60 | var driverFileName = path.resolve(process.cwd(), browserName); 61 | 62 | if (!fs.isFileSync(driverFileName)) { 63 | throw new Error('Could not find driver file: ' + driverFileName); 64 | } 65 | 66 | driver = require(driverFileName)(); 67 | } 68 | } 69 | 70 | return driver; 71 | } 72 | 73 | 74 | /** 75 | * Initialize the eyes SDK and set your private API key via the config file. 76 | */ 77 | function getEyesInstance() { 78 | 79 | if (global.eyesKey) { 80 | 81 | var eyes = new Eyes(); 82 | 83 | // retrieve eyes api key from config file in the project root as defined by the user 84 | eyes.setApiKey(global.eyesKey); 85 | 86 | return eyes; 87 | } 88 | 89 | return null; 90 | } 91 | 92 | function consoleInfo() { 93 | var args = [].slice.call(arguments), 94 | output = chalk.bgBlue.white('\n>>>>> \n' + args + '\n<<<<<\n'); 95 | 96 | console.log(output); 97 | } 98 | 99 | /** 100 | * Creates a list of variables to expose globally and therefore accessible within each step definition 101 | * @returns {void} 102 | */ 103 | function createWorld() { 104 | 105 | var runtime = { 106 | driver: null, // the browser object 107 | eyes: null, 108 | selenium: selenium, // the raw nodejs selenium driver 109 | By: selenium.By, // in keeping with Java expose selenium By 110 | by: selenium.By, // provide a javascript lowercase version 111 | until: selenium.until, // provide easy access to selenium until methods 112 | expect: expect, // expose chai expect to allow variable testing 113 | assert: assert, // expose chai assert to allow variable testing 114 | trace: consoleInfo, // expose an info method to log output to the console in a readable/visible format 115 | page: global.page || {}, // empty page objects placeholder 116 | shared: global.shared || {} // empty shared objects placeholder 117 | }; 118 | 119 | // expose properties to step definition methods via global variables 120 | Object.keys(runtime).forEach(function (key) { 121 | if (key === 'driver' && browserTeardownStrategy !== 'always') { 122 | return; 123 | } 124 | 125 | // make property/method available as a global (no this. prefix required) 126 | global[key] = runtime[key]; 127 | }); 128 | } 129 | 130 | /** 131 | * Import shared objects, pages object and helpers into global scope 132 | * @returns {void} 133 | */ 134 | function importSupportObjects() { 135 | 136 | // import shared objects from multiple paths (after global vars have been created) 137 | if (global.sharedObjectPaths && Array.isArray(global.sharedObjectPaths) && global.sharedObjectPaths.length > 0) { 138 | 139 | var allDirs = {}; 140 | 141 | // first require directories into objects by directory 142 | global.sharedObjectPaths.forEach(function (itemPath) { 143 | 144 | if (fs.existsSync(itemPath)) { 145 | 146 | var dir = requireDir(itemPath, { camelcase: true, recurse: true }); 147 | 148 | merge(allDirs, dir); 149 | } 150 | }); 151 | 152 | // if we managed to import some directories, expose them 153 | if (Object.keys(allDirs).length > 0) { 154 | 155 | // expose globally 156 | global.shared = allDirs; 157 | } 158 | } 159 | 160 | // import page objects (after global vars have been created) 161 | if (global.pageObjectPath && fs.existsSync(global.pageObjectPath)) { 162 | 163 | // require all page objects using camel case as object names 164 | global.page = requireDir(global.pageObjectPath, { camelcase: true, recurse: true }); 165 | } 166 | 167 | // add helpers 168 | global.helpers = require('../runtime/helpers.js'); 169 | } 170 | 171 | function closeBrowser() { 172 | // firefox quits on driver.close on the last window 173 | return driver.close().then(function () { 174 | if (browserName !== 'firefox'){ 175 | return driver.quit(); 176 | } 177 | }); 178 | } 179 | 180 | function teardownBrowser() { 181 | switch (browserTeardownStrategy) { 182 | case 'none': 183 | return Promise.resolve(); 184 | case 'clear': 185 | return helpers.clearCookiesAndStorages(); 186 | default: 187 | return closeBrowser(driver); 188 | } 189 | } 190 | 191 | // export the "World" required by cucumber to allow it to expose methods within step def's 192 | module.exports = function () { 193 | 194 | createWorld(); 195 | importSupportObjects(); 196 | 197 | // this.World must be set! 198 | this.World = createWorld; 199 | 200 | // set the default timeout for all tests 201 | this.setDefaultTimeout(global.DEFAULT_TIMEOUT); 202 | 203 | // create the driver and applitools eyes before scenario if it's not instantiated 204 | this.registerHandler('BeforeScenario', function (scenario) { 205 | 206 | if (!global.driver) { 207 | global.driver = getDriverInstance(); 208 | } 209 | 210 | if (!global.eyes) { 211 | global.eyes = getEyesInstance(); 212 | } 213 | }); 214 | 215 | this.registerHandler('AfterFeatures', function (features, done) { 216 | 217 | var cucumberReportPath = path.resolve(global.reportsPath, 'cucumber-report.json'); 218 | 219 | if (global.reportsPath && fs.existsSync(global.reportsPath)) { 220 | 221 | // generate the HTML report 222 | var reportOptions = { 223 | theme: 'bootstrap', 224 | jsonFile: cucumberReportPath, 225 | output: path.resolve(global.reportsPath, 'cucumber-report.html'), 226 | reportSuiteAsScenarios: true, 227 | launchReport: (!global.disableLaunchReport), 228 | ignoreBadJsonFile: true 229 | }; 230 | 231 | reporter.generate(reportOptions); 232 | 233 | // grab the file data 234 | var reportRaw = fs.readFileSync(cucumberReportPath).toString().trim(); 235 | var xmlReport = cucumberJunit(reportRaw); 236 | var junitOutputPath = path.resolve(global.junitPath, 'junit-report.xml'); 237 | 238 | fs.writeFileSync(junitOutputPath, xmlReport); 239 | } 240 | 241 | if (browserTeardownStrategy !== 'always') { 242 | closeBrowser().then(() => done()); 243 | } 244 | else { 245 | new Promise((resolve) => resolve(done())); 246 | } 247 | }); 248 | 249 | // executed after each scenario (always closes the browser to ensure fresh tests) 250 | this.After(function (scenario) { 251 | if (scenario.isFailed() && !global.noScreenshot) { 252 | // add a screenshot to the error report 253 | return driver.takeScreenshot().then(function (screenShot) { 254 | 255 | scenario.attach(new Buffer(screenShot, 'base64'), 'image/png'); 256 | 257 | return teardownBrowser().then(function() { 258 | if (eyes) { 259 | // If the test was aborted before eyes.close was called ends the test as aborted. 260 | return eyes.abortIfNotClosed(); 261 | } 262 | }); 263 | }); 264 | } 265 | return teardownBrowser(); 266 | }); 267 | }; 268 | -------------------------------------------------------------------------------- /shared-objects/test-data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | username: "import-test-user", 3 | password: "import-test-pa**word" 4 | }; -------------------------------------------------------------------------------- /shippable.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6.9.0 5 | 6 | addons: 7 | firefox: "57.0" 8 | 9 | env: 10 | global: 11 | - CHROME_VERSION=google-chrome-stable 12 | - DBUS_SESSION_BUS_ADDRESS=/dev/null 13 | - NODE_ENV=test 14 | 15 | build: 16 | pre_ci: 17 | - sudo apt-get update 18 | # - sudo apt-get install curl 19 | # install chrome browser required for selenium tests 20 | # - curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - 21 | # - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list 22 | # - sudo apt-get update -qqy 23 | # - sudo apt-get -qqy install ${CHROME_VERSION:-google-chrome-stable} 24 | # install ruby required to install cf cli 25 | # - sudo apt-get --assume-yes install ruby 26 | # - sudo gem install bundler 27 | # - curl -O https://s3.amazonaws.com/go-cli/releases/v6.17.1/cf-cli-installer_6.17.1_x86-64.deb 28 | # - dpkg -i cf-cli-installer_6.17.1_x86-64.deb 29 | 30 | pre_ci_boot: 31 | options: "--privileged=false --net=bridge" 32 | 33 | ci: 34 | - sudo apt-get install xvfb 35 | - export DISPLAY=:99.0 36 | - mkdir -p shippable/buildoutput 37 | - shippable_retry npm install 38 | - shippable_retry xvfb-run --server-args="-ac" node index.js -b firefox -t @search -x 30000 -------------------------------------------------------------------------------- /step-definitions/buy-workwear-steps.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | this.Given(/^I am on the Mammoth Workwear home page$/, function () { 4 | 5 | // load google 6 | return helpers.loadPage(page.mammothWorkwear.url); 7 | }); 8 | 9 | this.When(/^I click navigation item "([^"]*)"$/, function (linkTitle) { 10 | 11 | // click an item in the search results via the google page object 12 | return page.mammothWorkwear.clickNavigationItem(linkTitle); 13 | }); 14 | 15 | this.Then(/^I click product item "([^"]*)"$/, function (productTitle) { 16 | 17 | // click an item in the search results via the google page object 18 | return page.mammothWorkwear.clickProductItem(productTitle); 19 | }); 20 | 21 | this.Then(/^I should see product detail with title "([^"]*)"$/, function (pageTitle) { 22 | 23 | return page.mammothWorkwear.titleContains(pageTitle); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /step-definitions/google-search-steps.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | this.When(/^I search Google for "([^"]*)"$/, function (searchQuery) { 4 | 5 | return helpers.loadPage('http://www.google.com').then(function() { 6 | 7 | // use a method on the page object which also returns a promise 8 | return page.googleSearch.preformSearch(searchQuery); 9 | }); 10 | }); 11 | 12 | this.Then(/^I should see "([^"]*)" in the results$/, function (keywords) { 13 | 14 | // resolves if an item on the page contains text 15 | return driver.wait(until.elementsLocated(by.partialLinkText(keywords)), 10000); 16 | }); 17 | 18 | this.Then(/^I should see some results$/, function () { 19 | 20 | // driver wait returns a promise so return that 21 | return driver.wait(until.elementsLocated(by.css('div.g')), 10000).then(function() { 22 | 23 | // return the promise of an element to the following then. 24 | return driver.findElements(by.css('div.g')); 25 | }) 26 | .then(function (elements) { 27 | 28 | // verify this element has children 29 | expect(elements.length).to.not.equal(0); 30 | }); 31 | }); 32 | }; 33 | --------------------------------------------------------------------------------