diff --git a/README.md b/README.md index 2a1a2a4..b198299 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Note that it requires good development skills as well as good knowledge and unde 1. Create a new folder in your project to host your automated tests. 2. Create a new NPM project by executing `npm init`. More information about the different options [here](https://docs.npmjs.com/getting-started/using-a-package.json). 3. Install EAT package by running the following command from : `npm install --save etaf`. It creates a `node_modules` directory containing all the dependencies needed to run the project. -4. From , run `npx etaf install` to generate the skeleton of your test project. +4. From , run `npx etaf install` to generate the skeleton of your test project. If you want to generate the skeleton of your project with some working samples, run: `npx etaf install --sample=true` and then run `npm install --no-optional` ### Behind a Proxy Set the configuration of your proxy by editing the `.npmrc` file in your home directory: diff --git a/bin/etaf.js b/bin/etaf.js index d92e849..f54b3c0 100755 --- a/bin/etaf.js +++ b/bin/etaf.js @@ -7,6 +7,7 @@ const gulpDel = require('del'); const gulpMocha = require('gulp-mocha'); const webdriver = require('gulp-webdriver'); const yargs = require('yargs'); +const replace = require('replace'); const wdioOptions = { cucumberOpts: {} }; let configFile; @@ -76,7 +77,7 @@ const runProject = config => { * Create project structure tree. * @return {Promise} */ -const installProject = async () => { +const installProject = async(config) => { console.log('Creating test project structure...'); const allPromises = [ @@ -104,6 +105,24 @@ const installProject = async () => { await fs.move('gitignore', '.gitignore'); await fs.move('npmrc', '.npmrc'); console.log('Test project structure successfully created.'); + + if (config.sample == 'true') { + console.log('Adding project sample...'); + await fs.copy('node_modules/etaf/sample', '.'); + replace({ + regex: "baseUrl: 'http://localhost:8080'", + replacement: "baseUrl: 'https://racodond.github.io/test-automation-website/'", + paths: ['wdio.conf.js', 'wdio.local.conf.js'], + silent: true, + }); + replace({ + regex: 'dependencies": {', + replacement: 'dependencies": {\n "faker": "4.1.0",', + paths: ['package.json'], + silent: true, + }); + console.log("Project sample successfully added.") + } } catch (err) { console.error(err); process.exit(1); @@ -119,7 +138,13 @@ yargs .command({ command: 'install', desc: 'Create project structure tree', - handler: () => installProject(), + handler: argv => installProject(argv), + builder: yargsObj => { + yargsObj.option('sample', { + describe: 'To populate the project structure with a sample project.', + default: 'false', + }); + }, }) .command({ command: 'generate-local-conf', diff --git a/lib/conf/wdio.conf.js b/lib/conf/wdio.conf.js index ef5564f..65345ac 100644 --- a/lib/conf/wdio.conf.js +++ b/lib/conf/wdio.conf.js @@ -2,6 +2,7 @@ const fs = require('fs-extra'); const path = require('path'); +const replace = require('replace'); const getAllFilesIn = function (dir, filelist) { let files = filelist || []; diff --git a/package.json b/package.json index 6bbe48c..1bc0d6c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "merge": "1.2.1", "mocha": "5.2.0", "path": "0.12.7", + "replace": "1.0.0", "sinon": "7.1.1", "sinon-chai": "3.2.0", "wdio-allure-reporter": "0.8.3", diff --git a/sample/conf/realm/all-docker-chrome.js b/sample/conf/realm/all-docker-chrome.js new file mode 100644 index 0000000..650fa7a --- /dev/null +++ b/sample/conf/realm/all-docker-chrome.js @@ -0,0 +1,22 @@ +'use strict'; + +const merge = require('merge'); + +let config = require('./part/all'); +config = merge.recursive(true, require('./part/docker'), config); +config = merge.recursive(true, require('./part/chrome'), config); +config = merge.recursive(true, + { + reporterOptions: { + json: { + outputDir: 'output-json/all-docker-chrome/', + filename: 'report', + combined: 'true', + }, + }, + host: 'chrome', + }, + config); +config = merge.recursive(true, require('../../wdio.conf').config, config); + +exports.config = config; diff --git a/sample/conf/realm/all-docker-firefox.js b/sample/conf/realm/all-docker-firefox.js new file mode 100644 index 0000000..07465c4 --- /dev/null +++ b/sample/conf/realm/all-docker-firefox.js @@ -0,0 +1,23 @@ +'use strict'; + +const merge = require('merge'); + +let config = require('./part/all'); +config = merge.recursive(true, require('./part/docker'), config); +config = merge.recursive(true, require('./part/firefox'), config); +config = merge.recursive( + true, + { + reporterOptions: { + json: { + outputDir: 'output-json/all-docker-firefox/', + filename: 'report', + combined: 'true', + }, + }, + host: 'firefox', + }, + config); +config = merge.recursive(true, require('../../wdio.conf').config, config); + +exports.config = config; diff --git a/sample/conf/realm/part/all.js b/sample/conf/realm/part/all.js new file mode 100644 index 0000000..2f6a6c6 --- /dev/null +++ b/sample/conf/realm/part/all.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + specs: [ + './src/features/**/*.feature', + ], +}; diff --git a/sample/conf/realm/part/chrome.js b/sample/conf/realm/part/chrome.js new file mode 100644 index 0000000..6bf2ebc --- /dev/null +++ b/sample/conf/realm/part/chrome.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = { + capabilities: [ + { + browserName: 'chrome', + chromeOptions: { + // args: ['--headless'], + prefs: { + profile: { + default_content_setting_values: { images: 2 }, // Do not load images for tests to run faster + password_manager_enabled: false, // Deactivate password manager + }, + credentials_enable_service: false, // Deactivate password manager + }, + }, + }, + ], + + // seleniumInstallArgs: { + // proxy: 'http://localhost:3128', + // drivers: { + // chrome: { + // version: '2.43', + // arch: process.arch, + // baseURL: 'https://chromedriver.storage.googleapis.com', + // }, + // }, + // }, + // seleniumArgs: { + // drivers: { + // chrome: { + // version: '2.43', + // arch: process.arch, + // }, + // }, + // }, + +}; diff --git a/sample/conf/realm/part/docker.js b/sample/conf/realm/part/docker.js new file mode 100644 index 0000000..f3a5ab0 --- /dev/null +++ b/sample/conf/realm/part/docker.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + services: [], +}; diff --git a/sample/conf/realm/part/firefox.js b/sample/conf/realm/part/firefox.js new file mode 100644 index 0000000..f1280b7 --- /dev/null +++ b/sample/conf/realm/part/firefox.js @@ -0,0 +1,33 @@ +'use strict'; + +module.exports = { + capabilities: [ + { + browserName: 'firefox', + acceptInsecureCerts: true, + // "moz:firefoxOptions": { + // args: ['-headless'], + // } + }, + ], + + // seleniumInstallArgs: { + // proxy: 'http://localhost:3128', + // drivers: { + // firefox: { + // version: '0.23.0', + // arch: process.arch, + // baseURL: 'https://github.com/mozilla/geckodriver/releases/download', + // }, + // }, + // }, + // seleniumArgs: { + // drivers: { + // firefox: { + // version: '0.23.0', + // arch: process.arch, + // }, + // }, + // }, + +}; diff --git a/sample/docker/docker-compose-sample-project.sh b/sample/docker/docker-compose-sample-project.sh new file mode 100755 index 0000000..eab311d --- /dev/null +++ b/sample/docker/docker-compose-sample-project.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +export RUN_AS_UID=$(id -u) + +docker-compose up --build --exit-code-from sample-project +docker-compose down diff --git a/sample/docker/docker-compose.yml b/sample/docker/docker-compose.yml new file mode 100644 index 0000000..3245daf --- /dev/null +++ b/sample/docker/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3' +services: + chrome: + image: selenium/standalone-chrome:3.14.0 + container_name: chrome + environment: + - JAVA_OPTS=-Dselenium.LOGGER.level=WARNING + firefox: + image: selenium/standalone-firefox:3.14.0 + container_name: firefox + environment: + - JAVA_OPTS=-Dselenium.LOGGER.level=WARNING + sample-project: + image: node:8 + container_name: sample-project + command: /data/docker/run-sample-project-docker.sh + volumes: + - ../.:/data + environment: + - RUN_AS_UID=${RUN_AS_UID} + depends_on: + - firefox + - chrome diff --git a/sample/docker/run-sample-project-docker-ft.sh b/sample/docker/run-sample-project-docker-ft.sh new file mode 100755 index 0000000..5589a70 --- /dev/null +++ b/sample/docker/run-sample-project-docker-ft.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +cd /data + +echo "Installing sample project dependencies..." +npm install +echo "Sample project dependencies installed." + +echo "Running end-to-end tests on all-docker-chrome realm..." +npx etaf run --realm='all-docker-chrome' +echo "End-to-end tests run on all-docker-chrome realm." + +echo "Running end-to-end tests on all-docker-firefox realm..." +npx etaf run --realm='all-docker-firefox' +echo "End-to-end tests run on all-docker-firefox realm." diff --git a/sample/docker/run-sample-project-docker.sh b/sample/docker/run-sample-project-docker.sh new file mode 100755 index 0000000..803d127 --- /dev/null +++ b/sample/docker/run-sample-project-docker.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +if id "testrunner" >/dev/null 2>&1; then + echo "User 'testrunner' already exists." +else + echo "Creating user 'testrunner'..." + useradd -u $RUN_AS_UID -o -m -p $(echo testrunner | openssl passwd -1 -stdin) testrunner + echo "User 'testrunner' created." +fi + +su testrunner -c '/data/docker/run-sample-project-docker-ft.sh' diff --git a/sample/src/features/navigation.feature b/sample/src/features/navigation.feature new file mode 100644 index 0000000..b748807 --- /dev/null +++ b/sample/src/features/navigation.feature @@ -0,0 +1,21 @@ +Feature: Navigate + + Scenario Outline: Navigate directly to URL + When I go to the page + Then I should be on the page + Examples: + | page | + | home | + | search | + | results | + | contact | + + @smoke + Scenario Outline: Navigate from the home page menu + When I am on the home page + When I go to the page from the home page menu + Then I should be on the page + Examples: + | page | + | search | + | contact | diff --git a/sample/src/features/search.feature b/sample/src/features/search.feature new file mode 100644 index 0000000..fc930a4 --- /dev/null +++ b/sample/src/features/search.feature @@ -0,0 +1,27 @@ +Feature: Search engine + + Background: + Given I am on the search page + + @smoke + Scenario: Search for something and land on the results page + When I search for something + Then I should be on the results page + + @smoke + Scenario: Search for something that exists and see some results + When I search for something that exists + Then I should be on the results page + And I should see some results + + @smoke + Scenario: Search for something that does not exist and see no results + When I search for something that does not exist + Then I should be on the results page + And I should be told that no results have been found + + @query-reminder + Scenario: Be reminded the search query on the results page + When I search for something + Then I should be on the results page + And I should be reminded what I searched for diff --git a/sample/src/step_definitions/navigate.step.js b/sample/src/step_definitions/navigate.step.js new file mode 100644 index 0000000..c16ecac --- /dev/null +++ b/sample/src/step_definitions/navigate.step.js @@ -0,0 +1,25 @@ +'use strict'; + +const { defineSupportCode } = require('cucumber'); + +defineSupportCode(function ({ Given, When, Then }) { + + Given(/^I am on the (.+) page$/, function (pageName) { + return browser + .goToPage(pageName) + .seePage(pageName); + }); + + When(/^I go to the (.+) page$/, function (pageName) { + return browser.goToPage(pageName); + }); + + When(/^I go to the (.+) page from the home page menu$/, function (pageName) { + return browser.goToPageFromHomePageMenu(pageName); + }); + + Then(/^I should be on the (.+) page$/, function (pageName) { + return browser.seePage(pageName); + }); + +}); diff --git a/sample/src/step_definitions/search.step.js b/sample/src/step_definitions/search.step.js new file mode 100644 index 0000000..964758d --- /dev/null +++ b/sample/src/step_definitions/search.step.js @@ -0,0 +1,39 @@ +'use strict'; + +require('src/support/business-object/search.bo'); + +const { defineSupportCode } = require('cucumber'); + +defineSupportCode(function ({ Before, Given, When, Then }) { + + let query = require('src/support/data/search/search-for-something.data'); + + Before({ tags: '@query-reminder' }, function() { + query = require('src/support/data/search/search-for-something-query-reminder.data'); + }); + + When(/^I search for something$/, function () { + return browser.search(query); + }); + + When(/^I search for something that does not exist$/, function () { + return browser.searchForSomethingThatDoesNotExist(); + }); + + When(/^I search for something that exists$/, function () { + return browser.searchForSomethingThatExists(); + }); + + Then(/^I should see some results$/, function () { + return browser.seeResults(); + }); + + Then(/^I should be told that no results have been found$/, function () { + return browser.seeNoResults(); + }); + + Then(/^I should be reminded what I searched for$/, function () { + return browser.seeSearchQuery(query); + }); + +}); diff --git a/sample/src/support/business-object/navigate.bo.js b/sample/src/support/business-object/navigate.bo.js new file mode 100644 index 0000000..ac2386b --- /dev/null +++ b/sample/src/support/business-object/navigate.bo.js @@ -0,0 +1,50 @@ +'use strict'; + +class Navigate { + + constructor(pages, homePage) { + this.pages = pages; + this.homePage = homePage; + } + + goToPage(pageName) { + return browser.url(browser.options.baseUrl + this.pages[pageName]['path']); + } + + goToPageFromHomePageMenu(pageName) { + return this.homePage.goToPageFromMenu(pageName); + } + + seePage(pageName) { + return browser.getTitle().should.eventually.equal(this.pages[pageName]['title']); + } +} + +const navigate = new Navigate( + require('src/support/configuration/pages'), + require('src/support/page-object/home.page'), +); + +browser.addCommand('goToPage', function(pageName) { + logger.info(`Go to the ${pageName} page`, { + file: __filename, + method: 'browser.goToPage', + }); + return navigate.goToPage(pageName); +}); + +browser.addCommand('goToPageFromHomePageMenu', function(pageName) { + logger.info(`Go to the ${pageName} page from the home page menu`, { + file: __filename, + method: 'browser.goToPageFromHomePageMenu', + }); + return navigate.goToPageFromHomePageMenu(pageName); +}); + +browser.addCommand('seePage', function(pageName) { + logger.info(`See ${pageName}`, { + file: __filename, + method: 'browser.seePage', + }); + return navigate.seePage(pageName); +}); diff --git a/sample/src/support/business-object/search.bo.js b/sample/src/support/business-object/search.bo.js new file mode 100644 index 0000000..dc2ebc9 --- /dev/null +++ b/sample/src/support/business-object/search.bo.js @@ -0,0 +1,86 @@ +'use strict'; + +class Search { + + constructor(searchPage, resultsPage) { + this.searchPage = searchPage; + this.resultsPage = resultsPage; + } + + search(data) { + return this.searchPage.search(data); + } + + searchForSomethingThatDoesNotExist() { + return this.search(require('src/support/data/search/search-for-something-that-does-not-exist.data')); + } + + searchForSomethingThatExists() { + return this.search(require('src/support/data/search/search-for-something-that-exists.data')); + } + + seeNoResults() { + return this.resultsPage.seeNoResults(); + } + + seeResults() { + return this.resultsPage.seeResults(); + } + + seeSearchQuery(query) { + return this.resultsPage.seeSearchQuery(query); + } +} + +const search = new Search( + require('src/support/page-object/search.page'), + require('src/support/page-object/results.page'), +); + +browser.addCommand('search', function (data) { + logger.info(`Search for ${data}`, { + file: __filename, + method: 'browser.search', + }); + return search.search(data); +}); + +browser.addCommand('searchForSomethingThatDoesNotExist', function () { + logger.info('Search for something that does not exist', { + file: __filename, + method: 'browser.searchForSomethingThatDoesNotExist', + }); + return search.searchForSomethingThatDoesNotExist(); +}); + +browser.addCommand('searchForSomethingThatExists', function () { + logger.info('Search for something that exists', { + file: __filename, + method: 'browser.searchForSomethingThatExists', + }); + return search.searchForSomethingThatExists(); +}); + +browser.addCommand('seeNoResults', function () { + logger.info('See no results', { + file: __filename, + method: 'browser.seeNoResults', + }); + return search.seeNoResults(); +}); + +browser.addCommand('seeResults', function () { + logger.info('See several results', { + file: __filename, + method: 'browser.seeResults', + }); + return search.seeResults(); +}); + +browser.addCommand('seeSearchQuery', function (query) { + logger.info(`See search query: ${query}`, { + file: __filename, + method: 'browser.seeSearchQuery', + }); + return search.seeSearchQuery(query); +}); diff --git a/sample/src/support/configuration/pages.js b/sample/src/support/configuration/pages.js new file mode 100644 index 0000000..e87c25c --- /dev/null +++ b/sample/src/support/configuration/pages.js @@ -0,0 +1,6 @@ +module.exports = { + contact: { path: 'contact.html', title: 'Contact' }, + home: { path: '', title: 'Home' }, + results: { path: 'results.html', title: 'Search Results' }, + search: { path: 'search.html', title: 'Search' }, +}; diff --git a/sample/src/support/data/search/search-for-something-query-reminder.data.js b/sample/src/support/data/search/search-for-something-query-reminder.data.js new file mode 100644 index 0000000..b121eb6 --- /dev/null +++ b/sample/src/support/data/search/search-for-something-query-reminder.data.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = 'query reminder'; diff --git a/sample/src/support/data/search/search-for-something-that-does-not-exist.data.js b/sample/src/support/data/search/search-for-something-that-does-not-exist.data.js new file mode 100644 index 0000000..2f9e80f --- /dev/null +++ b/sample/src/support/data/search/search-for-something-that-does-not-exist.data.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = 'blabla...'; diff --git a/sample/src/support/data/search/search-for-something-that-exists.data.js b/sample/src/support/data/search/search-for-something-that-exists.data.js new file mode 100644 index 0000000..5a49a2b --- /dev/null +++ b/sample/src/support/data/search/search-for-something-that-exists.data.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = 'happiness'; diff --git a/sample/src/support/data/search/search-for-something.data.js b/sample/src/support/data/search/search-for-something.data.js new file mode 100644 index 0000000..10bd376 --- /dev/null +++ b/sample/src/support/data/search/search-for-something.data.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('faker').lorem.sentence(); diff --git a/sample/src/support/page-object/home.page.js b/sample/src/support/page-object/home.page.js new file mode 100644 index 0000000..01288c9 --- /dev/null +++ b/sample/src/support/page-object/home.page.js @@ -0,0 +1,18 @@ +'use strict'; + +class HomePage { + + get pageElements() { + return { + search: '#search', + contact: '#contact', + }; + } + + goToPageFromMenu(pageName) { + return browser.click(this.pageElements[pageName]); + } + +} + +module.exports = new HomePage(); diff --git a/sample/src/support/page-object/results.page.js b/sample/src/support/page-object/results.page.js new file mode 100644 index 0000000..446fcec --- /dev/null +++ b/sample/src/support/page-object/results.page.js @@ -0,0 +1,29 @@ +'use strict'; + +class ResultsPage { + + get pageElements() { + return { + query: '#query', + noResults: '#no-results', + severalResults: '#many-results', + }; + } + + seeNoResults() { + return browser.waitForVisible(this.pageElements.noResults).should.eventually.be.true; + } + + seeResults() { + return browser.waitForVisible(this.pageElements.severalResults).should.eventually.be.true; + } + + seeSearchQuery(query) { + return browser + .waitForText(this.pageElements.query) + .getText(this.pageElements.query).should.eventually.equal(query); + } + +} + +module.exports = new ResultsPage(); diff --git a/sample/src/support/page-object/search.page.js b/sample/src/support/page-object/search.page.js new file mode 100644 index 0000000..0e46a71 --- /dev/null +++ b/sample/src/support/page-object/search.page.js @@ -0,0 +1,20 @@ +'use strict'; + +class SearchPage { + + get pageElements() { + return { + query: '#query', + searchButton: '#search-button', + }; + } + + search(data) { + return browser + .setValue(this.pageElements.query, data) + .click(this.pageElements.searchButton); + } + +} + +module.exports = new SearchPage(); diff --git a/sample/src/support/world.js b/sample/src/support/world.js new file mode 100644 index 0000000..d8d7861 --- /dev/null +++ b/sample/src/support/world.js @@ -0,0 +1,4 @@ +'use strict'; + +require('etaf/lib/world'); +require('src/support/business-object/navigate.bo'); \ No newline at end of file