├── .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 | [](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 /div[@class="description-field col-flex-1"]> 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 |
--------------------------------------------------------------------------------