codeceptjs 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/README.md +15 -22
- package/bin/codecept.js +3 -1
- package/docs/advanced.md +1 -1
- package/docs/angular.md +6 -9
- package/docs/basics.md +388 -86
- package/docs/bdd.md +4 -3
- package/docs/build/Nightmare.js +3 -0
- package/docs/build/Polly.js +26 -12
- package/docs/build/Puppeteer.js +14 -13
- package/docs/build/TestCafe.js +101 -2
- package/docs/build/WebDriver.js +53 -52
- package/docs/changelog.md +86 -57
- package/docs/detox.md +235 -0
- package/docs/helpers/Detox.md +579 -0
- package/docs/helpers/Polly.md +13 -3
- package/docs/helpers/Puppeteer.md +155 -156
- package/docs/helpers/TestCafe.md +53 -0
- package/docs/helpers/WebDriver.md +209 -204
- package/docs/locators.md +2 -0
- package/docs/mobile.md +5 -1
- package/docs/puppeteer.md +59 -13
- package/docs/quickstart.md +47 -12
- package/docs/testcafe.md +157 -0
- package/docs/webdriver.md +453 -0
- package/lib/command/definitions.js +152 -7
- package/lib/command/gherkin/snippets.js +19 -8
- package/lib/command/init.js +30 -22
- package/lib/command/utils.js +1 -1
- package/lib/container.js +36 -10
- package/lib/data/dataScenarioConfig.js +18 -0
- package/lib/helper/Nightmare.js +3 -0
- package/lib/helper/Polly.js +26 -12
- package/lib/helper/Puppeteer.js +14 -13
- package/lib/helper/TestCafe.js +72 -2
- package/lib/helper/WebDriver.js +53 -52
- package/lib/helper/testcafe/testcafe-utils.js +3 -2
- package/lib/interfaces/scenarioConfig.js +2 -2
- package/lib/listener/config.js +2 -2
- package/lib/plugin/allure.js +3 -0
- package/lib/step.js +5 -2
- package/lib/ui.js +1 -1
- package/lib/utils.js +13 -21
- package/package.json +14 -12
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: webdriver
|
|
3
|
+
title: Testing with WebDriver
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
How does your client, manager, or tester, or any other non-technical person, know your web application is working? By opening the browser, accessing a site, clicking on links, filling in the forms, and actually seeing the content on a web page.
|
|
7
|
+
|
|
8
|
+
End to End tests can cover standard but complex scenarios from a user's perspective. With e2e tests you can be confident that users, following all defined scenarios, won't get errors. We check **functionality of application and a user interface** (UI) as well.
|
|
9
|
+
|
|
10
|
+
## What is Selenium WebDriver
|
|
11
|
+
|
|
12
|
+
The standard and proved way to run browser test automation over years is Selenium WebDriver. Over years this technology was standartized and works over all popular browsers and operating systems. There are cloud services like SauceLabs or BrowserStack which allow executing such browsers in the clooud. The superset of WebDriver protocol is also used to test [native and hybrid mobile applications](https://codecept.io/mobile).
|
|
13
|
+
|
|
14
|
+
Let's clarify the terms:
|
|
15
|
+
|
|
16
|
+
* Selenium - is a toolset for browser test automation
|
|
17
|
+
* WebDriver - a standard protocol for communicating between test framework and browsers
|
|
18
|
+
* JSON Wire - an older version of such protocol
|
|
19
|
+
|
|
20
|
+
We use [webdriverio](https://webdriver.io) library to run tests over WebDriver.
|
|
21
|
+
|
|
22
|
+
> Popular tool [Protractor](https://codecept.io/angular) also uses WebDriver for running end 2 end tests.
|
|
23
|
+
|
|
24
|
+
To proceed you need to have [CodeceptJS installed](https://codecept.io/quickstart#using-selenium-webdriver) and `WebDriver` helper selected.
|
|
25
|
+
|
|
26
|
+
Selenium WebDriver may be complicated from start, as it requires following tools to be installed and started.
|
|
27
|
+
|
|
28
|
+
1. Selenium Server - to execute and send commands to browser
|
|
29
|
+
2. ChromeDriver or GeckoDriver - to allow browsers to run in automated mode.
|
|
30
|
+
|
|
31
|
+
> Those tools can be easily installed via NPM. Use [selenium-standalone](https://www.npmjs.com/package/selenium-standalone) to automatically install them.
|
|
32
|
+
|
|
33
|
+
You can also use `@wdio/selenium-standalone-service` package, to install and start Selenium Server for your tests automatically.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
npm i @wdio/selenium-standalone-service --save-dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Enable it in config inside plugins section:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
exports.config = {
|
|
43
|
+
// ...
|
|
44
|
+
// inside condecept.conf.js
|
|
45
|
+
plugins: {
|
|
46
|
+
wdio: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
services: ['selenium-standalone']
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuring WebDriver
|
|
55
|
+
|
|
56
|
+
WebDriver can be configured to run browser tests in window, headlessly, on a remote server or in a cloud.
|
|
57
|
+
|
|
58
|
+
> By default CodeceptJS is already configured to run WebDriver tests locally with Chrome or Firefox. If you just need to start running tests - proceed to the next chapter.
|
|
59
|
+
|
|
60
|
+
Configuration for WebDriver should be provided inside `codecept.conf.js` file under `helpers: WebDriver` section:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
helpers: {
|
|
64
|
+
WebDriver: {
|
|
65
|
+
url: 'https://myapp.com',
|
|
66
|
+
browser: 'chrome',
|
|
67
|
+
host: '127.0.0.1',
|
|
68
|
+
port: 4444,
|
|
69
|
+
restart: false,
|
|
70
|
+
windowSize: '1920x1680',
|
|
71
|
+
desiredCapabilities: {
|
|
72
|
+
chromeOptions: {
|
|
73
|
+
args: [ /*"--headless",*/ "--disable-gpu", "--window-size=1200,1000", "--no-sandbox" ]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
WebDriver protocol works over HTTP, so you need to have a Selenium Server to be running or other service that will launch a browser for you. That's why you may need to specify `host`, `port`, `protocol`, and `path` parameters.
|
|
81
|
+
|
|
82
|
+
By default, those parameters are set to connect to local Selenium Server but they should be changed if you want to run tests via [Cloud Services](https://codecept.io/helpers/WebDriver#cloud-providers). You may also need `user` and `key` parameters to authenticate on cloud service.
|
|
83
|
+
|
|
84
|
+
If you want to run tests using raw ChromeDriver (which also supports WebDriver protocol) avoiding Selenium Server, you should provide following configuration:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
port: 9515,
|
|
88
|
+
browser: 'chrome',
|
|
89
|
+
path: '/',
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> If you face issues connecting to WebDriver, please check that corresponding server is running on a specified port. If host is other than `localhost` or port is other than `4444`, update the configuration.
|
|
93
|
+
|
|
94
|
+
Additional parameters for a specific browser can be set via `desiredCapabilities` options. For instance, this is how we can set to **run headless Chrome**:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
desiredCapabilities: {
|
|
98
|
+
chromeOptions: {
|
|
99
|
+
args: [ "--headless", "--disable-gpu", "--window-size=1200,1000", "--no-sandbox" ]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Next popular use case for capabilities is configuring what to do with unhandled alert popups.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
desiredCapabilities: {
|
|
108
|
+
// close all unexpected popups
|
|
109
|
+
unexpectedAlertBehaviour: 'dismiss',
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
There are also [browser and platform specific capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Services like SauceLabs, BrowserStack or browser vendors can provide their own specific capabilities for more tuning.
|
|
114
|
+
|
|
115
|
+
Here is a sample BrowserStack config for running tests on iOS mobile browser:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
helpers: {
|
|
119
|
+
WebDriver: {
|
|
120
|
+
host: 'hub.browserstack.com',
|
|
121
|
+
path: '/wd/hub',
|
|
122
|
+
url: 'http://WEBSITE:8080/renderer',
|
|
123
|
+
user: 'xx', // credentials
|
|
124
|
+
key: 'xx', // credentials
|
|
125
|
+
browser: 'iphone',
|
|
126
|
+
desiredCapabilities: {
|
|
127
|
+
'os_version' : '11',
|
|
128
|
+
'device' : 'iPhone 8', // you can select device
|
|
129
|
+
'real_mobile' : 'true', // real or emulated
|
|
130
|
+
'browserstack.local' : 'true',
|
|
131
|
+
'browserstack.debug' : 'true',
|
|
132
|
+
'browserstack.networkLogs' : 'true',
|
|
133
|
+
'browserstack.appium_version' : '1.9.1',
|
|
134
|
+
'browserstack.user' : 'xx', // credentials
|
|
135
|
+
'browserstack.key' : 'xx' // credentials
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
There are also options specific to CodeceptJS. By default CodeceptJS runs tests in the same browser window but clears cookies and local storage after each test. This behavior can be changed with these options:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// change to true to restart browser between tests
|
|
144
|
+
restart: false,
|
|
145
|
+
// don't change browser state and not clear cookies between tests
|
|
146
|
+
keepBrowserState: true,
|
|
147
|
+
keepCookies: true,
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> ▶ More config options available on [WebDriver helper reference](https://codecept.io/helpers/WebDriver#configuration)
|
|
151
|
+
|
|
152
|
+
### Configuring CI
|
|
153
|
+
|
|
154
|
+
To develop tests it's fine to use local Selenium Server and window mode. Setting up WebDriver on remote CI (Continous Integration) server is different. If there is no desktop and no window mode on CI.
|
|
155
|
+
|
|
156
|
+
There are following options available:
|
|
157
|
+
|
|
158
|
+
* Use headless Chrome or Firefox (see configuration above).
|
|
159
|
+
* Use [Selenoid](https://codecept.io/helpers/WebDriver#selenoid-options) to run browsers inside Docker containers.
|
|
160
|
+
* Use paid [cloud services (SauceLabs, BrowserStack, TestingBot)](https://codecept.io/helpers/WebDriver#cloud-providers).
|
|
161
|
+
|
|
162
|
+
## Writing Tests
|
|
163
|
+
|
|
164
|
+
CodeceptJS provides high-level API on top of WebDriver protocol. While most standard implementations focus on dealing with WebElements on page, CodeceptJS is about user scenarios and interactions. That's why you don't have a direct access to web elements inside a test, but it is proved that in majority of cases you don't need it. Tests written from user's perspective are simpler to write, understand, log and debug.
|
|
165
|
+
|
|
166
|
+
> If you come from Java, Python or Ruby don't be afraid of a new syntax. It is more flexible than you think!
|
|
167
|
+
|
|
168
|
+
A typical test case may look like this:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
Feature('login');
|
|
172
|
+
|
|
173
|
+
Scenario('login test', (I) => {
|
|
174
|
+
I.amOnPage('/login');
|
|
175
|
+
I.fillField('Username', 'john');
|
|
176
|
+
I.fillField('Password', '123456');
|
|
177
|
+
I.click('Login');
|
|
178
|
+
I.see('Welcome, John');
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
> ▶ Actions like `amOnPage`, `click`, `fillField` are not limited to WebDriver only. They work similarly for all available helpers. [Go to Basics guide to learn them](https://codecept.io/basics#writing-tests).
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
An empty test case can be created with `codeceptjs gt` command.
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
npx codeceptjs gt
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
It's easy to start writing a test if you use [interactive pause](https://codecept.io/basics#debug). Just open a web page and pause execution.
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
Feature('Sample Test');
|
|
194
|
+
|
|
195
|
+
Scenario('open my website', (I) => {
|
|
196
|
+
I.amOnPage('/');
|
|
197
|
+
pause();
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This is just enough to run a test, open a browser, and think what to do next to write a test case.
|
|
202
|
+
|
|
203
|
+
Let's execute a test.
|
|
204
|
+
|
|
205
|
+
When you execute such test with `codeceptjs run` command you may see the browser is started
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
npx codeceptjs run --steps
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
After a page is opened a full control of a browser is given to a terminal. Type in different commands such as `click`, `see`, `fillField` to write the test. A successful commands will be saved to `./output/cli-history` file and can be copied into a test.
|
|
212
|
+
|
|
213
|
+
> ℹ All actions are listed in [WebDriver helper reference](https://codecept.io/helpers/WebDriver).
|
|
214
|
+
|
|
215
|
+
An interactive shell output may look like this:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
Interactive shell started
|
|
219
|
+
Use JavaScript syntax to try steps in action
|
|
220
|
+
- Press ENTER to run the next step
|
|
221
|
+
- Press TAB twice to see all available commands
|
|
222
|
+
- Type exit + Enter to exit the interactive shell
|
|
223
|
+
I.fillField('.new-todo', 'Write a test')
|
|
224
|
+
I.pressKey('Enter')
|
|
225
|
+
I.
|
|
226
|
+
Commands have been saved to /home/davert/demos/codeceptjs/output/cli-history
|
|
227
|
+
```
|
|
228
|
+
After typing in successful commands you can copy them into a test.
|
|
229
|
+
|
|
230
|
+
Here is a test checking basic [todo application](http://todomvc.com/).
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
Feature('TodoMVC');
|
|
234
|
+
|
|
235
|
+
Scenario('create todo item', (I) => {
|
|
236
|
+
I.amOnPage('/examples/vue/');
|
|
237
|
+
I.waitForElement('.new-todo');
|
|
238
|
+
I.fillField('.new-todo', 'Write a test')
|
|
239
|
+
I.pressKey('Enter');
|
|
240
|
+
I.see('1 item left', '.todo-count');
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> [▶ Working example of CodeceptJS WebDriver tests](https://github.com/DavertMik/codeceptjs-webdriver-example) for TodoMVC application.
|
|
245
|
+
|
|
246
|
+
## Waiting
|
|
247
|
+
|
|
248
|
+
Web applications do not always respond instantly. That's why WebDriver protocol has methods to wait for changes on a page. CodeceptJS provides such commands prefixed with `wait*` so you could explicitly define what effects we wait for.
|
|
249
|
+
|
|
250
|
+
Most popular "waiters" are:
|
|
251
|
+
|
|
252
|
+
* `waitForText` - wait for text to appear on a page
|
|
253
|
+
* `waitForElement` - wait for element to appear on a page
|
|
254
|
+
* `waitForInvisible` - wait element to become invisible.
|
|
255
|
+
|
|
256
|
+
By default, they will wait for 1 second. This number can be changed in WebDriver configuration:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
// inside codecept.conf.js
|
|
260
|
+
exports.config = {
|
|
261
|
+
helpers: {
|
|
262
|
+
WebDriver: {
|
|
263
|
+
// WebDriver config goes here
|
|
264
|
+
// wait for 5 seconds
|
|
265
|
+
waitForTimeout: 5000
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## SmartWait
|
|
272
|
+
|
|
273
|
+
It is possible to wait for elements pragmatically. If a test uses element which is not on a page yet, CodeceptJS will wait for few extra seconds before failing. This feature is based on [Implicit Wait](http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp#implicit-waits) of Selenium. CodeceptJS enables implicit wait only when searching for a specific element and disables in all other cases. Thus, the performance of a test is not affected.
|
|
274
|
+
|
|
275
|
+
SmartWait can be enabled by setting wait option in WebDriver config.
|
|
276
|
+
Add `smartWait: 5000` to wait for additional 5s.
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
// inside codecept.conf.js
|
|
280
|
+
exports.config = {
|
|
281
|
+
helpers: {
|
|
282
|
+
WebDriver: {
|
|
283
|
+
// WebDriver config goes here
|
|
284
|
+
// smart wait for 5 seconds
|
|
285
|
+
smartWait: 5000
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
SmartWait works with a CSS/XPath locators in `click`, `seeElement` and other methods. See where it is enabled and where is not:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
I.click('Login'); // DISABLED, not a locator
|
|
295
|
+
I.fillField('user', 'davert'); // DISABLED, not a specific locator
|
|
296
|
+
I.fillField({name: 'password'}, '123456'); // ENABLED, strict locator
|
|
297
|
+
I.click('#login'); // ENABLED, locator is CSS ID
|
|
298
|
+
I.see('Hello, Davert'); // DISABLED, Not a locator
|
|
299
|
+
I.seeElement('#userbar'); // ENABLED
|
|
300
|
+
I.dontSeeElement('#login'); // DISABLED, can't wait for element to hide
|
|
301
|
+
I.seeNumberOfElements('button.link', 5); // DISABLED, can wait only for one element
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
SmartWait doesn't check element for visibility, so tests may fail even element is on a page.
|
|
306
|
+
|
|
307
|
+
Usage example:
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
// we use smartWait: 5000 instead of
|
|
311
|
+
// I.waitForElement('#click-me', 5);
|
|
312
|
+
// to wait for element on page
|
|
313
|
+
I.click('#click-me');
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
If it's hard to define what to wait, it is recommended to use [retries](https://codecept.io/basics/#retries) to rerun flaky steps.
|
|
317
|
+
|
|
318
|
+
## Auto Login
|
|
319
|
+
|
|
320
|
+
To share the same user session across different tests CodeceptJS provides [autoLogin plugin](https://codecept.io/plugins#autologin). It simplifies login management and reduces time consuming login operations. Instead of filling in login form before each test it saves the cookies of a valid user session and reuses it for next tests. If a session expires or doesn't exist, logs in a user again.
|
|
321
|
+
|
|
322
|
+
This plugin requires some configuration but is very simple in use:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
Scenario('do something with logged in user', (I, login)) => {
|
|
326
|
+
login('user');
|
|
327
|
+
I.see('Dashboard','h1');
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
With `autoLogin` plugin you can save cookies into a file and reuse same session on different runs.
|
|
332
|
+
|
|
333
|
+
> [▶ How to set up autoLogin plugin](https://codecept.io/plugins#autologin)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
## Multiple Windows
|
|
337
|
+
|
|
338
|
+
CodeceptJS allows to use several browser windows inside a test. Sometimes we are testing the functionality of websites that we cannot control, such as a closed-source managed package, and there are popups that either remain open for configuring data on the screen, or close as a result of clicking a window. We can use these functions in order to gain more control over which page is being tested with Codecept at any given time. For example:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
const assert = require('assert');
|
|
342
|
+
|
|
343
|
+
Scenario('should open main page of configured site, open a popup, switch to main page, then switch to popup, close popup, and go back to main page', async (I) => {
|
|
344
|
+
I.amOnPage('/');
|
|
345
|
+
const handleBeforePopup = await I.grabCurrentWindowHandle();
|
|
346
|
+
const urlBeforePopup = await I.grabCurrentUrl();
|
|
347
|
+
const allHandlesBeforePopup = await I.grabAllWindowHandles();
|
|
348
|
+
assert.equal(allHandlesBeforePopup.length, 1, 'Single Window');
|
|
349
|
+
|
|
350
|
+
await I.executeScript(() => {
|
|
351
|
+
window.open('https://www.w3schools.com/', 'new window', 'toolbar=yes,scrollbars=yes,resizable=yes,width=400,height=400');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const allHandlesAfterPopup = await I.grabAllWindowHandles();
|
|
355
|
+
assert.equal(allHandlesAfterPopup.length, 2, 'Two Windows');
|
|
356
|
+
|
|
357
|
+
await I.switchToWindow(allHandlesAfterPopup[1]);
|
|
358
|
+
const urlAfterPopup = await I.grabCurrentUrl();
|
|
359
|
+
assert.equal(urlAfterPopup, 'https://www.w3schools.com/', 'Expected URL: Popup');
|
|
360
|
+
|
|
361
|
+
assert.equal(handleBeforePopup, allHandlesAfterPopup[0], 'Expected Window: Main Window');
|
|
362
|
+
await I.switchToWindow(handleBeforePopup);
|
|
363
|
+
const currentURL = await I.grabCurrentUrl();
|
|
364
|
+
assert.equal(currentURL, urlBeforePopup, 'Expected URL: Main URL');
|
|
365
|
+
|
|
366
|
+
await I.switchToWindow(allHandlesAfterPopup[1]);
|
|
367
|
+
const urlAfterSwitchBack = await I.grabCurrentUrl();
|
|
368
|
+
assert.equal(urlAfterSwitchBack, 'https://www.w3schools.com/', 'Expected URL: Popup');
|
|
369
|
+
await I.closeCurrentTab();
|
|
370
|
+
|
|
371
|
+
const allHandlesAfterPopupClosed = await I.grabAllWindowHandles();
|
|
372
|
+
assert.equal(allHandlesAfterPopupClosed.length, 1, 'Single Window');
|
|
373
|
+
const currentWindowHandle = await I.grabCurrentWindowHandle();
|
|
374
|
+
assert.equal(currentWindowHandle, allHandlesAfterPopup[0], 'Expected Window: Main Window');
|
|
375
|
+
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Extending WebDriver
|
|
380
|
+
|
|
381
|
+
CodeceptJS doesn't aim to embrace all possible functionality of WebDriver. At some points you may find that some actions do not exist, however it is easy to add one. You will need to use WebDriver API from [webdriver.io](https://webdriver.io) library.
|
|
382
|
+
|
|
383
|
+
To create new actions which will be added into `I.` object you need to create a new helper. This can be done with `codeceptjs gh` command.
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
npx codeceptjs gh
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Name a new helper "Web". Now each method of a created class can be added to I object. Be sure to enable this helper in config:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
exports.config = {
|
|
393
|
+
helpers: {
|
|
394
|
+
WebDriver: { /* WebDriver config goes here */ },
|
|
395
|
+
WebHelper: {
|
|
396
|
+
// load custom helper
|
|
397
|
+
require: './web_helper.js'
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
> ℹ See [Custom Helper](https://codecept.io/helpers) guide to see more examples.
|
|
404
|
+
|
|
405
|
+
While implementing custom actions using WebDriver API please note that, there is two versions of protocol: WebDriver and JSON Wire. Depending on a browser version one of those protocols can be used. We can't know for sure which protocol is going to used, so we will need to implement an action using both APIs.
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
const Helper = codeceptjs.helper;
|
|
409
|
+
|
|
410
|
+
class Web extends Helper {
|
|
411
|
+
|
|
412
|
+
// method to drag an item to coordinates
|
|
413
|
+
async dragToPoint(el, x, y) {
|
|
414
|
+
// access browser object from WebDriver
|
|
415
|
+
const browser = this.helpers.WebDriver.browser;
|
|
416
|
+
await this.helpers.WebDriver.moveCursorTo(el);
|
|
417
|
+
|
|
418
|
+
if (browser.isW3C) {
|
|
419
|
+
// we use WebDriver protocol
|
|
420
|
+
return browser.performActions([
|
|
421
|
+
{"type": "pointerDown", "button": 0},
|
|
422
|
+
{"type": "pointerMove", "origin": "pointer", "duration": 1000, x, y },
|
|
423
|
+
{"type": "pointerUp", "button": 0}
|
|
424
|
+
]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// we use JSON Wire protocol
|
|
428
|
+
await browser.buttonDown(0);
|
|
429
|
+
await browser.moveToElement(null, x, y);
|
|
430
|
+
await browser.buttonUp(0);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// method which restarts browser
|
|
434
|
+
async restartBrowser() {
|
|
435
|
+
const browser = this.helpers.WebDriver.browser;
|
|
436
|
+
await browser.reloadSession();
|
|
437
|
+
await browser.maximizeWindow();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// method which goes to previous page
|
|
441
|
+
async backToPreviousPage() {
|
|
442
|
+
const browser = this.helpers.WebDriver.browser;
|
|
443
|
+
await browser.back();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
When a helper is created, regenerate your step definitions, so you could see those actions when using [intellisense](https://codecept.io/basics#intellisense):
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
npx codeceptjs def
|
|
452
|
+
```
|
|
453
|
+
|
|
@@ -9,7 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
11
|
const template = `
|
|
12
|
-
type ICodeceptCallback = (i
|
|
12
|
+
type ICodeceptCallback = (i?: CodeceptJS.{{I}}, current?:any{{callbackParams}}) => void;
|
|
13
13
|
|
|
14
14
|
declare class FeatureConfig {
|
|
15
15
|
retry(times: number): FeatureConfig
|
|
@@ -41,6 +41,134 @@ interface ILocator {
|
|
|
41
41
|
|
|
42
42
|
type LocatorOrString = string | ILocator | Locator;
|
|
43
43
|
|
|
44
|
+
declare class Container {
|
|
45
|
+
create(config: Object, opts: Object): void
|
|
46
|
+
plugins(name?: string): Object
|
|
47
|
+
support(name?: string): Object
|
|
48
|
+
helpers(name?: string): Object
|
|
49
|
+
translation(): Object
|
|
50
|
+
mocha(): Object
|
|
51
|
+
append(newContainer: Object): void
|
|
52
|
+
clear(newHelpers: Object, newSupport: Object, newPlugins: Object): void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare class RecorderSession {
|
|
56
|
+
running: boolean
|
|
57
|
+
start(name: string): void
|
|
58
|
+
restore(name: string): void
|
|
59
|
+
catch(fn: CallableFunction): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare class Recorder {
|
|
63
|
+
retries: Object[]
|
|
64
|
+
start(): void
|
|
65
|
+
isRunning(): boolean
|
|
66
|
+
startUnlessRunning(): void
|
|
67
|
+
errHandler(fn: CallableFunction): void
|
|
68
|
+
reset(): void
|
|
69
|
+
session: RecorderSession
|
|
70
|
+
add(taskName, fn?: CallableFunction, force?: boolean, retry?: boolean): Promise<any>
|
|
71
|
+
retry(opts: Object): Promise<any>
|
|
72
|
+
catch(customErrFn: CallableFunction): Promise<any>
|
|
73
|
+
catchWithoutStop(customErrFn: CallableFunction ): Promise<any>
|
|
74
|
+
throw(err: Error): Promise<any>
|
|
75
|
+
saveFirstAsyncError(err: Error): void
|
|
76
|
+
getAsyncErr(): Promise<Error>
|
|
77
|
+
cleanAsyncErr(): void
|
|
78
|
+
stop():void
|
|
79
|
+
promise(): Promise<any>
|
|
80
|
+
scheduled(): string[]
|
|
81
|
+
toString(): string
|
|
82
|
+
add(hookName: string, fn: CallableFunction, force?: boolean): void
|
|
83
|
+
catch(customErrFn: CallableFunction)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare class CodeceptJSEvent {
|
|
87
|
+
dispatcher: EventEmitter
|
|
88
|
+
test: {
|
|
89
|
+
started: string
|
|
90
|
+
before: string
|
|
91
|
+
after: string
|
|
92
|
+
passed: string
|
|
93
|
+
failed: string
|
|
94
|
+
finished: string
|
|
95
|
+
}
|
|
96
|
+
suite: {
|
|
97
|
+
before: string,
|
|
98
|
+
after: string,
|
|
99
|
+
}
|
|
100
|
+
hook: {
|
|
101
|
+
started: string,
|
|
102
|
+
passed: string,
|
|
103
|
+
}
|
|
104
|
+
step: {
|
|
105
|
+
before: string,
|
|
106
|
+
after: string,
|
|
107
|
+
started: string,
|
|
108
|
+
passed: string,
|
|
109
|
+
failed: string,
|
|
110
|
+
finished: string,
|
|
111
|
+
}
|
|
112
|
+
all: {
|
|
113
|
+
before: string,
|
|
114
|
+
after: string,
|
|
115
|
+
result: string,
|
|
116
|
+
}
|
|
117
|
+
multiple: {
|
|
118
|
+
before: string,
|
|
119
|
+
after: string,
|
|
120
|
+
}
|
|
121
|
+
emit(event: string, param: string)
|
|
122
|
+
cleanDispatcher(): void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare class Output {
|
|
126
|
+
colors: any
|
|
127
|
+
styles: {
|
|
128
|
+
error: any,
|
|
129
|
+
success: any,
|
|
130
|
+
scenario: any,
|
|
131
|
+
basic: any,
|
|
132
|
+
debug: any,
|
|
133
|
+
log: any,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
print(msg: string): void
|
|
137
|
+
stepShift: number
|
|
138
|
+
level(level: number): number
|
|
139
|
+
process(process: string): string
|
|
140
|
+
debug(msg: string): void
|
|
141
|
+
log(msg: string): void
|
|
142
|
+
error(msg: string): void
|
|
143
|
+
success(msg: string): void
|
|
144
|
+
plugin(name: string, msg: string): void
|
|
145
|
+
step(step: any): void
|
|
146
|
+
suite: {
|
|
147
|
+
started: Function
|
|
148
|
+
}
|
|
149
|
+
test: {
|
|
150
|
+
started(test: string): void
|
|
151
|
+
passed(test: string): void
|
|
152
|
+
failed(test: string): void
|
|
153
|
+
skipped(test: string): void
|
|
154
|
+
}
|
|
155
|
+
scenario: {
|
|
156
|
+
started(test: string): void
|
|
157
|
+
passed(test: string): void
|
|
158
|
+
failed(test: string): void
|
|
159
|
+
}
|
|
160
|
+
say(message: string, color?: string): void
|
|
161
|
+
result(passed: number, failed: number, skipped: number, duration: string): void
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
declare class Config {
|
|
165
|
+
create(newConfig: Object): Object
|
|
166
|
+
load(configFile: string): Config
|
|
167
|
+
get(key: string, val: any): any
|
|
168
|
+
append(additionalConfig: Object): Object
|
|
169
|
+
reset(): Object
|
|
170
|
+
}
|
|
171
|
+
|
|
44
172
|
declare class Helper {
|
|
45
173
|
/** Abstract method to provide required config options */
|
|
46
174
|
static _config(): any;
|
|
@@ -129,7 +257,9 @@ declare function BeforeSuite(callback: ICodeceptCallback): void;
|
|
|
129
257
|
declare function After(callback: ICodeceptCallback): void;
|
|
130
258
|
declare function AfterSuite(callback: ICodeceptCallback): void;
|
|
131
259
|
|
|
132
|
-
declare function inject(): {
|
|
260
|
+
declare function inject(): {
|
|
261
|
+
{{injectPageObjects}}
|
|
262
|
+
};
|
|
133
263
|
declare function locate(selector: LocatorOrString): Locator;
|
|
134
264
|
declare function within(selector: LocatorOrString, callback: Function): Promise<any>;
|
|
135
265
|
declare function session(selector: LocatorOrString, callback: Function): Promise<any>;
|
|
@@ -140,6 +270,12 @@ declare function secret(secret: any): string;
|
|
|
140
270
|
declare const codeceptjs: any;
|
|
141
271
|
|
|
142
272
|
declare namespace CodeceptJS {
|
|
273
|
+
export const container: Container
|
|
274
|
+
export const recorder: Recorder
|
|
275
|
+
export const event: CodeceptJSEvent
|
|
276
|
+
export const output: Output
|
|
277
|
+
export const config: Config
|
|
278
|
+
|
|
143
279
|
export interface {{I}} {
|
|
144
280
|
{{methods}}
|
|
145
281
|
}
|
|
@@ -151,6 +287,8 @@ declare module "codeceptjs" {
|
|
|
151
287
|
}
|
|
152
288
|
`;
|
|
153
289
|
|
|
290
|
+
const injectSupportTemplate = ' {{name}}: CodeceptJS.{{name}}';
|
|
291
|
+
|
|
154
292
|
const pageObjectTemplate = `
|
|
155
293
|
export interface {{name}} {
|
|
156
294
|
{{methods}}
|
|
@@ -181,19 +319,26 @@ module.exports = function (genPath, options) {
|
|
|
181
319
|
|
|
182
320
|
const supports = container.support(); // return all support objects
|
|
183
321
|
const exportPageObjects = [];
|
|
322
|
+
const injectPageObjects = [];
|
|
184
323
|
const callbackParams = [];
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
324
|
+
// See #1795 and #1799, there's no obvious way to use for-in with Proxies
|
|
325
|
+
Reflect.ownKeys(supports).forEach((name) => {
|
|
326
|
+
if (name === 'I') {
|
|
327
|
+
injectPageObjects.push(injectSupportTemplate.replace(/{{name}}/g, String(name)));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
callbackParams.push(`${String(name)}?:CodeceptJS.${String(name)}`);
|
|
188
331
|
const pageObject = supports[name];
|
|
189
332
|
const pageMethods = addAllMethodsInObject(pageObject, {}, []);
|
|
190
333
|
let pageObjectExport = pageObjectTemplate.replace('{{methods}}', pageMethods.join(''));
|
|
191
|
-
pageObjectExport = pageObjectExport.replace('{{name}}', name);
|
|
334
|
+
pageObjectExport = pageObjectExport.replace('{{name}}', String(name));
|
|
335
|
+
injectPageObjects.push(injectSupportTemplate.replace(/{{name}}/g, String(name)));
|
|
192
336
|
exportPageObjects.push(pageObjectExport);
|
|
193
|
-
}
|
|
337
|
+
});
|
|
194
338
|
|
|
195
339
|
let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
|
|
196
340
|
definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
|
|
341
|
+
definitionsTemplate = definitionsTemplate.replace('{{injectPageObjects}}', injectPageObjects.join('\n'));
|
|
197
342
|
definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${[...callbackParams, '...args: any'].join(', ')}`);
|
|
198
343
|
if (translations) {
|
|
199
344
|
definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, translations.I);
|