├── .gitignore ├── LICENSE ├── README.md ├── features ├── github.feature ├── google.feature ├── step_definitions │ ├── github.js │ └── google.js └── support │ ├── errorHandling.js │ ├── hooks.js │ ├── pages │ └── github-page.js │ ├── testControllerHolder.js │ └── world.js ├── package.json └── reports └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Screenshots 5 | reports/screenshots 6 | 7 | # Added empty reports folder 8 | reports/* 9 | !reports/.gitkeep 10 | 11 | #VSCode files 12 | .vscode/* 13 | 14 | #Linter 15 | .eslintrc.json 16 | 17 | #Package-lock 18 | package-lock.json 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ryan Quellhorst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integration of TestCafe and CucumberJS 2 | 3 | This is a demonstration of integration [TestCafé](https://github.com/DevExpress/testcafe) into [CucumberJS](https://github.com/cucumber/cucumber-js) tests using TestCafe and Cucumber. 4 | 5 | Big thank you to [helen-dikareva](https://github.com/helen-dikareva/) for your help in starting the integration with your [repo](https://github.com/helen-dikareva/testcafe-cucumber-demo). This is a fork of all of the hard work you've put in. 6 | 7 | Also, thanks to the team at [TestCafé](https://github.com/DevExpress/testcafe) for allowing testers to break away from Selenium. 8 | 9 | **Depreciation Notice** - [There are talks to officially support the Gherkin syntax in TestCafé](https://github.com/DevExpress/testcafe/issues/1373#issuecomment-291526857). Once those changes are in place I will no longer support this repo. Please voice your support of these changes becoming native to TestCafé. 10 | 11 | ## Versions 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
TestCafé1.1.0
CucumberJS5.1.0
22 | 23 | ## Installation 24 | 25 | 1. Make sure [Node.js](https://nodejs.org/) is installed 26 | 2. Navigate to the root of the repo 27 | 3. Use the `npm install` command 28 | 29 | ## Running tests 30 | 31 | ### Windows 32 | You can run tests by executing the `.\node_modules\.bin\cucumber-js.cmd` or `npm test` commands in command prompt 33 | 34 | ### Mac or Linux 35 | You can run tests by executing `node_modules/cucumber/bin/cucumber-js` 36 | 37 | ## Documentation 38 | * [Initial Setup](https://github.com/rquellh/testcafe-cucumber/wiki/Initial-Setup) 39 | * [Debuging in VSCode](https://github.com/rquellh/testcafe-cucumber/wiki/Debugging-in-VSCode) 40 | * [Using TestCafé](https://github.com/rquellh/testcafe-cucumber/wiki/Using-TestCafe) 41 | * [Creating your first test](https://github.com/rquellh/testcafe-cucumber/wiki/Creating-your-first-test) 42 | * [Selectors](https://github.com/rquellh/testcafe-cucumber/wiki/Selectors) 43 | * [Actions](https://github.com/rquellh/testcafe-cucumber/wiki/Actions) 44 | * [Assertions](https://github.com/rquellh/testcafe-cucumber/wiki/Assertions) 45 | * [TestCafé & CucumberJS](https://github.com/rquellh/testcafe-cucumber/wiki/TestCafe-&-CucumberJS) 46 | * [Helpful VSCode Setup](https://github.com/rquellh/testcafe-cucumber/wiki/Helpful-VSCode-Setup) 47 | * [Creating Features](https://github.com/rquellh/testcafe-cucumber/wiki/Creating-Features) 48 | * [Creating Step Definitions](https://github.com/rquellh/testcafe-cucumber/wiki/Creating-Step-Definitions) 49 | * [Adding TestCafé Functionality to Cucumber steps](https://github.com/rquellh/testcafe-cucumber/wiki/Adding-TestCafe-Functionality-to-Cucumber-steps) 50 | * [Harnessing Cucumber's Power](https://github.com/rquellh/testcafe-cucumber/wiki/Harnessing-Cucumber's-Power) 51 | * [Page Object](https://github.com/rquellh/testcafe-cucumber/wiki/Page-Object) 52 | * [Running Tests](https://github.com/rquellh/testcafe-cucumber/wiki/Running-Tests) 53 | * [Reporting and Taking Screenshots](https://github.com/rquellh/testcafe-cucumber/wiki/Reporting-and-Taking-Screenshots) 54 | 55 | ## Notes 56 | 57 | * As of the time I am writting this, there is only 1 passing test of 3. I decided to not make all of the tests passing, so you could see how failures are handled. 58 | 59 | * My solution closes the TestCafé browser between each scenario. I tried to keep it open between scenarios but had trouble with handling failures. If you find a solution, I'd like to know. 60 | 61 | * With TestCafé version 0.19.0, you no longer have to manually update stack-chain. Thank you to the TestCafé crew for making the integration much easier. 62 | 63 | ## Contributors 64 | Thanks to everyone who has contributed to this project over the last few years. 65 | 66 | [cmasekar](https://github.com/cmasekar) |[benkirbyten10](https://github.com/benkirbyten10) |[vvedachalam](https://github.com/vvedachalam) |[azzra](https://github.com/azzra) | 67 | :---: |:---: |:---: |:---: | 68 | -------------------------------------------------------------------------------- /features/github.feature: -------------------------------------------------------------------------------- 1 | Feature: Searching for TestCafe on GitHub 2 | 3 | I want to find TestCafe repository on GitHub 4 | 5 | Scenario: Searching for TestCafe on GitHub 6 | Given I open the GitHub page 7 | When I am typing my search request "TestCafe" on GitHub 8 | Then I am pressing enter key on GitHub 9 | Then I should see that the first GitHub's result is DevExpress/testcafe 10 | 11 | Scenario: Try to use TestCafe Role 12 | Given I open the GitHub page 13 | Then I am trying to use Role 14 | -------------------------------------------------------------------------------- /features/google.feature: -------------------------------------------------------------------------------- 1 | Feature: Searching for TestCafe by Google 2 | 3 | I want to find TestCafe repository by Google search 4 | 5 | Scenario: Searching for TestCafe by Google 6 | Given I am open Google's search page 7 | When I am typing my search request "github TestCafe" on Google 8 | Then I press the "enter" key on Google 9 | Then I should see that the first Google's result is "GitHub - DevExpress/testcafe:" 10 | 11 | 12 | Scenario: Failing scenario 13 | Given I am open Google's search page 14 | When I am typing my search request "github TestCafe" on Google 15 | Then I press the "enter" key on Google 16 | Then I should see that the first Google's result is "kittens" -------------------------------------------------------------------------------- /features/step_definitions/github.js: -------------------------------------------------------------------------------- 1 | const {Given, When, Then} = require('cucumber'); 2 | const Role = require('testcafe').Role; 3 | const githubPage = require('../support/pages/github-page'); 4 | 5 | Given(/^I open the GitHub page$/, async function() { 6 | await testController.navigateTo(githubPage.github.url()); 7 | }); 8 | 9 | When(/^I am typing my search request "([^"]*)" on GitHub$/, async function(text) { 10 | await testController.typeText(githubPage.github.searchButton(), text); 11 | }); 12 | 13 | Then(/^I am pressing (.*) key on GitHub$/, async function(text) { 14 | await testController.pressKey(text); 15 | }); 16 | 17 | Then(/^I should see that the first GitHub\'s result is (.*)$/, async function(text) { 18 | await testController.expect(githubPage.github.firstSearchResult().innerText).contains(text); 19 | }); 20 | 21 | const gitHubRoleForExample = Role(githubPage.github.url() + 'login', async function(t) { 22 | await t 23 | .click(githubPage.github.loginButton()) 24 | .expect(githubPage.github.loginErrorMessage().innerText).contains('Incorrect username or password.'); 25 | }); 26 | 27 | Then(/^I am trying to use (.*)$/, async function(text) { 28 | await testController.useRole(gitHubRoleForExample); 29 | }); 30 | -------------------------------------------------------------------------------- /features/step_definitions/google.js: -------------------------------------------------------------------------------- 1 | const {Given, When, Then} = require('cucumber'); 2 | const Selector = require('testcafe').Selector; 3 | 4 | Given('I am open Google\'s search page', async function() { 5 | await testController.navigateTo('https://google.com'); 6 | }); 7 | 8 | When('I am typing my search request {string} on Google', async function(text) { 9 | var input = Selector('.gLFyf').with({boundTestRun: testController}); 10 | await this.addScreenshotToReport(); 11 | await testController.typeText(input, text); 12 | }); 13 | 14 | Then('I press the {string} key on Google', async function(text) { 15 | await testController.pressKey(text); 16 | }); 17 | 18 | Then('I should see that the first Google\'s result is {string}', async function(text) { 19 | var firstLink = Selector('#rso').find('a').with({boundTestRun: testController}); 20 | await testController.expect(firstLink.innerText).contains(text); 21 | }); 22 | -------------------------------------------------------------------------------- /features/support/errorHandling.js: -------------------------------------------------------------------------------- 1 | const testcafe = require('testcafe'); 2 | const hooks = require('../support/hooks'); 3 | 4 | exports.addErrorToController = function() { 5 | testController.executionChain 6 | .catch(function(result) { 7 | const errAdapter = new testcafe.embeddingUtils.TestRunErrorFormattableAdapter(result, { 8 | testRunPhase: testController.testRun.phase, 9 | userAgent: testController.testRun.browserConnection.browserInfo.userAgent, 10 | }); 11 | return testController.testRun.errs.push(errAdapter); 12 | }); 13 | }; 14 | 15 | exports.ifErrorTakeScreenshot = function(resolvedTestController) { 16 | 17 | if (hooks.getIsTestCafeError() === true && testController.testRun.opts.takeScreenshotsOnFails === true) { 18 | if (process.argv.includes('--format') || process.argv.includes('-f') || process.argv.includes('--format-options')) { 19 | resolvedTestController.executionChain._state = "fulfilled" 20 | return resolvedTestController.takeScreenshot().then(function(path) { 21 | return hooks.getAttachScreenshotToReport(path); 22 | }); 23 | } else { 24 | return resolvedTestController.takeScreenshot(); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /features/support/hooks.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const createTestCafe = require('testcafe'); 3 | const testControllerHolder = require('../support/testControllerHolder'); 4 | const {AfterAll, setDefaultTimeout, Before, After, Status} = require('cucumber'); 5 | const errorHandling = require('../support/errorHandling'); 6 | const TIMEOUT = 20000; 7 | 8 | let isTestCafeError = false; 9 | let attachScreenshotToReport = null; 10 | let cafeRunner = null; 11 | let n = 0; 12 | 13 | function createTestFile() { 14 | fs.writeFileSync('test.js', 15 | 'import errorHandling from "./features/support/errorHandling.js";\n' + 16 | 'import testControllerHolder from "./features/support/testControllerHolder.js";\n\n' + 17 | 18 | 'fixture("fixture")\n' + 19 | 20 | 'test\n' + 21 | '("test", testControllerHolder.capture)') 22 | } 23 | 24 | function runTest(iteration, browser) { 25 | createTestCafe('localhost', 1338 + iteration, 1339 + iteration) 26 | .then(function(tc) { 27 | cafeRunner = tc; 28 | const runner = tc.createRunner(); 29 | return runner 30 | .src('./test.js') 31 | .screenshots('reports/screenshots/', true) 32 | .browsers(browser) 33 | .run() 34 | .catch(function(error) { 35 | console.error(error); 36 | }); 37 | }) 38 | .then(function(report) { 39 | }); 40 | } 41 | 42 | 43 | setDefaultTimeout(TIMEOUT); 44 | 45 | Before(function() { 46 | runTest(n, this.setBrowser()); 47 | createTestFile(); 48 | n += 2; 49 | return this.waitForTestController.then(function(testController) { 50 | return testController.maximizeWindow(); 51 | }); 52 | }); 53 | 54 | After(function() { 55 | fs.unlinkSync('test.js'); 56 | testControllerHolder.free(); 57 | }); 58 | 59 | After(async function(testCase) { 60 | const world = this; 61 | if (testCase.result.status === Status.FAILED) { 62 | isTestCafeError = true; 63 | attachScreenshotToReport = world.attachScreenshotToReport; 64 | errorHandling.addErrorToController(); 65 | await errorHandling.ifErrorTakeScreenshot(testController) 66 | } 67 | }); 68 | 69 | AfterAll(function() { 70 | let intervalId = null; 71 | 72 | function waitForTestCafe() { 73 | intervalId = setInterval(checkLastResponse, 500); 74 | } 75 | 76 | function checkLastResponse() { 77 | if (testController.testRun.lastDriverStatusResponse === 'test-done-confirmation') { 78 | cafeRunner.close(); 79 | process.exit(); 80 | clearInterval(intervalId); 81 | } 82 | } 83 | 84 | waitForTestCafe(); 85 | }); 86 | 87 | const getIsTestCafeError = function() { 88 | return isTestCafeError; 89 | }; 90 | 91 | const getAttachScreenshotToReport = function(path) { 92 | return attachScreenshotToReport(path); 93 | }; 94 | 95 | exports.getIsTestCafeError = getIsTestCafeError; 96 | exports.getAttachScreenshotToReport = getAttachScreenshotToReport; 97 | -------------------------------------------------------------------------------- /features/support/pages/github-page.js: -------------------------------------------------------------------------------- 1 | const {Selector} = require('testcafe'); 2 | 3 | // Selectors 4 | 5 | function select(selector) { 6 | return Selector(selector).with({boundTestRun: testController}); 7 | } 8 | 9 | exports.github = { 10 | url: function() { 11 | return 'https://github.com/'; 12 | }, 13 | searchBox: function() { 14 | return select('.header-search-input'); 15 | }, 16 | firstSearchResult: function() { 17 | return Selector('.repo-list-item').nth(0).with({boundTestRun: testController}); 18 | }, 19 | loginButton: function() { 20 | return select('.btn.btn-primary.btn-block'); 21 | }, 22 | loginErrorMessage: function() { 23 | return select('#js-flash-container > div > div'); 24 | }, 25 | searchButton: function() { 26 | return select('.header-search-input'); 27 | }, 28 | firstSearchResult: function() { 29 | return Selector('.repo-list-item').nth(0).with({boundTestRun: testController}); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /features/support/testControllerHolder.js: -------------------------------------------------------------------------------- 1 | const testControllerHolder = { 2 | 3 | testController: null, 4 | captureResolver: null, 5 | getResolver: null, 6 | 7 | capture: function(t) { 8 | testControllerHolder.testController = t; 9 | 10 | if (testControllerHolder.getResolver) { 11 | testControllerHolder.getResolver(t); 12 | } 13 | 14 | return new Promise(function(resolve) { 15 | testControllerHolder.captureResolver = resolve; 16 | }); 17 | }, 18 | 19 | free: function() { 20 | testControllerHolder.testController = null; 21 | 22 | if (testControllerHolder.captureResolver) { 23 | testControllerHolder.captureResolver(); 24 | } 25 | }, 26 | 27 | get: function() { 28 | return new Promise(function(resolve) { 29 | if (testControllerHolder.testController) { 30 | resolve(testControllerHolder.testController); 31 | } else { 32 | testControllerHolder.getResolver = resolve; 33 | } 34 | }); 35 | }, 36 | }; 37 | 38 | module.exports = testControllerHolder; 39 | -------------------------------------------------------------------------------- /features/support/world.js: -------------------------------------------------------------------------------- 1 | const {setWorldConstructor} = require('cucumber'); 2 | const testControllerHolder = require('./testControllerHolder'); 3 | const base64Img = require('base64-img'); 4 | 5 | function CustomWorld({attach, parameters}) { 6 | 7 | this.waitForTestController = testControllerHolder.get() 8 | .then(function(tc) { 9 | return testController = tc; 10 | }); 11 | 12 | this.attach = attach; 13 | 14 | this.setBrowser = function() { 15 | if (parameters.browser === undefined) { 16 | return 'chrome'; 17 | } else { 18 | return parameters.browser; 19 | } 20 | }; 21 | 22 | this.addScreenshotToReport = function() { 23 | if (process.argv.includes('--format') || process.argv.includes('-f') || process.argv.includes('--format-options')) { 24 | testController.takeScreenshot() 25 | .then(function(screenshotPath) { 26 | const imgInBase64 = base64Img.base64Sync(screenshotPath); 27 | const imageConvertForCuc = imgInBase64.substring(imgInBase64.indexOf(',') + 1); 28 | return attach(imageConvertForCuc, 'image/png'); 29 | }) 30 | .catch(function(error) { 31 | console.warn('The screenshot was not attached to the report'); 32 | }); 33 | } else { 34 | return new Promise((resolve) => { 35 | resolve(null); 36 | }); 37 | } 38 | }; 39 | 40 | this.attachScreenshotToReport = function(pathToScreenshot) { 41 | const imgInBase64 = base64Img.base64Sync(pathToScreenshot); 42 | const imageConvertForCuc = imgInBase64.substring(imgInBase64.indexOf(',') + 1); 43 | return attach(imageConvertForCuc, 'image/png'); 44 | }; 45 | } 46 | 47 | setWorldConstructor(CustomWorld); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcafe-cucumber", 3 | "version": "0.1.0", 4 | "description": "An integration of TestCafe and CucumberJS", 5 | "author": "Ryan Quellhorst ", 6 | "contributors": [ 7 | "Chirag Masekar" 8 | ], 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rquellh/testcafe-cucumber" 13 | }, 14 | "scripts": { 15 | "debug": "node --inspect=1337 --debug-brk --nolazy node_modules/cucumber/bin/cucumber-js --tags @debug --format json:./reports/report.json", 16 | "test": "./node_modules/.bin/cucumber-js.cmd", 17 | "test-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json", 18 | "test-chrome": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"chrome\\\"}\"", 19 | "test-ie": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"ie\\\"}\"", 20 | "test-edge": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"edge\\\"}\"", 21 | "test-firefox": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"firefox\\\"}\"", 22 | "test-opera": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"opera\\\"}\"", 23 | "test-safari": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"safari\\\"}\"", 24 | "test-chrome-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"chrome\\\"}\"", 25 | "test-ie-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"ie\\\"}\"", 26 | "test-edge-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"edge\\\"}\"", 27 | "test-firefox-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"firefox\\\"}\"", 28 | "test-opera-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"opera\\\"}\"", 29 | "test-safari-report": "./node_modules/.bin/cucumber-js.cmd --format json:./reports/report.json --world-parameters \"{\\\"browser\\\": \\\"safari\\\"}\"", 30 | "test-chrome-headless": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"chrome:headless\\\"}\"", 31 | "test-firefox-headless": "./node_modules/.bin/cucumber-js.cmd --world-parameters \"{\\\"browser\\\": \\\"firefox:headless\\\"}\"" 32 | }, 33 | "dependencies": { 34 | "base64-img": "^1.0.4", 35 | "cucumber": "^5.1.0", 36 | "eslint": "^4.19.1", 37 | "npm": "^6.0.0", 38 | "testcafe": "^1.1.0" 39 | }, 40 | "devDependencies": { 41 | "eslint-config-google": "^0.9.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rquellh/testcafe-cucumber/11644e30aacf353ad13208f37cafe3e49d922f57/reports/.gitkeep --------------------------------------------------------------------------------