├── part-1
├── .gitignore
├── globals.js
├── .babelrc
├── nightwatch.conf.js
├── pages
│ ├── instancesPage.js
│ └── loginPage.js
├── tests
│ └── testLogin.js
├── package.json
├── nightwatch.json
└── README.md
├── .gitignore
├── part-2
├── .gitignore
├── .babelrc
├── globals.js
├── nightwatch.conf.js
├── pages
│ ├── socketsPage.js
│ ├── instancesPage.js
│ └── loginPage.js
├── tests
│ ├── testLogin.js
│ └── testInstances.js
├── package.json
├── commands
│ └── clickListItemDropdown.js
├── nightwatch.json
└── README.md
├── part-3
├── .gitignore
├── .babelrc
├── globals.js
├── nightwatch.conf.js
├── pages
│ ├── socketsPage.js
│ ├── instancesPage.js
│ ├── loginPage.js
│ └── scriptEndpointsPage.js
├── scripts
│ ├── cleanUp.js
│ ├── saveVariables.js
│ ├── deleteInstance.js
│ ├── createInstance.js
│ ├── createScript.js
│ ├── createTestData.js
│ └── createConnection.js
├── commands
│ ├── fillInput.js
│ ├── clickElement.js
│ ├── selectDropdownValue.js
│ └── clickListItemDropdown.js
├── tests
│ ├── testLogin.js
│ ├── testInstances.js
│ └── testScriptEndpoint.js
├── package.json
├── nightwatch.json
└── README.md
└── README.md
/part-1/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | reports
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | reports
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/part-2/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | reports
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/part-1/globals.js:
--------------------------------------------------------------------------------
1 | export default {
2 | waitForConditionTimeout: 10000,
3 | };
4 |
--------------------------------------------------------------------------------
/part-3/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | reports
3 | npm-debug.log
4 | tempInstance.js
5 |
--------------------------------------------------------------------------------
/part-1/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": [
4 | "add-module-exports",
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/part-2/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": [
4 | "add-module-exports",
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/part-3/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": [
4 | "add-module-exports",
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/part-1/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 |
3 | module.exports = require('./nightwatch.json');
4 |
--------------------------------------------------------------------------------
/part-2/globals.js:
--------------------------------------------------------------------------------
1 | export default {
2 | waitForConditionTimeout: 10000,
3 | instanceName: INSTANCE_NAME
4 | };
5 |
--------------------------------------------------------------------------------
/part-2/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 |
3 | module.exports = require('./nightwatch.json');
4 |
--------------------------------------------------------------------------------
/part-3/globals.js:
--------------------------------------------------------------------------------
1 | export default {
2 | waitForConditionTimeout: 10000,
3 | instanceName: INSTANCE_NAME
4 | };
5 |
--------------------------------------------------------------------------------
/part-3/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 |
3 | module.exports = require('./nightwatch.json');
4 |
--------------------------------------------------------------------------------
/part-2/pages/socketsPage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | elements: {
3 | instancesDropdown: {
4 | selector: '.instances-dropdown'
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/part-3/pages/socketsPage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | elements: {
3 | instancesDropdown: {
4 | selector: '.instances-dropdown'
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/part-1/pages/instancesPage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | elements: {
3 | instancesListDescription: {
4 | selector: '//div[@class="description-field col-flex-1"]',
5 | locateStrategy: 'xpath'
6 | }
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/part-3/scripts/cleanUp.js:
--------------------------------------------------------------------------------
1 | import createConnection from './createConnection';
2 | import deleteInstance from './deleteInstance';
3 | import tempInstance from '../tempInstance';
4 |
5 | createConnection()
6 | .then((user) => deleteInstance(user, tempInstance.instanceName))
7 | .catch((error) => console.error('Cleanup error:\n', error.message));
8 |
--------------------------------------------------------------------------------
/part-3/commands/fillInput.js:
--------------------------------------------------------------------------------
1 | // Command that will clear value of given element
2 | // and then fill with target string.
3 | exports.command = function fillInput(element, string) {
4 | return this
5 | .waitForElementVisible(element)
6 | .clearValue(element)
7 | .pause(300)
8 | .setValue(element, string)
9 | .pause(1000);
10 | };
11 |
--------------------------------------------------------------------------------
/part-3/scripts/saveVariables.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 |
3 | const saveVariables = (data) => {
4 | const fileName = 'tempInstance.js';
5 | const variableFile = fs.createWriteStream(`./${fileName}`);
6 | const json = JSON.stringify(data);
7 |
8 | variableFile.write('export default ' + json + ';');
9 | console.log(`\n> File saved as ${fileName}`);
10 | };
11 |
12 | export default saveVariables;
13 |
--------------------------------------------------------------------------------
/part-3/commands/clickElement.js:
--------------------------------------------------------------------------------
1 | // Command that will wait for a given element to be visible, then
2 | // will move to it, click and pause for 1sec. Mostly used for buttons or any
3 | // other clickable parts of UI.
4 | exports.command = function clickElement(element) {
5 | return this
6 | .waitForElementPresent(element)
7 | .moveToElement(element, 0, 0)
8 | .click(element)
9 | .pause(1000);
10 | };
11 |
--------------------------------------------------------------------------------
/part-3/scripts/deleteInstance.js:
--------------------------------------------------------------------------------
1 | const deleteInstance = (user, instanceName) => {
2 | const instance = {
3 | name: instanceName
4 | };
5 | return user.connection.Instance
6 | .please()
7 | .delete(instance)
8 | .then(() => console.log(`${instanceName} was deleted.`))
9 | .catch((error) => console.error('Instance delete error:\n', error.message));
10 | };
11 |
12 | export default deleteInstance;
13 |
--------------------------------------------------------------------------------
/part-1/tests/testLogin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'User Logs in': (client) => {
3 | const loginPage = client.page.loginPage();
4 | const instancesPage = client.page.instancesPage();
5 |
6 | loginPage
7 | .navigate()
8 | .login(process.env.EMAIL, process.env.PASSWORD);
9 |
10 |
11 | instancesPage.expect.element('@instancesListDescription').to.be.visible;
12 |
13 | client.end();
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/part-2/tests/testLogin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'User Logs in': (client) => {
3 | const loginPage = client.page.loginPage();
4 | const instancesPage = client.page.instancesPage();
5 |
6 | loginPage
7 | .navigate()
8 | .login(process.env.EMAIL, process.env.PASSWORD);
9 |
10 | instancesPage.expect.element('@instancesListDescription').text.to.contain('Your first instance.');
11 |
12 | client.end();
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/part-3/tests/testLogin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'User Logs in': (client) => {
3 | const loginPage = client.page.loginPage();
4 | const instancesPage = client.page.instancesPage();
5 |
6 | loginPage
7 | .navigate()
8 | .login(process.env.EMAIL, process.env.PASSWORD);
9 |
10 | instancesPage.expect.element('@instancesListDescription').text.to.contain('Your first instance.');
11 |
12 | client.end();
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/part-3/scripts/createInstance.js:
--------------------------------------------------------------------------------
1 | const createInstance = (user) => {
2 | const name = 'testInstance' + Date.now();
3 | const instance = {
4 | name
5 | };
6 |
7 | return user.connection.Instance
8 | .please()
9 | .create(instance)
10 | .then(() => {
11 | user.instanceName = name;
12 | user.connection.setInstanceName(user.instanceName);
13 | return user;
14 | })
15 | .catch((error) => console.error('Instance error:\n', error.message));
16 | };
17 |
18 | export default createInstance;
19 |
--------------------------------------------------------------------------------
/part-3/commands/selectDropdownValue.js:
--------------------------------------------------------------------------------
1 | // Command that selects given dropdownValue from targeted element.
2 | exports.command = function selectDropdownValue(element, dropdownValue) {
3 | const value = `//iframe//following-sibling::div//div[text()="${dropdownValue}"]`;
4 |
5 | return this
6 | .waitForElementVisible(element)
7 | .moveToElement(element, 0, 0)
8 | .pause(500)
9 | .mouseButtonClick()
10 | .pause(500)
11 | .waitForElementVisible(value)
12 | .pause(500)
13 | .click(value)
14 | .pause(500);
15 | };
16 |
--------------------------------------------------------------------------------
/part-3/scripts/createScript.js:
--------------------------------------------------------------------------------
1 | const createScript = (user) => {
2 | const label = 'testScript' + Date.now();
3 | const scriptObject = {
4 | label,
5 | source: 'print "Hellow World!"',
6 | runtime_name: 'python_library_v5.0'
7 | };
8 |
9 | return user.connection.Script
10 | .please()
11 | .create(scriptObject)
12 | .then(() => {
13 | user.scriptName = label;
14 | return user;
15 | })
16 | .catch((error) => console.error('Script error:\n', error.message));
17 | };
18 |
19 | export default createScript;
20 |
--------------------------------------------------------------------------------
/part-3/scripts/createTestData.js:
--------------------------------------------------------------------------------
1 | import createConnection from './createConnection';
2 | import createInstance from './createInstance';
3 | import createScript from './createScript';
4 | import saveVariables from './saveVariables';
5 |
6 | createConnection()
7 | .then((user) => createInstance(user))
8 | .then((user) => createScript(user))
9 | .then((user) => {
10 | delete user.connection;
11 | console.log('Your test setup:\n', user);
12 | saveVariables(user);
13 | })
14 | .catch((error) => console.log('Global error:\n', error));
15 |
--------------------------------------------------------------------------------
/part-2/pages/instancesPage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | elements: {
3 | instancesListDescription: {
4 | selector: '//div[@class="description-field col-flex-1"]',
5 | locateStrategy: 'xpath'
6 | },
7 | instancesTable: {
8 | selector: 'div[id=instances]'
9 | },
10 | instanceDialogEditTitle: {
11 | selector: '//h3[text()="Update an Instance"]',
12 | locateStrategy: 'xpath'
13 | },
14 | instanceDialogCancelButton: {
15 | selector: '//button//span[text()="Cancel"]',
16 | locateStrategy: 'xpath'
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/part-3/pages/instancesPage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | elements: {
3 | instancesListDescription: {
4 | selector: '//div[@class="description-field col-flex-1"]',
5 | locateStrategy: 'xpath'
6 | },
7 | instancesTable: {
8 | selector: 'div[id=instances]'
9 | },
10 | instanceDialogEditTitle: {
11 | selector: '//h3[text()="Update an Instance"]',
12 | locateStrategy: 'xpath'
13 | },
14 | instanceDialogCancelButton: {
15 | selector: '//button//span[text()="Cancel"]',
16 | locateStrategy: 'xpath'
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/part-1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "syncano-testing-examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "e2e-setup": "selenium-standalone install",
8 | "test": "nightwatch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "babel-cli": "6.11.4",
14 | "babel-core": "6.11.4",
15 | "babel-loader": "6.2.4",
16 | "babel-plugin-add-module-exports": "0.2.1",
17 | "babel-preset-es2015": "6.9.0",
18 | "nightwatch": "0.9.9",
19 | "selenium-standalone": "5.9.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/part-2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "syncano-testing-examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "e2e-setup": "selenium-standalone install",
8 | "test": "nightwatch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "babel-cli": "6.11.4",
14 | "babel-core": "6.11.4",
15 | "babel-loader": "6.2.4",
16 | "babel-plugin-add-module-exports": "0.2.1",
17 | "babel-preset-es2015": "6.9.0",
18 | "nightwatch": "0.9.9",
19 | "selenium-standalone": "5.9.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/part-3/scripts/createConnection.js:
--------------------------------------------------------------------------------
1 | import Syncano from 'syncano';
2 |
3 | const createConnection = () => {
4 | const credentials = {
5 | email: process.env.EMAIL,
6 | password: process.env.PASSWORD
7 | }
8 | const connection = Syncano()
9 |
10 | return connection
11 | .Account
12 | .login(credentials)
13 | .then((user) => {
14 | connection.setAccountKey(user.account_key)
15 | user.connection = connection;
16 | return user;
17 | })
18 | .catch((error) => console.error('Connection error:\n', error.message));
19 | };
20 |
21 | export default createConnection;
22 |
--------------------------------------------------------------------------------
/part-3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "syncano-testing-examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "e2e-setup": "selenium-standalone install",
8 | "test": "nightwatch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "babel-cli": "6.11.4",
14 | "babel-core": "6.11.4",
15 | "babel-loader": "6.2.4",
16 | "babel-plugin-add-module-exports": "0.2.1",
17 | "babel-preset-es2015": "6.9.0",
18 | "nightwatch": "0.9.9",
19 | "selenium-standalone": "5.9.0",
20 | "syncano": "1.0.28"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/part-1/pages/loginPage.js:
--------------------------------------------------------------------------------
1 | const loginCommands = {
2 | login(email, pass) {
3 | return this
4 | .waitForElementVisible('@emailInput')
5 | .setValue('@emailInput', email)
6 | .setValue('@passInput', pass)
7 | .waitForElementVisible('@loginButton')
8 | .click('@loginButton')
9 | }
10 | };
11 |
12 | export default {
13 | url: 'https://dashboard.syncano.io/#/login',
14 | commands: [loginCommands],
15 | elements: {
16 | emailInput: {
17 | selector: 'input[type=text]'
18 | },
19 | passInput: {
20 | selector: 'input[name=password]'
21 | },
22 | loginButton: {
23 | selector: 'button[type=submit]'
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/part-2/pages/loginPage.js:
--------------------------------------------------------------------------------
1 | const loginCommands = {
2 | login(email, pass) {
3 | return this
4 | .waitForElementVisible('@emailInput')
5 | .setValue('@emailInput', email)
6 | .setValue('@passInput', pass)
7 | .waitForElementVisible('@loginButton')
8 | .click('@loginButton')
9 | }
10 | };
11 |
12 | export default {
13 | url: 'https://dashboard.syncano.io/#/login',
14 | commands: [loginCommands],
15 | elements: {
16 | emailInput: {
17 | selector: 'input[type=text]'
18 | },
19 | passInput: {
20 | selector: 'input[name=password]'
21 | },
22 | loginButton: {
23 | selector: 'button[type=submit]'
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/part-3/pages/loginPage.js:
--------------------------------------------------------------------------------
1 | const loginCommands = {
2 | login(email, pass) {
3 | return this
4 | .waitForElementVisible('@emailInput')
5 | .setValue('@emailInput', email)
6 | .setValue('@passInput', pass)
7 | .waitForElementVisible('@loginButton')
8 | .click('@loginButton');
9 | }
10 | };
11 |
12 | export default {
13 | url: 'https://dashboard.syncano.io/#/login',
14 | commands: [loginCommands],
15 | elements: {
16 | emailInput: {
17 | selector: 'input[type=text]'
18 | },
19 | passInput: {
20 | selector: 'input[name=password]'
21 | },
22 | loginButton: {
23 | selector: 'button[type=submit]'
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/part-2/tests/testInstances.js:
--------------------------------------------------------------------------------
1 | export default {
2 | before(client) {
3 | const loginPage = client.page.loginPage();
4 | const instancesPage = client.page.instancesPage();
5 |
6 | loginPage
7 | .navigate()
8 | .login(process.env.EMAIL, process.env.PASSWORD);
9 |
10 | instancesPage.waitForElementPresent('@instancesTable');
11 | },
12 | after(client) {
13 | client.end();
14 | },
15 | 'User clicks Edit Instance dropdown option': (client) => {
16 | const instancesPage = client.page.instancesPage();
17 | const socketsPage = client.page.socketsPage();
18 | const instanceName = client.globals.instanceName;
19 |
20 | instancesPage
21 | .clickListItemDropdown(instanceName, 'Edit')
22 | .waitForElementPresent('@instanceDialogEditTitle')
23 | .waitForElementPresent('@instanceDialogCancelButton')
24 | .click('@instanceDialogCancelButton')
25 | .waitForElementPresent('@instancesTable')
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/part-3/tests/testInstances.js:
--------------------------------------------------------------------------------
1 | export default {
2 | before(client) {
3 | const loginPage = client.page.loginPage();
4 | const instancesPage = client.page.instancesPage();
5 |
6 | loginPage
7 | .navigate()
8 | .login(process.env.EMAIL, process.env.PASSWORD);
9 |
10 | instancesPage.waitForElementPresent('@instancesTable');
11 | },
12 | after(client) {
13 | client.end();
14 | },
15 | 'User clicks Edit Instance dropdown option': (client) => {
16 | const instancesPage = client.page.instancesPage();
17 | const socketsPage = client.page.socketsPage();
18 | const instanceName = client.globals.instanceName;
19 |
20 | instancesPage
21 | .clickListItemDropdown(instanceName, 'Edit')
22 | .waitForElementPresent('@instanceDialogEditTitle')
23 | .waitForElementPresent('@instanceDialogCancelButton')
24 | .click('@instanceDialogCancelButton')
25 | .waitForElementPresent('@instancesTable')
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/part-2/commands/clickListItemDropdown.js:
--------------------------------------------------------------------------------
1 | // 'listItem' is the item name from the list. Corresponding dropdown menu will be clicked
2 | // 'dropdoownChoice' can be part of the name of the dropdown option like "Edit" or "Delete"
3 |
4 | exports.command = function clickListItemDropdown(listItem, dropdownChoice) {
5 | const listItemDropdown =
6 | `//div[text()="${listItem}"]/../../../following-sibling::div//span[@class="synicon-dots-vertical"]`;
7 | const choice = `//div[contains(text(), "${dropdownChoice}")]`;
8 |
9 | return this
10 | .useXpath()
11 | .waitForElementVisible(listItemDropdown)
12 | .click(listItemDropdown)
13 | // Waiting for the dropdown click animation to finish
14 | .waitForElementNotPresent('//span[@class="synicon-dots-vertical"]/preceding-sibling::span/div')
15 | .click(choice)
16 | // Waiting for dropdown to be removed from DOM
17 | .waitForElementNotPresent('//iframe/following-sibling::div//span[@type="button"]');
18 | };
19 |
--------------------------------------------------------------------------------
/part-3/commands/clickListItemDropdown.js:
--------------------------------------------------------------------------------
1 | // 'listItem' is the item name from the list. Corresponding dropdown menu will be clicked
2 | // 'dropdoownChoice' can be part of the name of the dropdown option like "Edit" or "Delete"
3 |
4 | exports.command = function clickListItemDropdown(listItem, dropdownChoice) {
5 | const listItemDropdown =
6 | `//div[text()="${listItem}"]/../../../following-sibling::div//span[@class="synicon-dots-vertical"]`;
7 | const choice = `//div[contains(text(), "${dropdownChoice}")]`;
8 |
9 | return this
10 | .useXpath()
11 | .waitForElementVisible(listItemDropdown)
12 | .click(listItemDropdown)
13 | // Waiting for the dropdown click animation to finish
14 | .waitForElementNotPresent('//span[@class="synicon-dots-vertical"]/preceding-sibling::span/div')
15 | .click(choice)
16 | // Waiting for dropdown to be removed from DOM
17 | .waitForElementNotPresent('//iframe/following-sibling::div//span[@type="button"]');
18 | };
19 |
--------------------------------------------------------------------------------
/part-3/tests/testScriptEndpoint.js:
--------------------------------------------------------------------------------
1 | import tempInstance from '../tempInstance';
2 |
3 | export default {
4 | before: (client) => {
5 | const loginPage = client.page.loginPage();
6 |
7 | loginPage
8 | .navigate()
9 | .login(process.env.EMAIL, process.env.PASSWORD);
10 | client.pause(2000);
11 | },
12 | after: (client) => client.end(),
13 | 'User adds Script Endpoint socket': (client) => {
14 | const scriptEndpointsPage = client.page.scriptEndpointsPage();
15 | const scriptEndpointName = 'testScriptEndpoint';
16 |
17 | scriptEndpointsPage
18 | .navigate()
19 | .clickElement('@scriptEndpointZeroStateAddButton')
20 | .fillInput('@scriptEndpointModalNameInput', scriptEndpointName)
21 | .fillInput('@scriptEndpointModalDropdown', tempInstance.scriptName)
22 | .clickElement('@scriptEndpointUserOption')
23 | .clickElement('@scriptEndpointModalNextButton')
24 | .clickElement('@scriptEndpointSummaryCloseButton')
25 | .waitForElementVisible('@scriptEndpointListItemRow');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/part-3/pages/scriptEndpointsPage.js:
--------------------------------------------------------------------------------
1 | import tempInstance from '../tempInstance';
2 |
3 | export default {
4 | url: `https://dashboard.syncano.io/#/instances/${tempInstance.instanceName}/script-endpoints`,
5 | elements: {
6 | scriptEndpointZeroStateAddButton: {
7 | selector: '//*[@data-e2e="zero-state-add-button"]',
8 | locateStrategy: 'xpath'
9 | },
10 | scriptEndpointModalNameInput: {
11 | selector: 'input[name="name"]'
12 | },
13 | scriptEndpointModalDropdown: {
14 | selector: 'input[data-e2e="script-name"]'
15 | },
16 | scriptEndpointUserOption: {
17 | selector: `[data-e2e=${tempInstance.scriptName}-user-option]`
18 | },
19 | scriptEndpointModalNextButton: {
20 | selector: '[data-e2e="script-dialog-confirm-button"]'
21 | },
22 | scriptEndpointSummaryCloseButton: {
23 | selector: '[data-e2e="script-endpoint-summary-dialog-close-button"]'
24 | },
25 | scriptEndpointListItemRow: {
26 | selector: '[data-e2e="testscriptendpoint-script-socket-row"]'
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/part-1/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders": ["tests"],
3 | "output_folder": "reports",
4 | "custom_commands_path": "",
5 | "custom_assertions_path": "",
6 | "page_objects_path": "pages",
7 | "globals_path": "globals",
8 |
9 | "selenium": {
10 | "start_process": true,
11 | "server_path": "./node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar",
12 | "log_path": "./reports",
13 | "host": "127.0.0.1",
14 | "port": 4444,
15 | "cli_args": {
16 | "webdriver.chrome.driver": "./node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver"
17 | }
18 | },
19 | "test_settings": {
20 | "default": {
21 | "launch_url": "https://dashboard.syncano.io",
22 | "selenium_port": 4444,
23 | "selenium_host": "localhost",
24 | "silent": true,
25 | "screenshots": {
26 | "enabled": false,
27 | "path": ""
28 | },
29 | "desiredCapabilities": {
30 | "browserName": "chrome",
31 | "javascriptEnabled": true,
32 | "acceptSslCerts": true
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/part-2/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders": ["tests"],
3 | "output_folder": "reports",
4 | "custom_commands_path": "commands",
5 | "custom_assertions_path": "",
6 | "page_objects_path": "pages",
7 | "globals_path": "./globals",
8 |
9 | "selenium": {
10 | "start_process": true,
11 | "server_path": "./node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar",
12 | "log_path": "./reports",
13 | "host": "127.0.0.1",
14 | "port": 4444,
15 | "cli_args": {
16 | "webdriver.chrome.driver": "./node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver"
17 | }
18 | },
19 | "test_settings": {
20 | "default": {
21 | "launch_url": "https://dashboard.syncano.io",
22 | "selenium_port": 4444,
23 | "selenium_host": "localhost",
24 | "silent": true,
25 | "screenshots": {
26 | "enabled": false,
27 | "path": ""
28 | },
29 | "desiredCapabilities": {
30 | "browserName": "chrome",
31 | "javascriptEnabled": true,
32 | "acceptSslCerts": true
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/part-3/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders": ["tests"],
3 | "output_folder": "reports",
4 | "custom_commands_path": "commands",
5 | "custom_assertions_path": "",
6 | "page_objects_path": "pages",
7 | "globals_path": "./globals",
8 |
9 | "selenium": {
10 | "start_process": true,
11 | "server_path": "./node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar",
12 | "log_path": "./reports",
13 | "host": "127.0.0.1",
14 | "port": 4444,
15 | "cli_args": {
16 | "webdriver.chrome.driver": "./node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver"
17 | }
18 | },
19 | "test_settings": {
20 | "default": {
21 | "launch_url": "https://dashboard.syncano.io",
22 | "selenium_port": 4444,
23 | "selenium_host": "localhost",
24 | "silent": true,
25 | "screenshots": {
26 | "enabled": false,
27 | "path": ""
28 | },
29 | "desiredCapabilities": {
30 | "browserName": "chrome",
31 | "javascriptEnabled": true,
32 | "acceptSslCerts": true,
33 | "chromeOptions": {
34 | "args": ["window-size=1366,768"]
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Syncano Testing Examples
2 | [](https://www.syncano.io/slack-invite/)
3 |
4 | End to End testing of React applications with Nightwatch
5 |
6 | ## Introduction
7 | In the mid of 2015 our front-end team took the challenge of rebuilding the entire Dashboard from scratch. In a matter of three months we built a new version using the [React](https://github.com/facebook/react) library. Since it was hard to keep up with writing unit tests at such demanding pace we decided that end-to-end (e2e) will be our go-to test strategy.
8 |
9 | The most obvious choice for e2e tests is [Selenium](https://github.com/SeleniumHQ/selenium) but there are many language bindings and frameworks to choose from. Eventually we settled on [Nightwatch.js](http://nightwatchjs.org/).
10 |
11 | We wanted to share our experience, thus we have created this repository holding all our blog posts with code examples.
12 | Every part of it will be organized in a separate folder beginning with `part-` and number representing the blog posts number in the series.
13 |
14 | ## Table of Contents
15 |
16 | Part Title | Folder
17 | ---------- | --------------
18 | End to End testing of React apps with Nightwatch | [Part 1](part-1/)
19 | Before(), after() hooks and custom commands in Nightwatch | [Part 2](part-2/)
20 | Data Driven Testing at Syncano | [Part 3](part-3/)
21 |
22 | ## Requirements
23 | First thing you need to do is to install [Node.js](https://nodejs.org/en/) if you don’t yet have it. You can find the installation instructions on the Node.js project page. Once you have node installed, you can take advantage of it’s package manager called `npm`.
24 |
25 | You will also `need`:
26 | - [Chrome Browser](https://www.google.com/chrome/)
27 | - [Java v8](https://java.com/en/download/)
28 | - [Java Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
29 | - [Syncano Dashboard Account](https://dashboard.syncano.io/#/signup)
30 |
31 | As they are all required for `Selenium` and `Nightwatch` to work properly.
32 |
33 | ## Installation
34 |
35 | Before you will be able to run any tests you should install proper `part` in it's folder. To do so just follow examples below, where `X` is post number/directory.
36 |
37 | ```sh
38 | $ cd part-X/
39 | $ npm install
40 | $ npm run e2e-setup
41 | ```
42 | Now you have installed all dependancies using `npm` and executed `node` script that installs selenium.
43 |
44 | ## Contact
45 |
46 | If you have any questions, or just want to say hi, drop us a line at [support@syncano.com](mailto:support@syncano.com).
47 |
--------------------------------------------------------------------------------
/part-2/README.md:
--------------------------------------------------------------------------------
1 | # Testing @ Syncano
2 |
3 | ### Testing React apps with Nightwatch - before(), after() hooks and custom commands
4 |
5 | This is the second part of End to End testing of React apps with Nightwatch series.
6 | In the [previous post](https://www.syncano.io/blog/testing-syncano/) I've talked about Nightwatch:
7 | - installation
8 | - configuration of nightwatch.json and package.json files
9 | - adding ECMAS 6 to Nightwatch
10 | - Writing the test in Page Object Pattern methodology
11 |
12 | In this part I'll focus on couple of tricks that'll let you write better tests.
13 | I'll cover:
14 | - Using `before()` and `after()` hooks in your tests
15 | - Extending Nightwatch with custom commands
16 |
17 |
18 | This post builds upon the previous part of the series, which can be found here:
19 | [End to End testing of React apps with Nightwatch - Part 1](https://www.syncano.io/blog/testing-syncano/)
20 | You don't have to go through the first part but we'll be basing on the code
21 | that was written there. The code can be found in [syncano-testing-examples](https://github.com/Syncano/syncano-testing-examples/)
22 | in **part-1** folder. Finished code for this part of Nightwatch tutorial series
23 | can be found in **part-2** folder.
24 |
25 | Since we moved all the technicalities out of the way, we can get to the good
26 | bits. Lets start with the before() and after() hooks in Nightwatch.
27 |
28 | #### Using before() and after() hooks in your tests
29 |
30 | `before()` and `after()` hooks are quite self descriptive. They let you write code,
31 | that'll get executed before or after your test suite (tests that are grouped in
32 | one file). Another useful variation are `beforeEach()` and `afterEach()` hooks.
33 | Pieces of code encapsulate in these will get executed before or after **each**
34 | test in a file. Ok, enough with the theory! Lets see those bad boys in action.
35 |
36 | > It's also possible to use `before()` and `after()` hooks in a global context.
37 | > In this case they would execute code before and after whole suite is run.
38 | > These hooks should be defined in globals.js file
39 |
40 | Remember the login test we've written in the previous part (it's in `tests/testLogin.js`
41 | file)? It looked like this:
42 |
43 | ```javascript
44 | export default {
45 | 'User Logs in': (client) => {
46 | const loginPage = client.page.loginPage();
47 | const instancesPage = client.page.instancesPage();
48 |
49 | loginPage
50 | .navigate()
51 | .login(process.env.EMAIL, process.env.PASSWORD);
52 |
53 | instancesPage.expect.element('@instancesListDescription').text.to.contain('Your first instance.');
54 |
55 | client.end();
56 | }
57 | };
58 | ```
59 |
60 | That's very nice. But what if I wanted to:
61 | - Have couple of tests grouped in a single file (they are executed sequentially)
62 | - The browser to open before all tests from this file and closed after
63 | they are finished
64 | - Login should be performed before all the tests
65 |
66 | This is where the hooks come in. Thanks to `before()` and `after()` I can extract
67 | parts of the logic out of the tests and make them more robust. Lets consider a
68 | case, where I'd want a user to login and then view his Instance details. This is
69 | how I'd structure such test:
70 |
71 | ```javascript
72 | export default {
73 | before(client) {
74 | const loginPage = client.page.loginPage();
75 | const instancesPage = client.page.instancesPage();
76 |
77 | loginPage
78 | .navigate()
79 | .login(process.env.EMAIL, process.env.PASSWORD);
80 |
81 | instancesPage.waitForElementPresent('@instancesTable');
82 | },
83 | after(client) {
84 | client.end();
85 | },
86 | 'User goes to Instance details view': (client) => {
87 | const instancesPage = client.page.instancesPage();
88 | const socketsPage = client.page.socketsPage();
89 |
90 | instancesPage
91 | .navigate()
92 | .click('@instancesTableName')
93 |
94 | socketsPage.waitForElementPresent('instancesDropdown');
95 | }
96 | };
97 | ```
98 | So now the `before()` hook will take care of login steps and `after()` will close
99 | the browser when all tests from this file are done. Simple, right? The only thing
100 | I need to do now is fill in the missing selectors. I'll add `@instancesTable` selector to the
101 | instancesPage, so that it looks like this:
102 |
103 | ```javascript
104 | export default {
105 | elements: {
106 | instancesListDescription: {
107 | selector: '//div[@class="description-field col-flex-1"]',
108 | locateStrategy: 'xpath'
109 | },
110 | instancesTable: {
111 | selector: 'div[id=instances]'
112 | }
113 | }
114 | };
115 | ```
116 |
117 | Since it's a css selector, I don't have to pass the `locateStrategy` property
118 | in the instancesTable object because nightwatch is using css as a default
119 | locator strategy.
120 |
121 | I'll also need to add `socketsPage.js` file in the `pages` folder and add these
122 | lines:
123 |
124 | instancesPage:
125 |
126 | ```javascript
127 | export default {
128 | elements: {
129 | instancesDropdown: {
130 | selector: '.instances-dropdown'
131 | }
132 | }
133 | };
134 | ```
135 |
136 | That's it! The only thing you need to do now, is to export your email and password
137 | (if you haven't done so) as an environment variables. Open your terminal app
138 | and type these lines:
139 |
140 | ```sh
141 | export EMAIL=YOUR_SYNCANO_EMAIL
142 | export PASSWORD=YOUR_SYNCANO_PASSWORD
143 | ```
144 |
145 | > If you don't want to use environment variables, you can pass your email
146 | > and password as strings directly to loginPage.login() method
147 |
148 | ### Extending nightwatch with custom commands
149 |
150 | Once your test suite gets bigger, you'll notice that there are steps within your
151 | tests that could be abstracted away and reused across your project. This is where
152 | custom commands come in. Thanks to this feature you'll be able to define methods
153 | that are accessible from anywhere within a test suite.
154 |
155 | First thing we need to do, is add a folder for the custom commands. You can add
156 | it in the root of the project and name it `commands`. Once it's done, you'll have
157 | to tell nightwatch where the custom commands are. To do this:
158 | - open `nightwatch.json` file
159 | - edit the code in line 4 to look like this:
160 |
161 | ```javascript
162 | "custom_commands_path": "./commands",
163 | ```
164 | Now nightwatch will know where to look for the commands.
165 |
166 | Since there are a lot of dropdowns in the Syncano Dashboard, it makes sense to
167 | abstract the logic around them into a custom command. The command will:
168 | - wait for the dropdown element to be visible
169 | - click the dropdown
170 | - wait for the dropdown animation to finish (this helps with the test stability)
171 | - click the dropdown option
172 | - wait for the dropdown to be removed from the DOM
173 |
174 | In order to create this command:
175 | - add `clickListItemDropdown.js` file in the commands folder
176 | - paste this code in the `clickListItemDropdown.js` file:
177 |
178 | ```javascript
179 | // 'listItem' is the item name from the list. Corresponding dropdown menu will be clicked
180 | // 'dropdoownChoice' can be part of the name of the dropdown option like "Edit" or "Delete"
181 |
182 | exports.command = function clickListItemDropdown(listItem, dropdownChoice) {
183 | const listItemDropdown =
184 | `//div[text()="${listItem}"]/../../../following-sibling::div//span[@class="synicon-dots-vertical"]`;
185 | const choice = `//div[contains(text(), "${dropdownChoice}")]`;
186 |
187 | return this
188 | .useXpath()
189 | .waitForElementVisible(listItemDropdown)
190 | .click(listItemDropdown)
191 | // Waiting for the dropdown click animation to finish
192 | .waitForElementNotPresent('//span[@class="synicon-dots-vertical"]/preceding-sibling::span/div')
193 | .click(choice)
194 | // Waiting for dropdown to be removed from DOM
195 | .waitForElementNotPresent('//iframe/following-sibling::div[@style]/div');
196 | };
197 | ```
198 |
199 | Now, since we have the command ready we will want to use it in a test. Create a
200 | `testInstances.js` file in the `tests` folder. We will use the `before()` and
201 | `after()` hooks from the first part of this post. The draft for this test will
202 | look like this:
203 |
204 | ```javascript
205 |
206 | export default {
207 | before(client) {
208 | const loginPage = client.page.loginPage();
209 | const instancesPage = client.page.instancesPage();
210 |
211 | loginPage
212 | .navigate()
213 | .login(process.env.EMAIL, process.env.PASSWORD);
214 |
215 | instancesPage.waitForElementPresent('@instancesTable');
216 | },
217 | after(client) {
218 | client.end();
219 | },
220 | 'User clicks Edit Instance dropdown option': (client) => {
221 | const instancesPage = client.page.instancesPage();
222 | const socketsPage = client.page.socketsPage();
223 | const instanceName = client.globals.instanceName;
224 |
225 | instancesPage
226 | .clickListItemDropdown(instanceName, 'Edit')
227 | .waitForElementPresent('@instanceDialogEditTitle')
228 | .waitForElementPresent('@instanceDialogCancelButton')
229 | .click('@instanceDialogCancelButton')
230 | .waitForElementPresent('@instancesTable')
231 | }
232 | };
233 | ```
234 |
235 | The test will:
236 | - log in the user in the `before()` step
237 | - Click the Instance dropdown
238 | - Click 'Edit' option from the dropdown
239 | - Wait for the Dialog window to show up
240 | - Click 'Cancel' button
241 | - Wait for the Instances list to show up
242 |
243 | What we still need to do is to add the missing selectors in the `pages/instancesPage.js`
244 | file. Copy the code and paste it below the existing selectors (remember about adding
245 | comma after the last one already present):
246 |
247 | ```javascript
248 | instanceDialogEditTitle: {
249 | selector: '//h3[text()="Update an Instance"]',
250 | locateStrategy: 'xpath'
251 | },
252 | instanceDialogCancelButton: {
253 | selector: '//button//span[text()="Cancel"]',
254 | locateStrategy: 'xpath'
255 | }
256 | ```
257 |
258 | We are also using a global variable within a test. Go to `globals.js` file and
259 | add a new line:
260 |
261 | ```javascript
262 | instanceName: INSTANCE_NAME
263 | ```
264 | where the INSTANCE_NAME would be the name of your Syncano instance.
265 |
266 |
267 | > Rembember to use npm run e2e-setup before starting tests. You only need to do it once.
268 |
269 | Now, since everything is ready, you can run your tests. We want to run only a
270 | single test, so we'll run the suite like this:
271 |
272 | ```sh
273 | npm test -t tests/testInstances.js
274 | ```
275 |
276 | That's it for the second part of "Testing React apps with Nightwatch" series. Be sure to follow us for more parts to come!
277 |
278 | If you have any questions or just want to say hi, drop me a line at support@syncano.com
279 |
--------------------------------------------------------------------------------
/part-1/README.md:
--------------------------------------------------------------------------------
1 | # Testing @ Syncano
2 |
3 | ### End to End testing of React applications with Nightwatch part I
4 |
5 | #### Why we joined the dark side
6 | In the mid of 2015 our front-end team took the challenge of rebuilding the entire Dashboard from scratch. In a matter of three months we built a new version using the React library. Since it was hard to keep up with writing unit tests at such demanding pace we decided that end-to-end (e2e) will be our go-to test strategy.
7 |
8 | The most obvious choice for e2e tests is Selenium but there are many language bindings and frameworks to choose from. Eventually we settled on Nightwatch.js for a number of reasons:
9 |
10 | * It has built-in support for Page Object Pattern methodology
11 | * It’s written in Node.js so it nicely integrates with the front-end stack
12 | * It has built-in test runner. You can run your tests in parallel, sequentially, with different environments etc.
13 | * It was easy to integrate with CircleCI which we currently use as our continuous integration tool
14 | * It’s handling taking screenshots on errors and failures
15 |
16 | In this post I’ll show you how to setup a simple Nightwatch project with using the Page Object Pattern. The finished code for this tutorial is on [Github](https://github.com/Syncano/syncano-testing-examples) so you can grab the fully working example from there or follow the tutorial steps to make it from scratch.
17 |
18 | #### Installation
19 | First thing you need to do is to install Node.js if you don’t yet have it. You can find the installation instructions on the Node.js project page. Once you have node installed, you can take advantage of it’s package manager called `npm`.
20 |
21 | Go to your terminal, create an empty repository and cd into it. Next, type `npm init`. You can skip the steps of initialising `package.json` file by pressing enter several times and typing ‘yes’ at the end.
22 |
23 | Once you have a package.json file, while in the same directory, type `npm install nightwatch --save-dev`. This will install the latest version of nightwatch into the `node_modules` directory inside your project and save it in your `package.json` file as a development dependency.
24 |
25 | Next, in order to be able to run the tests, we need to download the Selenium standalone server. We could do this manually and take it from the projects’ website but lets use npm to handle this:
26 |
27 | - Type `npm install selenium-standalone --save-dev`
28 | - Modify your package.json file by adding a `scripts` property with `"e2e-setup": "selenium-standalone install"` and `"test": "nightwatch"` lines.
29 |
30 | The package.json should look more or less like this:
31 |
32 | ```javascript
33 | {
34 | "name": "syncano-testing-examples",
35 | "version": "1.0.0",
36 | "description": "",
37 | "main": "index.js",
38 | "scripts": {
39 | "e2e-setup": "selenium-standalone install",
40 | "test": "nightwatch"
41 | },
42 | "author": "",
43 | "license": "ISC",
44 | "devDependencies": {
45 | "babel-cli": "6.11.4",
46 | "babel-core": "6.11.4",
47 | "babel-loader": "6.2.4",
48 | "babel-plugin-add-module-exports": "0.2.1",
49 | "babel-preset-es2015": "6.9.0",
50 | "nightwatch": "0.9.9",
51 | "selenium-standalone": "5.9.0"
52 | }
53 | }
54 |
55 | ```
56 |
57 | Now running `npm run e2e-setup` will download the latest version of selenium server and chromedriver (which will be needed for running tests in Chrome browser)
58 |
59 | #### Configuration
60 |
61 | Nightwatch relies on `nightwatch.json` as the configuration file for the test runs. It should be placed in projects root directory. It specifies various configuration settings like test environments (browsers, resolutions), test file paths and selenium-specific settings. This is how the configuration file can look like:
62 |
63 | ```javascript
64 | {
65 | "src_folders": ["tests"],
66 | "output_folder": "reports",
67 | "custom_commands_path": "",
68 | "custom_assertions_path": "",
69 | "page_objects_path": "pages",
70 | "globals_path": "globals",
71 |
72 | "selenium": {
73 | "start_process": true,
74 | "server_path": "./node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar",
75 | "log_path": "./reports",
76 | "host": "127.0.0.1",
77 | "port": 4444,
78 | "cli_args": {
79 | "webdriver.chrome.driver": "./node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver"
80 | }
81 | },
82 | "test_settings": {
83 | "default": {
84 | "launch_url": "https://dashboard.syncano.io",
85 | "selenium_port": 4444,
86 | "selenium_host": "localhost",
87 | "silent": true,
88 | "desiredCapabilities": {
89 | "browserName": "chrome",
90 | "javascriptEnabled": true,
91 | "acceptSslCerts": true
92 | }
93 | }
94 | }
95 | }
96 | ```
97 |
98 | I'll go through the important parts of the `nightwatch.json` file:
99 |
100 | * `src_folders` - an array that contains the folders that your tests reside in
101 | * `output_folder` - folder where the test artifacts (XML reports, selenium log and screenshots) are being stored
102 | * `page_objects_path` - a folder where your Page Objects will be defined
103 | * `globals_path` - path to a file which stores global variables
104 | * `selenium` - selenium specific settings. In our case it's important to have the `start_process` set to `true` so that selenium server starts automatically. Also the `server_path` and `webdriver.chrome.driver` paths should have proper folder specified.
105 |
106 | `test_settings` is an object where you specify the test environments. The important bit in the `default` environment is the `desiredCapabilities` object where we specify the `chrome` as the `browserName` so that Nightwatch will run the test against it.
107 |
108 | #### Adding ECMAScript 6 to nightwatch
109 |
110 | We are writing the Syncano Dashboard according to the ECMAScript 6 specs and we wanted to do the same for Nightwatch. In order to be able to do that, you'll have to add a `nightwatch.conf.js` file to the root of your project. The file should contain these couple of lines:
111 |
112 | ```javascript
113 | require('babel-core/register');
114 |
115 | module.exports = require('./nightwatch.json');
116 | ```
117 | Bang! You can now write your tests in ECMAS 6
118 |
119 | > Edit: things have changed since I've written this article. Now you'll need to
120 | > add es2015 preset in .babelrc config file and add `add-module-exports` plugin
121 | > and do `npm i babel-plugin-add-module-exports babel-preset-es2015 --save-dev`.
122 | > Everything should work after that. See the syncano-testing-examples repo for
123 | > details
124 |
125 | #### The Tests
126 |
127 | Before we get to the test code there are only two things left to do:
128 |
129 | * Go to [Syncano Dashboard]("https://dashboard.syncano.io/#/signup") and sign up to our service (if you suspect that this article is an elaborate plot to make you sign up, then you are right)
130 | * Go to your terminal and paste these two lines (where "your_email" and "your_password" will be the credentials that you just used when signing up):
131 | * `export EMAIL="your_email"`
132 | * `export PASSWORD="your_password"`
133 |
134 | (If you are on a windows machine than the command will be `SET` instead of `export`)
135 |
136 | ##### Test if a user can log in to the application
137 | In the root of your project create a `tests` directory. Create a testLogin.js file and paste there this code:
138 |
139 | ```javascript
140 | export default {
141 | 'User Logs in': (client) => {
142 | const loginPage = client.page.loginPage();
143 | const instancesPage = client.page.instancesPage();
144 |
145 | loginPage
146 | .navigate()
147 | .login(process.env.EMAIL, process.env.PASSWORD);
148 |
149 | instancesPage.expect.element('@instancesListDescription').text.to.contain('Your first instance.');
150 |
151 | client.end();
152 | }
153 | };
154 | ```
155 |
156 | This is a test that is checking if a user is able to log in to the application. As you can see the code is simple:
157 |
158 | * User navigates to the log in page
159 | * User logs in using his credentials (I'm using node `process.env` method to get the environment variables we exported in the previous step)
160 | * The tests asserts that 'Your first instance.' text is visible on the page.
161 | * `client.end()` method ends the browser session
162 |
163 | The way to achieve this sort of clarity within a test, where the business logic is presented clearly and test can be easily understood even by non tech-saavy people is by introducing the Page Object pattern. `loginPage` and `instancesPage` objects contain all the methods and ui elements that are needed to make interactions within that page.
164 |
165 | ##### Log in Page Object
166 | Page Objects files should be created in a `pages` folder. Create one in the root of your project. Next, create a `loginPage.js` file that will contain this code:
167 |
168 | ```javascript
169 | const loginCommands = {
170 | login(email, pass) {
171 | return this
172 | .waitForElementVisible('@emailInput')
173 | .setValue('@emailInput', email)
174 | .setValue('@passInput', pass)
175 | .waitForElementVisible('@loginButton')
176 | .click('@loginButton')
177 | }
178 | };
179 |
180 | export default {
181 | url: 'https://dashboard.syncano.io/#/login',
182 | commands: [loginCommands],
183 | elements: {
184 | emailInput: {
185 | selector: 'input[type=text]'
186 | },
187 | passInput: {
188 | selector: 'input[name=password]'
189 | },
190 | loginButton: {
191 | selector: 'button[type=submit]'
192 | }
193 | }
194 | };
195 | ```
196 |
197 | The file contains an object loginCommands that stores a `login` method. The `login` method waits for an email input element to be visible, sets the values of email and password fields, waits for login button to be visible and finally clicks the button. We actually could write these steps in the "User Logs in" test. If we are planning to create a bigger test suite though then it makes sense to encapsulate that logic into a single method that can be reused in multiple test scenarios.
198 |
199 | Apart from the `loginCommands` there's a second object defined below which is actually the Page Object that we instantiate in the `testLogin.js` file with this line:
200 |
201 | `const loginPage = client.page.loginPage();`
202 |
203 | as you can see the Page Object contains:
204 |
205 | * the pages url (when `navigate()` method in the test is called it uses this url as a parameter)
206 | * `commands` property where we pass the `loginCommands` object defined above, so that the `login` method can be used within this page's context
207 | * `elements` property where the actual selectors for making interactions with the web page are stored
208 |
209 | As you've probably noticed there's an `@` prefix used before the locators both inside the test and in the loginCommands object. This tells Nightwatch that it should refer to the key declared in the `elements` property inside the Page Object.
210 |
211 | ##### Instances Page Object
212 |
213 | Now let's create a second file in the pages folder that will be named `instancesPage.js`. It should contain the following code:
214 |
215 | ```javascript
216 | export default {
217 | elements: {
218 | instancesListDescription: {
219 | selector: '//div[@class="description-field col-flex-1"]',
220 | locateStrategy: 'xpath'
221 | }
222 | }
223 | };
224 | ```
225 |
226 | It's a lot simpler than the loginPage file since it only has a single `instancesListDescription` element. What is interesting about this element is that it's not a CSS selector as the elements in the loginPage.js file but an XPath selector. You can use XPath selectors by adding a `locateStrategy: xpath` property to the desired element.
227 |
228 | The `instancesListDescription` element is used in the 11 line of the loginPage.js file to assert if a login was successful.
229 |
230 | ```javascript
231 | instancesPage.expect.element('@instancesListDescription').to.be.visible;
232 | ```
233 | As you can see the assertion is verbose and readable because Nightwatch relies on [Chai Expect](http://chaijs.com/api/bdd/) library which allows for use of these BDD-style assertions.
234 |
235 | ##### Global configuration
236 |
237 | There's one last piece of the puzzle missing in order to be able to run the tests. Nightwatch commands like `waitForElementVisible()` or the assertions require the timeout parameter to be passed along the element, so that the test throws an error when that timeout limit is reached. So normally the `waitForElementVisible()` method would look like this:
238 |
239 | `waitForElementVisible('@anElement', 3000)`
240 |
241 | similarly the assertion would also have to have the timeout specified:
242 |
243 | `instancesPage.expect.element('@instancesListDescription').to.be.visible.after(3000);`
244 |
245 | Where `3000` is the amount of milliseconds after which the test throws an `element not visible` exception. Fortunately we can move that value outside the test so that the code is cleaner. In order to do that create a globals.js file in the root of your project and paste there this code:
246 |
247 | ```javascript
248 | export default {
249 | waitForConditionTimeout: 10000,
250 | };
251 | ```
252 |
253 | Now all the Nightwatch methods that require a timeout will have this global 10 second timeout specified as default. You can still define a special timeout for single calls if needed.
254 |
255 | ##### Running the test
256 |
257 | That's it! The only thing left to do is to run the test. In the terminal, go to your projects' root directory (where the nightwatch.json file is in) and run this command:
258 |
259 | > Rembember to use npm run e2e-setup before starting tests. You only need to do it once.
260 |
261 | `npm test`
262 |
263 | With a bit of luck you should see a console output similar to this one:
264 |
265 | ```
266 | Starting selenium server... started - PID: 13085
267 |
268 | [Test Login] Test Suite
269 | =======================
270 |
271 | Running: User Logs in
272 | ✔ Element was visible after 87 milliseconds.
273 | ✔ Element