├── .gitignore ├── README.md ├── part-1 ├── .babelrc ├── .gitignore ├── README.md ├── globals.js ├── nightwatch.conf.js ├── nightwatch.json ├── package.json ├── pages │ ├── instancesPage.js │ └── loginPage.js └── tests │ └── testLogin.js ├── part-2 ├── .babelrc ├── .gitignore ├── README.md ├── commands │ └── clickListItemDropdown.js ├── globals.js ├── nightwatch.conf.js ├── nightwatch.json ├── package.json ├── pages │ ├── instancesPage.js │ ├── loginPage.js │ └── socketsPage.js └── tests │ ├── testInstances.js │ └── testLogin.js └── part-3 ├── .babelrc ├── .gitignore ├── README.md ├── commands ├── clickElement.js ├── clickListItemDropdown.js ├── fillInput.js └── selectDropdownValue.js ├── globals.js ├── nightwatch.conf.js ├── nightwatch.json ├── package.json ├── pages ├── instancesPage.js ├── loginPage.js ├── scriptEndpointsPage.js └── socketsPage.js ├── scripts ├── cleanUp.js ├── createConnection.js ├── createInstance.js ├── createScript.js ├── createTestData.js ├── deleteInstance.js └── saveVariables.js └── tests ├── testInstances.js ├── testLogin.js └── testScriptEndpoint.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syncano Testing Examples 2 | [![Slack Status](https://img.shields.io/badge/chat-on_slack-blue.svg)](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-1/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /part-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports -------------------------------------------------------------------------------- /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 was visible after 43 milliseconds. 274 | ✔ Expected element text to contain: "Your first instance." - condition was met in 2474ms 275 | 276 | OK. 3 assertions passed. (10.437s) 277 | ``` 278 | 279 | Well done! You've run your first Nightwatch test. 280 | 281 | ### Summary 282 | In this article you've learned how to: 283 | 284 | * Install and configure Nightwatch.js and it's dependencies 285 | * Create an end to end test in the Page Object Pattern methodology 286 | * Use the globals.js file 287 | 288 | This just the beginning in terms of what can be achieved with Nightwatch. In the following posts I'll be showing you how to: 289 | 290 | * Use `before()` and `after()` hooks in your tests 291 | * Extend Nightwatch with custom commands 292 | * Add your tests to continuous integration tools like CircleCI 293 | * Use cool XPath selectors that will save you development time 294 | 295 | If you have any questions or just want to say hi, drop me a line at support@syncano.com 296 | -------------------------------------------------------------------------------- /part-1/globals.js: -------------------------------------------------------------------------------- 1 | export default { 2 | waitForConditionTimeout: 10000, 3 | }; 4 | -------------------------------------------------------------------------------- /part-1/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | 3 | module.exports = require('./nightwatch.json'); 4 | -------------------------------------------------------------------------------- /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-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-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-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-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/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /part-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /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-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-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-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-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-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-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-2/pages/socketsPage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | elements: { 3 | instancesDropdown: { 4 | selector: '.instances-dropdown' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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-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/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /part-3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports 3 | npm-debug.log 4 | tempInstance.js 5 | -------------------------------------------------------------------------------- /part-3/README.md: -------------------------------------------------------------------------------- 1 | # Testing React apps with Nightwatch - Data Driven Testing at Syncano 2 | 3 | This is the third part of the “End-to-End Testing of React Apps with Nightwatch” series. 4 | 5 | In the [previous posts](https://www.syncano.io/blog/end-to-end-testing-of-react-apps-with-nightwatch-part-2/), we've talked about Nightwatch and how to: 6 | 7 | - install and configure Nightwatch.js and it's dependencies 8 | - create an end-to-end test in the Page Object Pattern methodology 9 | - use the globals.js file 10 | - use before() and after() hooks in your tests 11 | - expand the functions of Nightwatch with custom commands 12 | 13 | 14 | ## In this part, I'll focus on creating tools for data-driven testing. 15 | 16 | What is data-driven testing? Let's check Wikipedia for a definition of it. 17 | 18 | > Data-driven testing is the creation of test scripts to run together with their related data sets in a framework. The framework provides re-usable test logic to reduce maintenance and improve test coverage. Input and result (test criteria) data values can be stored in one or more central data sources or databases, the actual format and organization can be implementation specific. 19 | 20 | In general, this is exactly what we'll try to achieve. 21 | 22 | _Why?_ you may ask. 23 | 24 | While we expanded our tests in `Syncano`, we had to deal with race conditions, `Nightwatch` running tests in the order of folder/file names, and general issues with components dependencies. 25 | 26 | To solve these, and many other problems, our decision was to create accounts/Instances and components of our Dashboard before tests even start. This is how our tool was born. 27 | 28 | In this post, I'll show you a simplified version of our approach to data-driven testing. 29 | 30 | I'll cover: 31 | 32 | - creating a JavaScript tool for data-driven testing 33 | - using this tool with Nightwatch tests 34 | - running tests with our tool 35 | 36 | >This post builds upon the previous part of the series, which can be found here: [End-to-End Testing of React Apps with Nightwatch - Part 2](https://www.syncano.io/blog/end-to-end-testing-of-react-apps-with-nightwatch-part-2/). You don't have to go through the first two parts, but I'll be building on the code that was written there. The code can be found in [syncano-testing-examples](https://github.com/Syncano/syncano-testing-examples/), inside of **part-1** and **part-2** folders. 37 | 38 | >Finished code for this part of the Nightwatch tutorial series can be found in **part-3** folder. 39 | 40 | ## Creating javascript tool for data driven testing 41 | 42 | Before we start we'll need to modify `package.json` a bit by adding `syncano` as a dependency. 43 | 44 | ```javascript 45 | [...] 46 | "devDependencies": { 47 | [...] 48 | "syncano": "1.0.23" 49 | } 50 | ``` 51 | 52 | This will let us use a `Syncano` package to connect to the Syncano Dashboard, create a test Instance, and feed it with data. 53 | 54 | Because we use the API in Syncano for all the operations, and we have JavaScript libraries, we can easily use them in our tests! 55 | 56 | The overall plan of how our tool will work is: 57 | 58 | - connect to Syncano account 59 | - create test Instance in our account 60 | - create test script in our Instance 61 | - cave our test Instance information in a js file 62 | - use our tool in the tests 63 | - perform a cleanup on our account after tests 64 | 65 | Let's get to work! We'll start by creating a new folder named `scripts`, where we will add all the files to keep the repo organized. 66 | 67 | First, I'll show you how to create a simple connection script to a `Syncano` account, which will enable us to later feed our account with data. 68 | 69 | Let's create a file named `createConnection.js` and paste this code in there: 70 | 71 | ```javascript 72 | import Syncano from 'syncano'; 73 | 74 | const createConnection = () => { 75 | const credentials = { 76 | email: process.env.EMAIL, 77 | password: process.env.PASSWORD 78 | } 79 | const connection = Syncano() 80 | 81 | return connection 82 | .Account 83 | .login(credentials) 84 | .then((user) => { 85 | connection.setAccountKey(user.account_key) 86 | user.connection = connection; 87 | return user; 88 | }) 89 | .catch((error) => console.error('Connection', error.message)); 90 | } 91 | 92 | export default createConnection; 93 | ``` 94 | 95 | Right now we have a function that will connect us with Syncano, get connection object, and return it. 96 | 97 | Also, we are using our exported `EMAIL` and `PASSWORD`; this way, the script will connect to your account (an explanation of how the export works is in [part one](https://www.syncano.io/blog/testing-syncano/) of this series). In our code, we simply use the `Syncano` JavaScript library to connect to our account, and return the connection object that we will assign to the `user` variable. 98 | 99 | > I won't focus too much on explaining JavaScript and our Syncano library. If you need more info, check them out at [docs](http://docs.syncano.io/v0.1.1/docs/). 100 | 101 | But we need to use it, so now we will create the main part of the whole `test data creation` script. 102 | 103 | So, the next step is to create a `createTestData.js` file in the `scripts` folder. This will be our main script that will call other JavaScript files and execute them. 104 | 105 | Let's source our newly created connection in it. 106 | 107 | ```javascript 108 | import createConnection from './createConnection'; 109 | 110 | createConnection() 111 | .then((user) => console.log(user.connection)) 112 | .catch((error) => console.log(error)); 113 | ``` 114 | 115 | We've just sourced our script! Now, if we run it in the terminal using `babel-node scripts/createTestData.js` we should get a really big output. That's the connection object. 116 | 117 | Neat! But we need to expand our script. So, our next step will be creating a test Instance. Don't worry, we will delete it at the end of our tests, so that it won't mess up your account! 118 | 119 | Let's create another file -- `createInstances.js` -- with this code in it: 120 | 121 | ```javascript 122 | const createInstance = (user) => { 123 | const name = 'testInstance' + Date.now(); 124 | const instance = { 125 | name 126 | }; 127 | 128 | return user.connection.Instance 129 | .please() 130 | .create(instance) 131 | .then(() => { 132 | user.instanceName = name; 133 | user.connection.setInstanceName(user.instanceName); 134 | return user; 135 | }) 136 | .catch((error) => console.error('Instance error:\n', error.message)); 137 | } 138 | 139 | export default createInstance; 140 | ``` 141 | 142 | Since we are connected with the Syncano API using the Syncano JS lib, we can now start creating test data. 143 | 144 | As you may see, we don't import the `Syncano` library here, we just pass the user that has the connection object. 145 | 146 | After the script succeeds, we assign our Instance name to `user.instanceName`, and set it as the current Instance (this is needed for next script). 147 | 148 | Now we need to modify our `createTestData.js` file. Let's do this by changing it to: 149 | 150 | ```javascript 151 | import createConnection from './createConnection'; 152 | import createInstance from './createInstance'; 153 | 154 | createConnection() 155 | .then((user) => createInstance(user)) 156 | .then((user) => { 157 | delete user.connection; 158 | console.log('Your test setup:\n', user); 159 | }) 160 | .catch((error) => console.log('Global error:\n', error)); 161 | ``` 162 | 163 | We have imported our `createInstance` file, chained it with createConnection, and logged out of our setup at the end of script. 164 | 165 | > We also added delete `user.connection` to cut out the connection object in the final output, as it is not useful for us anymore. 166 | 167 | Ok, we have our test Instance created. Now it's time to add more data items that'll be needed in further tests. 168 | 169 | I'll show you how to add a `script`, which we will later use to create an e2e test in `Nightwatch` by creating a script endpoint. Our script name will be displayed in a dropdown menu. 170 | 171 | But, first things first, let's create a file named `createScript.js` and append it with this code: 172 | 173 | ```javascript 174 | const createScript = (user) => { 175 | const label = 'testScript' + Date.now(); 176 | const scriptObject = { 177 | label, 178 | source: 'print "Hellow World!"', 179 | runtime_name: 'python_library_v5.0' 180 | }; 181 | 182 | return user.connection.Script 183 | .please() 184 | .create(scriptObject) 185 | .then(() => { 186 | user.scriptName = label; 187 | return user; 188 | }) 189 | .catch((error) => console.error('Script error:\n', error.message)); 190 | }; 191 | 192 | export default createScript; 193 | ``` 194 | 195 | Now we have a way to create a script. Neat! We are almost done with the data creation! 196 | 197 | As before, we append the user with `scriptName`. This way we will be able to save it later to a file. 198 | 199 | It's time to modify `createTestData.js` once more. Just replace it with: 200 | 201 | ```javascript 202 | import createConnection from './createConnection'; 203 | import createInstance from './createInstance'; 204 | import createScript from './createScript'; 205 | 206 | createConnection() 207 | .then((user) => createInstance(user)) 208 | .then((user) => createScript(user)) 209 | .then((user) => { 210 | delete user.connection; 211 | console.log('Your test setup:\n', user); 212 | }) 213 | .catch((error) => console.log('Global error:\n', error)); 214 | ``` 215 | 216 | As before, we are just chaining the `createScript` method after `createInstance`. 217 | 218 | Great! We have created a simple tool to connect to our account and to create a test `Instance` and a test `script`. 219 | 220 | Now we can start testing... But wait! We are missing two very important things. We should somehow save our `instanceName` and `scriptName` so that `Nightwatch` tests can use them. We should also do some cleanup after the tests are finished! 221 | 222 | Right now we need to export our variables to a file, so create a `saveVariables.js` file in the `scripts` folder. Append it with: 223 | 224 | ```javascript 225 | import fs from 'fs'; 226 | 227 | const saveVariables = (data) => { 228 | const fileName = 'tempInstance.js'; 229 | const variableFile = fs.createWriteStream(`./${fileName}`); 230 | const json = JSON.stringify(data); 231 | 232 | variableFile.write('export default ' + json + ';'); 233 | console.log(`\n> File saved as ${fileName}`); 234 | }; 235 | 236 | export default saveVariables; 237 | ``` 238 | By simply converting our object to JSON and doing a small trick in `'export default ' + json + ';'` we create a JavaScript file that can be easily imported in our tests! 239 | 240 | We also need to append the `createTestData.js` file with the newly created `saveVariables.js` file: 241 | 242 | ```javascript 243 | import createConnection from './createConnection'; 244 | import createInstance from './createInstance'; 245 | import createScript from './createScript'; 246 | import saveVariables from './saveVariables'; 247 | 248 | createConnection() 249 | .then((user) => createInstance(user)) 250 | .then((user) => createScript(user)) 251 | .then((user) => { 252 | delete user.connection; 253 | console.log('Your test setup:\n', user); 254 | saveVariables(user); 255 | }) 256 | .catch((error) => console.log('Global error:\n', error)); 257 | ``` 258 | 259 | Now we have saved our test data in a file! We will use that file in our tests to get the `script` and `Instance` names. 260 | 261 | The only thing we’re missing is a cleanup routine. So let's create a script that will delete our `Instance` after we've used it to run our tests. 262 | 263 | We're gonna reuse the `createInstancje.js` file and create a new one with just small modifications, naming it `deleteInstancje.js`; 264 | 265 | ```javascript 266 | const deleteInstance = (user, instanceName) => { 267 | const instance = { 268 | name: instanceName 269 | }; 270 | return user.connection.Instance 271 | .please() 272 | .delete(instance) 273 | .then(() => console.log(`${instanceName} was deleted.`)) 274 | .catch((error) => console.error('Instance delete error:\n', error.message)); 275 | }; 276 | 277 | export default deleteInstance; 278 | ``` 279 | 280 | And let's create a main script that will do the cleanup. Start by naming it `cleanUp.js`. 281 | 282 | ```javascript 283 | import createConnection from './createConnection'; 284 | import deleteInstance from './deleteInstance'; 285 | import tempInstance from '../tempInstance'; 286 | 287 | createConnection() 288 | .then((user) => deleteInstance(user, tempInstance.instanceName)) 289 | .catch((error) => console.error('Cleanup error:\n', error.message)); 290 | ``` 291 | 292 | We created it in a similar fashion as `createTestData.js`. To execute it, just type `babel-node scripts/cleanUp.js` in the terminal. 293 | 294 | Everything is set up! Now we only need to write our tests in Nightwatch to see how our tool works with it. 295 | 296 | So let's get to the work! :mans_shoe: 297 | 298 | ## Using the JavaScript tool with Nightwatch tests 299 | 300 | > In this section, I'll use custom commands that we use on a daily basis to speed up the work and make the code easier to read. I won't explain how they are working. For more info on that, just check our [github repo](https://github.com/Syncano/syncano-testing-examples). 301 | 302 | > I have also slightly altered `nightwatch.json` and `package.json`, so be sure to check those. 303 | 304 | Our tool creates a testing Instance and a file with variables for us, but how can we use it? Let’s consider a case where we want to test our Script Endpoint socket in the Dashboard. The tests require that the user has a `script` (component) before he can make a Script Endpoint. How we can solve this issue? We could just create one more test case in our test suite for a Script Endpoint. But... it won't be good idea in the long run. We could easily duplicate code that way, creating unnecessary additional steps that take longer to execute, and we would have to do cleanup after each test suite. 305 | 306 | That's why we have created our tool. First create the file `testScriptEndpoint.js` in the tests folder. This is what the test code should look like: 307 | 308 | ```javascript 309 | import tempInstance from '../tempInstance'; 310 | 311 | export default { 312 | before: (client) => { 313 | const loginPage = client.page.loginPage(); 314 | 315 | loginPage 316 | .navigate() 317 | .login(process.env.EMAIL, process.env.PASSWORD); 318 | }, 319 | after: (client) => client.end(), 320 | 'User adds Script Endpoint socket': (client) => { 321 | const scriptEndpointsPage = client.page.scriptEndpointsPage(); 322 | const scriptEndpointName = 'testScriptEndpoint'; 323 | 324 | scriptEndpointsPage 325 | .navigate() 326 | .clickElement('@scriptEndpointZeroStateAddButton') 327 | .fillInput('@scriptEndpointModalNameInput', scriptEndpointName) 328 | .selectDropdownValue('@scriptEndpointModalDropdown', tempInstance.scriptName) 329 | .clickElement('@scriptEndpointModalNextButton') 330 | .clickElement('@scriptEndpointSummaryCloseButton') 331 | .waitForElementVisible('@scriptEndpointListItemRow'); 332 | } 333 | } 334 | ``` 335 | 336 | If you followed the previous parts of the series, the code should be familiar to you. But let's take a closer look at these two lines of code: 337 | 338 | ```javascript 339 | import tempInstance from '../tempInstance'; 340 | [...] 341 | .selectDropdownValue('@scriptEndpointModalDropdown', tempInstance.scriptName) 342 | [...] 343 | ``` 344 | 345 | As you can see, we have imported our `tempInstance.js` file that was generated using our tool, and there is where we have our scriptName. 346 | 347 | By referring to it by `tempInstance.scriptName`, we can get it's value and use it in our tests, just like in the snippet above. How cool is that!? Now we don't need to create additional test cases before the main test. 348 | 349 | Thanks to that, we have just created a Nightwatch test that will navigate to our `script-endpoints` page, fill required fields with data, and then select a script that we created using our tool! 350 | 351 | But that's not all, we still need to write all selectors for the Script Endpoint page. So let's create a file named `scriptEndpointsPage.js` in our `tests` folder. 352 | 353 | This is how I've created it. 354 | 355 | 356 | ```javascript 357 | import tempInstance from '../tempInstance'; 358 | 359 | export default { 360 | url: `https://dashboard.syncano.io/#/instances/${tempInstance.instanceName}/script-endpoints`, 361 | elements: { 362 | scriptEndpointZeroStateAddButton: { 363 | selector: '//*[@data-e2e="zero-state-add-button"]', 364 | locateStrategy: 'xpath' 365 | }, 366 | scriptEndpointModalNameInput: { 367 | selector: 'input[name="name"]' 368 | }, 369 | scriptEndpointModalDropdown: { 370 | selector: 'input[data-e2e="script-name"]' 371 | }, 372 | scriptEndpointUserOption: { 373 | selector: `[data-e2e=${tempInstance.scriptName}-user-option]` 374 | }, 375 | scriptEndpointModalNextButton: { 376 | selector: '[data-e2e="script-dialog-confirm-button"]' 377 | }, 378 | scriptEndpointSummaryCloseButton: { 379 | selector: '[data-e2e="script-endpoint-summary-dialog-close-button"]' 380 | }, 381 | scriptEndpointListItemRow: { 382 | selector: '[data-e2e="testscriptendpoint-script-socket-row"]' 383 | } 384 | } 385 | } 386 | ``` 387 | 388 | Most of this should be familiar to you from `Part 1` and `Part 2` of our series. 389 | 390 | But let's focus on both lines below: 391 | 392 | ```javascript 393 | import tempInstance from '../tempInstance'; 394 | [...] 395 | url: `https://dashboard.syncano.io/#/instances/${tempInstance.instanceName}/script-endpoints`, 396 | [...] 397 | ``` 398 | 399 | The first line is the same as in the `scriptEndpointsPage.js` file. We simply `import` the file with data used for the tests. As for the second line, you will see that we are referring to `instanceName` from the `tempInstance.js` file. 400 | 401 | Thanks to that, our tests are universal. Every time a new test Instance is created, we don't need to change the URL. Our navigate function, used in the tests, will know where to go. 402 | 403 | > You may also see that I have used different locators than the locators used in `Part 1` and `Part 2`. This is due to the fact that we are rewriting some parts of the Dashboard to include data-e2e attributes. This helps us target DOM objects easily and without any issues while performing tests. Locators are written as CSS selectors, omitting the type of tag that they are attached to. DOM can change easily, but with selectors like this we're spending less time on test maintenance. We'll discuss this approach in future blog posts. 404 | 405 | ## Running the tests 406 | 407 | Now we are ready to run tests using our tool and our newly created tests. 408 | 409 | To do that, you could use every command one by one, but by doing it that way you can forget some of the steps. 410 | 411 | Instead, just copy this to your terminal: 412 | 413 | > Don't forget to include your main Instance name in `globals.js`. It is not used in our test, but it is still necessary for others. 414 | 415 | ```sh 416 | babel-node scripts/createTestData.js \ 417 | && npm test -t tests/testScriptEndpoint.js \ 418 | && babel-node scripts/cleanUp.js 419 | ``` 420 | 421 | Neat! We just created a data-driven test using our tool! 422 | 423 | > The way tests are started in the above code is not the best way. We could create a test runner in bash to give us more control over tests, since here only one of the tests will start. But that is information for a future topic in our Nightwatch series. 424 | 425 | ## Summary 426 | 427 | In this article you've learned how to: 428 | 429 | - create a JavaScript tool for data-driven testing 430 | - create Nightwatch tests using that tool 431 | 432 | That's it for the third part of our "End-to-End Testing of React Apps with Nightwatch" series. 433 | Be sure to follow us for more parts to come! 434 | 435 | If you have any questions, or just want to say hi, drop us a line at [support@syncano.com](mailto:support@syncano.com). 436 | -------------------------------------------------------------------------------- /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/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/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/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/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-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 | -------------------------------------------------------------------------------- /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-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-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-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-3/pages/socketsPage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | elements: { 3 | instancesDropdown: { 4 | selector: '.instances-dropdown' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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/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/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/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-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-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/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/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/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 | --------------------------------------------------------------------------------