codeceptjs 3.0.3-beta.7 → 3.0.5
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 +71 -6
- package/docs/build/Appium.js +1 -1
- package/docs/build/Nightmare.js +4 -5
- package/docs/build/Playwright.js +75 -24
- package/docs/build/Protractor.js +1 -1
- package/docs/build/Puppeteer.js +3 -3
- package/docs/build/REST.js +20 -1
- package/docs/build/TestCafe.js +1 -1
- package/docs/build/WebDriver.js +3 -3
- package/docs/changelog.md +71 -6
- package/docs/data.md +5 -5
- package/docs/detox.md +2 -2
- package/docs/docker.md +11 -11
- package/docs/examples.md +4 -4
- package/docs/helpers/Appium.md +1 -1
- package/docs/helpers/Nightmare.md +4 -5
- package/docs/helpers/Playwright.md +81 -61
- package/docs/helpers/Protractor.md +1 -1
- package/docs/helpers/Puppeteer.md +1 -1
- package/docs/helpers/REST.md +9 -0
- package/docs/helpers/TestCafe.md +1 -1
- package/docs/helpers/WebDriver.md +1 -1
- package/docs/locators.md +2 -2
- package/docs/mobile-react-native-locators.md +2 -2
- package/docs/mobile.md +3 -3
- package/docs/pageobjects.md +3 -1
- package/docs/reports.md +3 -3
- package/docs/typescript.md +47 -5
- package/docs/webapi/fillField.mustache +1 -1
- package/lib/cli.js +16 -10
- package/lib/command/init.js +15 -7
- package/lib/command/interactive.js +6 -7
- package/lib/command/run.js +1 -1
- package/lib/config.js +6 -1
- package/lib/container.js +1 -1
- package/lib/helper/Nightmare.js +1 -1
- package/lib/helper/Playwright.js +40 -24
- package/lib/helper/REST.js +20 -1
- package/lib/helper/WebDriver.js +2 -2
- package/lib/interfaces/gherkin.js +17 -3
- package/lib/output.js +2 -2
- package/lib/plugin/allure.js +3 -7
- package/lib/plugin/autoDelay.js +1 -1
- package/lib/plugin/screenshotOnFail.js +1 -2
- package/lib/step.js +2 -1
- package/lib/within.js +1 -1
- package/lib/workers.js +13 -1
- package/package.json +6 -6
- package/typings/index.d.ts +7 -2
- package/typings/types.d.ts +55 -21
package/docs/locators.md
CHANGED
|
@@ -21,7 +21,7 @@ If the locator is an object, it should have a single element, with the key signi
|
|
|
21
21
|
|
|
22
22
|
Examples:
|
|
23
23
|
|
|
24
|
-
* {
|
|
24
|
+
* {id: 'foo'} matches `<div id="foo">`
|
|
25
25
|
* {name: 'foo'} matches `<div name="foo">`
|
|
26
26
|
* {css: 'input[type=input][value=foo]'} matches `<input type="input" value="foo">`
|
|
27
27
|
* {xpath: "//input[@type='submit'][contains(@value, 'foo')]"} matches `<input type="submit" value="foobar">`
|
|
@@ -229,7 +229,7 @@ locate('button').after('.btn-cancel');
|
|
|
229
229
|
|
|
230
230
|
ID locators are best to select the exact semantic element in web and mobile testing:
|
|
231
231
|
|
|
232
|
-
* `#user` or `{
|
|
232
|
+
* `#user` or `{ id: 'user' }` finds element with id="user"
|
|
233
233
|
* `~user` finds element with accessibility id "user" (in Mobile testing) or with `aria-label=user`.
|
|
234
234
|
|
|
235
235
|
## Custom Locators
|
|
@@ -46,14 +46,14 @@ You could do it just by changing `automationName` in the `helpers` section of th
|
|
|
46
46
|
```
|
|
47
47
|
Then you could locate components using XPath expression:
|
|
48
48
|
```js
|
|
49
|
-
I.tap({android:
|
|
49
|
+
I.tap({android: '//*[@view-tag="someButton"]', ios: '~someButton'})
|
|
50
50
|
```
|
|
51
51
|
This way test would work for both platforms without any changes in code.
|
|
52
52
|
To simplify things further you could write a helper function:
|
|
53
53
|
```js
|
|
54
54
|
function tid(id) {
|
|
55
55
|
return {
|
|
56
|
-
android:
|
|
56
|
+
android: `//*[@view-tag="${id}"]`,
|
|
57
57
|
ios: '~' + id
|
|
58
58
|
}
|
|
59
59
|
}
|
package/docs/mobile.md
CHANGED
|
@@ -19,7 +19,7 @@ I.see('davert@codecept.io', '~email of the customer'));
|
|
|
19
19
|
I.clearField('~email of the customer'));
|
|
20
20
|
I.dontSee('Nothing special', '~email of the customer'));
|
|
21
21
|
I.seeElement({
|
|
22
|
-
android:
|
|
22
|
+
android: 'android.widget.Button',
|
|
23
23
|
ios: '//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'
|
|
24
24
|
});
|
|
25
25
|
```
|
|
@@ -118,6 +118,7 @@ helpers: {
|
|
|
118
118
|
app: "bs://<hashed app-id>",
|
|
119
119
|
host: "hub-cloud.browserstack.com",
|
|
120
120
|
port: 4444,
|
|
121
|
+
platform: "ios",
|
|
121
122
|
user: "BROWSERSTACK_USER",
|
|
122
123
|
key: "BROWSERSTACK_KEY",
|
|
123
124
|
device: "iPhone 7"
|
|
@@ -262,7 +263,7 @@ It is often happen that mobile applications behave similarly on different platfo
|
|
|
262
263
|
CodeceptJS provides a way to specify different locators for Android and iOS platforms:
|
|
263
264
|
|
|
264
265
|
```js
|
|
265
|
-
I.click({android:
|
|
266
|
+
I.click({android: '//android.widget.Button', ios: '//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'});
|
|
266
267
|
```
|
|
267
268
|
|
|
268
269
|
In case some code should be executed on one platform and ignored on others use `runOnAndroid` and `runOnIOS` methods:
|
|
@@ -293,4 +294,3 @@ Just as you can specify android, and ios-specific locators, you can do so for we
|
|
|
293
294
|
```js
|
|
294
295
|
I.click({web: '#login', ios: '//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'});
|
|
295
296
|
```
|
|
296
|
-
|
package/docs/pageobjects.md
CHANGED
|
@@ -32,7 +32,9 @@ const { I, myPage, mySteps } = inject();
|
|
|
32
32
|
// inject objects for a test by name
|
|
33
33
|
Scenario('sample test', ({ I, myPage, mySteps }) => {
|
|
34
34
|
// ...
|
|
35
|
-
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
36
38
|
## Actor
|
|
37
39
|
|
|
38
40
|
During initialization you were asked to create a custom steps file. If you accepted this option, you are now able to use the `custom_steps.js` file to extend `I`. See how the `login` method can be added to `I`:
|
package/docs/reports.md
CHANGED
|
@@ -360,9 +360,9 @@ Configure mocha-multi with reports that you want:
|
|
|
360
360
|
"mocha-junit-reporter": {
|
|
361
361
|
"stdout": "./output/console.log",
|
|
362
362
|
"options": {
|
|
363
|
-
"mochaFile": "./output/result.xml"
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
"mochaFile": "./output/result.xml",
|
|
364
|
+
"attachments": true //add screenshot for a failed test
|
|
365
|
+
}
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
}
|
package/docs/typescript.md
CHANGED
|
@@ -19,10 +19,10 @@ Example:
|
|
|
19
19
|
|
|
20
20
|

|
|
21
21
|
|
|
22
|
-
- Checks types - thanks to TypeScript support in CodeceptJS now allow to tests your tests. TypeScript can prevent some errors:
|
|
22
|
+
- Checks types - thanks to TypeScript support in CodeceptJS now allow to tests your tests. TypeScript can prevent some errors:
|
|
23
23
|
- invalid type of variables passed to function;
|
|
24
24
|
- calls no-exist method from PageObject or `I` object;
|
|
25
|
-
- incorrectly used CodeceptJS features;
|
|
25
|
+
- incorrectly used CodeceptJS features;
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
## Getting Started
|
|
@@ -106,7 +106,7 @@ declare namespace CodeceptJS {
|
|
|
106
106
|
|
|
107
107
|
## Types for custom helper or page object
|
|
108
108
|
|
|
109
|
-
If you want to get types for your [custom helper](https://codecept.io/helpers/#configuration), you can add their automatically with CodeceptJS command `npx codeceptjs def`.
|
|
109
|
+
If you want to get types for your [custom helper](https://codecept.io/helpers/#configuration), you can add their automatically with CodeceptJS command `npx codeceptjs def`.
|
|
110
110
|
|
|
111
111
|
For example, if you add the new step `printMessage` for your custom helper like this:
|
|
112
112
|
```js
|
|
@@ -121,9 +121,9 @@ export = CustomHelper
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
Then you need to add this helper to your `codecept.conf.js` like in this [docs](https://codecept.io/helpers/#configuration).
|
|
124
|
-
And then run the command `npx codeceptjs def`.
|
|
124
|
+
And then run the command `npx codeceptjs def`.
|
|
125
125
|
|
|
126
|
-
As result our `steps.d.ts` file will be updated like this:
|
|
126
|
+
As result our `steps.d.ts` file will be updated like this:
|
|
127
127
|
```ts
|
|
128
128
|
/// <reference types='codeceptjs' />
|
|
129
129
|
type CustomHelper = import('./CustomHelper');
|
|
@@ -156,3 +156,45 @@ declare namespace CodeceptJS {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
```
|
|
159
|
+
|
|
160
|
+
## Types for custom strict locators
|
|
161
|
+
|
|
162
|
+
You can define [custom strict locators](https://codecept.io/locators/#custom-strict-locators) that can be used in all methods taking a locator (parameter type `LocatorOrString`).
|
|
163
|
+
|
|
164
|
+
Example: A custom strict locator with a `data` property, which can be used like this:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
I.click({ data: 'user-login' });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
In order to use the custom locator in TypeScript code, its type shape needs to be registered in the interface `CustomLocators` in your `steps.d.ts` file:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
/// <reference types='codeceptjs' />
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
declare namespace CodeceptJS {
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
interface CustomLocators {
|
|
180
|
+
data: { data: string };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The property keys used in the `CustomLocators` interface do not matter (only the *types* of the interface properties are used). For simplicity it is recommended to use the name that is also used in your custom locator itself.
|
|
186
|
+
|
|
187
|
+
You can also define more complicated custom locators with multiple (also optional) properties:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
/// <reference types='codeceptjs' />
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
declare namespace CodeceptJS {
|
|
194
|
+
...
|
|
195
|
+
|
|
196
|
+
interface CustomLocators {
|
|
197
|
+
data: { data: string, value?: number, flag?: boolean };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
@@ -12,4 +12,4 @@ I.fillField('form#login input[name=username]', 'John');
|
|
|
12
12
|
I.fillField({css: 'form#login input[name=username]'}, 'John');
|
|
13
13
|
```
|
|
14
14
|
@param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
|
|
15
|
-
@param {
|
|
15
|
+
@param {CodeceptJS.StringOrSecret} value text value to fill.
|
package/lib/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ const ms = require('ms');
|
|
|
3
3
|
const event = require('./event');
|
|
4
4
|
const AssertionFailedError = require('./assert/error');
|
|
5
5
|
const output = require('./output');
|
|
6
|
+
const { MetaStep } = require('./step');
|
|
6
7
|
|
|
7
8
|
const cursor = Base.cursor;
|
|
8
9
|
let currentMetaStep = [];
|
|
@@ -77,17 +78,22 @@ class Cli extends Base {
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
event.dispatcher.on(event.step.started, (step) => {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
let processingStep = step;
|
|
82
|
+
const metaSteps = [];
|
|
83
|
+
while (processingStep.metaStep) {
|
|
84
|
+
metaSteps.unshift(processingStep.metaStep);
|
|
85
|
+
processingStep = processingStep.metaStep;
|
|
86
|
+
}
|
|
87
|
+
const shift = metaSteps.length;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
|
|
90
|
+
if (currentMetaStep[i] !== metaSteps[i]) {
|
|
91
|
+
output.stepShift = 3 + 2 * i;
|
|
92
|
+
if (metaSteps[i]) output.step(metaSteps[i]);
|
|
86
93
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
printMetaStep(step.metaStep);
|
|
94
|
+
}
|
|
95
|
+
currentMetaStep = metaSteps;
|
|
96
|
+
output.stepShift = 3 + 2 * shift;
|
|
91
97
|
output.step(step);
|
|
92
98
|
});
|
|
93
99
|
|
package/lib/command/init.js
CHANGED
|
@@ -143,7 +143,7 @@ module.exports = function (initPath) {
|
|
|
143
143
|
error(err);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const finish = () => {
|
|
146
|
+
const finish = async () => {
|
|
147
147
|
// create steps file by default
|
|
148
148
|
const stepFile = './steps_file.js';
|
|
149
149
|
fs.writeFileSync(path.join(testsPath, stepFile), defaultActor);
|
|
@@ -196,21 +196,29 @@ module.exports = function (initPath) {
|
|
|
196
196
|
print(`Intellisense enabled in ${jsconfigFile}`);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
const generateDefinitionsManually = colors.bold(`To get auto-completion support, please generate type definitions: ${colors.green('npx codeceptjs def')}`);
|
|
200
|
+
|
|
199
201
|
if (packages) {
|
|
200
202
|
try {
|
|
201
203
|
install(packages);
|
|
202
|
-
print(colors.green.bold(`Packages ${packages} installed successfully`));
|
|
203
|
-
print('To get autocompletion generate type definitions', colors.green('npx codeceptjs def'))
|
|
204
204
|
|
|
205
|
+
if (testsPath) {
|
|
206
|
+
print(generateDefinitionsManually);
|
|
207
|
+
} else {
|
|
208
|
+
const { code } = spawn.sync('npx', ['codeceptjs', 'def']);
|
|
209
|
+
if (code !== 0) {
|
|
210
|
+
print(generateDefinitionsManually);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
205
213
|
} catch (err) {
|
|
206
214
|
print(colors.bold.red(err.toString()));
|
|
207
215
|
print();
|
|
208
216
|
print(colors.bold.red('Please install next packages manually:'));
|
|
209
|
-
print(
|
|
217
|
+
print(`npm i ${packages.join(' ')} --save-dev`);
|
|
210
218
|
print();
|
|
211
219
|
print('Things to do after missing packages installed:');
|
|
212
|
-
print('☑
|
|
213
|
-
print('☑ Create first test', colors.green('npx codeceptjs gt'));
|
|
220
|
+
print('☑', generateDefinitionsManually);
|
|
221
|
+
print('☑ Create first test:', colors.green('npx codeceptjs gt'));
|
|
214
222
|
print(colors.bold.magenta('Find more information at https://codecept.io'));
|
|
215
223
|
return;
|
|
216
224
|
}
|
|
@@ -259,7 +267,7 @@ function install(dependencies, verbose) {
|
|
|
259
267
|
|
|
260
268
|
if (!fs.existsSync(path.join(process.cwd(), 'package.json'))) {
|
|
261
269
|
dependencies.push('codeceptjs');
|
|
262
|
-
throw new Error("Error: 'package.json' file not found. Generate it with 'npm init -y'
|
|
270
|
+
throw new Error("Error: 'package.json' file not found. Generate it with 'npm init -y' command.");
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
if (!installedLocally()) {
|
|
@@ -4,7 +4,7 @@ const Codecept = require('../codecept');
|
|
|
4
4
|
const event = require('../event');
|
|
5
5
|
const output = require('../output');
|
|
6
6
|
|
|
7
|
-
module.exports = function (path, options) {
|
|
7
|
+
module.exports = async function (path, options) {
|
|
8
8
|
// Backward compatibility for --profile
|
|
9
9
|
process.profile = options.profile;
|
|
10
10
|
process.env.profile = options.profile;
|
|
@@ -14,11 +14,8 @@ module.exports = function (path, options) {
|
|
|
14
14
|
const codecept = new Codecept(config, options);
|
|
15
15
|
codecept.init(testsPath);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
output.error(`Error while running bootstrap file :${err}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
17
|
+
try {
|
|
18
|
+
await codecept.bootstrap();
|
|
22
19
|
|
|
23
20
|
if (options.verbose) output.level(3);
|
|
24
21
|
|
|
@@ -36,5 +33,7 @@ module.exports = function (path, options) {
|
|
|
36
33
|
recorder.add(() => event.emit(event.suite.after, {}));
|
|
37
34
|
recorder.add(() => event.emit(event.all.result, {}));
|
|
38
35
|
recorder.add(() => codecept.teardown());
|
|
39
|
-
})
|
|
36
|
+
} catch (err) {
|
|
37
|
+
output.error(`Error while running bootstrap file :${err}`);
|
|
38
|
+
}
|
|
40
39
|
};
|
package/lib/command/run.js
CHANGED
|
@@ -19,9 +19,9 @@ module.exports = async function (test, options) {
|
|
|
19
19
|
createOutputDir(config, testRoot);
|
|
20
20
|
|
|
21
21
|
const codecept = new Codecept(config, options);
|
|
22
|
-
codecept.init(testRoot);
|
|
23
22
|
|
|
24
23
|
try {
|
|
24
|
+
codecept.init(testRoot);
|
|
25
25
|
await codecept.bootstrap();
|
|
26
26
|
codecept.loadTests();
|
|
27
27
|
await codecept.run(test);
|
package/lib/config.js
CHANGED
|
@@ -77,7 +77,12 @@ class Config {
|
|
|
77
77
|
return loadConfigFile(jsonConfig);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
const tsConfig = path.join(configFile, 'codecept.conf.ts');
|
|
81
|
+
if (isFile(tsConfig)) {
|
|
82
|
+
return loadConfigFile(tsConfig);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`Can not load config from ${jsConfig} or ${jsonConfig} or ${tsConfig}\nCodeceptJS is not initialized in this dir. Execute 'codeceptjs init' to start`);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/**
|
package/lib/container.js
CHANGED
|
@@ -171,7 +171,7 @@ function createHelpers(config) {
|
|
|
171
171
|
if (require('./utils').installedLocally()) {
|
|
172
172
|
install = `npm install --save-dev ${requirements.join(' ')}`;
|
|
173
173
|
} else {
|
|
174
|
-
console.log(
|
|
174
|
+
console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
|
|
175
175
|
install = `[sudo] npm install -g ${requirements.join(' ')}`;
|
|
176
176
|
}
|
|
177
177
|
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
|
package/lib/helper/Nightmare.js
CHANGED
package/lib/helper/Playwright.js
CHANGED
|
@@ -488,7 +488,7 @@ class Playwright extends Helper {
|
|
|
488
488
|
this.page = page;
|
|
489
489
|
if (!page) return;
|
|
490
490
|
page.setDefaultNavigationTimeout(this.options.getPageTimeout);
|
|
491
|
-
this.context = await this.page
|
|
491
|
+
this.context = await this.page;
|
|
492
492
|
if (this.config.browser === 'chrome') {
|
|
493
493
|
await page.bringToFront();
|
|
494
494
|
}
|
|
@@ -617,7 +617,7 @@ class Playwright extends Helper {
|
|
|
617
617
|
|
|
618
618
|
async _withinEnd() {
|
|
619
619
|
this.withinLocator = null;
|
|
620
|
-
this.context = await this.page
|
|
620
|
+
this.context = await this.page;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
@@ -800,11 +800,7 @@ class Playwright extends Helper {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
/**
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* ```js
|
|
806
|
-
* I.seeTitleEquals('Test title.');
|
|
807
|
-
* ```
|
|
803
|
+
* {{> seeTitleEquals }}
|
|
808
804
|
*/
|
|
809
805
|
async seeTitleEquals(text) {
|
|
810
806
|
const title = await this.page.title();
|
|
@@ -1069,14 +1065,7 @@ class Playwright extends Helper {
|
|
|
1069
1065
|
}
|
|
1070
1066
|
|
|
1071
1067
|
/**
|
|
1072
|
-
*
|
|
1073
|
-
* Force clicks an element without waiting for it to become visible and not animating.
|
|
1074
|
-
*
|
|
1075
|
-
* ```js
|
|
1076
|
-
* I.forceClick('#hiddenButton');
|
|
1077
|
-
* I.forceClick('Click me', '#hidden');
|
|
1078
|
-
* ```
|
|
1079
|
-
*
|
|
1068
|
+
* {{> forceClick }}
|
|
1080
1069
|
*/
|
|
1081
1070
|
async forceClick(locator, context = null) {
|
|
1082
1071
|
return proceedClick.call(this, locator, context, { force: true });
|
|
@@ -1500,6 +1489,10 @@ class Playwright extends Helper {
|
|
|
1500
1489
|
* I.executeScript(([x, y]) => x + y, [x, y]);
|
|
1501
1490
|
* ```
|
|
1502
1491
|
* If a function returns a Promise it will wait for its resolution.
|
|
1492
|
+
*
|
|
1493
|
+
* @param {string|function} fn function to be executed in browser context.
|
|
1494
|
+
* @param {any} [arg] optional argument to pass to the function
|
|
1495
|
+
* @return {Promise<any>}
|
|
1503
1496
|
*/
|
|
1504
1497
|
async executeScript(fn, arg) {
|
|
1505
1498
|
let context = this.page;
|
|
@@ -1590,8 +1583,7 @@ class Playwright extends Helper {
|
|
|
1590
1583
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1591
1584
|
const els = await this._locate(locator);
|
|
1592
1585
|
this.debug(`Matched ${els.length} elements`);
|
|
1593
|
-
const
|
|
1594
|
-
const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
|
|
1586
|
+
const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
1595
1587
|
|
|
1596
1588
|
return cssValues;
|
|
1597
1589
|
}
|
|
@@ -1771,9 +1763,21 @@ class Playwright extends Helper {
|
|
|
1771
1763
|
async waitForEnabled(locator, sec) {
|
|
1772
1764
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1773
1765
|
locator = new Locator(locator, 'css');
|
|
1766
|
+
const matcher = await this.context;
|
|
1767
|
+
let waiter;
|
|
1774
1768
|
const context = await this._getContext();
|
|
1775
|
-
|
|
1776
|
-
|
|
1769
|
+
if (!locator.isXPath()) {
|
|
1770
|
+
const valueFn = function ([locator]) {
|
|
1771
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0;
|
|
1772
|
+
};
|
|
1773
|
+
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout });
|
|
1774
|
+
} else {
|
|
1775
|
+
const enabledFn = function ([locator, $XPath]) {
|
|
1776
|
+
eval($XPath); // eslint-disable-line no-eval
|
|
1777
|
+
return $XPath(null, locator).filter(el => !el.disabled).length > 0;
|
|
1778
|
+
};
|
|
1779
|
+
waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
|
|
1780
|
+
}
|
|
1777
1781
|
return waiter.catch((err) => {
|
|
1778
1782
|
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
1779
1783
|
});
|
|
@@ -1785,9 +1789,21 @@ class Playwright extends Helper {
|
|
|
1785
1789
|
async waitForValue(field, value, sec) {
|
|
1786
1790
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1787
1791
|
const locator = new Locator(field, 'css');
|
|
1792
|
+
const matcher = await this.context;
|
|
1793
|
+
let waiter;
|
|
1788
1794
|
const context = await this._getContext();
|
|
1789
|
-
|
|
1790
|
-
|
|
1795
|
+
if (!locator.isXPath()) {
|
|
1796
|
+
const valueFn = function ([locator, value]) {
|
|
1797
|
+
return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
|
|
1798
|
+
};
|
|
1799
|
+
waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout });
|
|
1800
|
+
} else {
|
|
1801
|
+
const valueFn = function ([locator, $XPath, value]) {
|
|
1802
|
+
eval($XPath); // eslint-disable-line no-eval
|
|
1803
|
+
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
|
|
1804
|
+
};
|
|
1805
|
+
waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], { timeout: waitTimeout });
|
|
1806
|
+
}
|
|
1791
1807
|
return waiter.catch((err) => {
|
|
1792
1808
|
const loc = locator.toString();
|
|
1793
1809
|
throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
@@ -2023,7 +2039,7 @@ class Playwright extends Helper {
|
|
|
2023
2039
|
return;
|
|
2024
2040
|
}
|
|
2025
2041
|
if (!locator) {
|
|
2026
|
-
this.context =
|
|
2042
|
+
this.context = this.page;
|
|
2027
2043
|
return;
|
|
2028
2044
|
}
|
|
2029
2045
|
|
|
@@ -2059,7 +2075,7 @@ class Playwright extends Helper {
|
|
|
2059
2075
|
/**
|
|
2060
2076
|
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
2061
2077
|
*
|
|
2062
|
-
* See [
|
|
2078
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2063
2079
|
*
|
|
2064
2080
|
* @param {*} opts
|
|
2065
2081
|
*/
|
|
@@ -2433,7 +2449,7 @@ async function targetCreatedHandler(page) {
|
|
|
2433
2449
|
}
|
|
2434
2450
|
// if context element was in iframe - keep it
|
|
2435
2451
|
// if (await this.context.ownerFrame()) return;
|
|
2436
|
-
this.context =
|
|
2452
|
+
this.context = page;
|
|
2437
2453
|
});
|
|
2438
2454
|
});
|
|
2439
2455
|
page.on('console', (msg) => {
|
package/lib/helper/REST.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const axios = require('axios').default;
|
|
2
|
+
const Secret = require('../secret');
|
|
2
3
|
|
|
3
4
|
const Helper = require('../helper');
|
|
4
5
|
|
|
@@ -75,12 +76,18 @@ class REST extends Helper {
|
|
|
75
76
|
* @param {*} request
|
|
76
77
|
*/
|
|
77
78
|
async _executeRequest(request) {
|
|
79
|
+
const _debugRequest = { ...request };
|
|
78
80
|
axios.defaults.timeout = request.timeout || this.options.timeout;
|
|
79
81
|
|
|
80
82
|
if (this.headers && this.headers.auth) {
|
|
81
83
|
request.auth = this.headers.auth;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
if (request.data instanceof Secret) {
|
|
87
|
+
_debugRequest.data = '*****';
|
|
88
|
+
request.data = typeof request.data === 'object' ? { ...request.data.toString() } : request.data.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
if ((typeof request.data) === 'string') {
|
|
85
92
|
if (!request.headers || !request.headers['Content-Type']) {
|
|
86
93
|
request.headers = { ...request.headers, ...{ 'Content-Type': 'application/x-www-form-urlencoded' } };
|
|
@@ -91,7 +98,7 @@ class REST extends Helper {
|
|
|
91
98
|
await this.config.onRequest(request);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
this.debugSection('Request', JSON.stringify(
|
|
101
|
+
this.debugSection('Request', JSON.stringify(_debugRequest));
|
|
95
102
|
|
|
96
103
|
let response;
|
|
97
104
|
try {
|
|
@@ -149,6 +156,10 @@ class REST extends Helper {
|
|
|
149
156
|
*
|
|
150
157
|
* ```js
|
|
151
158
|
* I.sendPostRequest('/api/users.json', { "email": "user@user.com" });
|
|
159
|
+
*
|
|
160
|
+
* // To mask the payload in logs
|
|
161
|
+
* I.sendPostRequest('/api/users.json', secret({ "email": "user@user.com" }));
|
|
162
|
+
*
|
|
152
163
|
* ```
|
|
153
164
|
*
|
|
154
165
|
* @param {*} url
|
|
@@ -176,6 +187,10 @@ class REST extends Helper {
|
|
|
176
187
|
*
|
|
177
188
|
* ```js
|
|
178
189
|
* I.sendPatchRequest('/api/users.json', { "email": "user@user.com" });
|
|
190
|
+
*
|
|
191
|
+
* // To mask the payload in logs
|
|
192
|
+
* I.sendPatchRequest('/api/users.json', secret({ "email": "user@user.com" }));
|
|
193
|
+
*
|
|
179
194
|
* ```
|
|
180
195
|
*
|
|
181
196
|
* @param {string} url
|
|
@@ -203,6 +218,10 @@ class REST extends Helper {
|
|
|
203
218
|
*
|
|
204
219
|
* ```js
|
|
205
220
|
* I.sendPutRequest('/api/users.json', { "email": "user@user.com" });
|
|
221
|
+
*
|
|
222
|
+
* // To mask the payload in logs
|
|
223
|
+
* I.sendPutRequest('/api/users.json', secret({ "email": "user@user.com" }));
|
|
224
|
+
*
|
|
206
225
|
* ```
|
|
207
226
|
*
|
|
208
227
|
* @param {string} url
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -718,7 +718,7 @@ class WebDriver extends Helper {
|
|
|
718
718
|
* @param {object} locator
|
|
719
719
|
*/
|
|
720
720
|
async _smartWait(locator) {
|
|
721
|
-
this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);
|
|
721
|
+
this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${JSON.stringify(locator)} in ${this.options.smartWait}`);
|
|
722
722
|
await this.defineTimeout({ implicit: this.options.smartWait });
|
|
723
723
|
}
|
|
724
724
|
|
|
@@ -1370,7 +1370,7 @@ class WebDriver extends Helper {
|
|
|
1370
1370
|
*/
|
|
1371
1371
|
async grabBrowserLogs() {
|
|
1372
1372
|
if (this.browser.isW3C) {
|
|
1373
|
-
this.debug('Logs not
|
|
1373
|
+
this.debug('Logs not available in W3C specification');
|
|
1374
1374
|
return;
|
|
1375
1375
|
}
|
|
1376
1376
|
return this.browser.getLogs('browser');
|
|
@@ -28,11 +28,16 @@ module.exports = (text) => {
|
|
|
28
28
|
|
|
29
29
|
const runSteps = async (steps) => {
|
|
30
30
|
for (const step of steps) {
|
|
31
|
-
event.emit(event.bddStep.before, step);
|
|
32
31
|
const metaStep = new Step.MetaStep(null, step.text);
|
|
33
32
|
metaStep.actor = step.keyword.trim();
|
|
34
33
|
const setMetaStep = (step) => {
|
|
35
|
-
if (step.metaStep)
|
|
34
|
+
if (step.metaStep) {
|
|
35
|
+
if (step.metaStep === metaStep) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setMetaStep(step.metaStep);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
36
41
|
step.metaStep = metaStep;
|
|
37
42
|
};
|
|
38
43
|
const fn = matchStep(step.text);
|
|
@@ -44,10 +49,19 @@ module.exports = (text) => {
|
|
|
44
49
|
if (step.argument.type === 'DataTable') metaStep.comment = `\n${transformTable(step.argument)}`;
|
|
45
50
|
if (step.argument.content) metaStep.comment = `\n${step.argument.content}`;
|
|
46
51
|
}
|
|
47
|
-
|
|
52
|
+
step.startTime = Date.now();
|
|
53
|
+
step.match = fn.line;
|
|
54
|
+
event.emit(event.bddStep.before, step);
|
|
55
|
+
event.dispatcher.prependListener(event.step.before, setMetaStep);
|
|
48
56
|
try {
|
|
49
57
|
await fn(...fn.params);
|
|
58
|
+
step.status = 'passed';
|
|
59
|
+
} catch (err) {
|
|
60
|
+
step.status = 'failed';
|
|
61
|
+
step.err = err;
|
|
62
|
+
throw err;
|
|
50
63
|
} finally {
|
|
64
|
+
step.endTime = Date.now();
|
|
51
65
|
event.dispatcher.removeListener(event.step.before, setMetaStep);
|
|
52
66
|
}
|
|
53
67
|
event.emit(event.bddStep.after, step);
|
package/lib/output.js
CHANGED
|
@@ -102,14 +102,14 @@ module.exports = {
|
|
|
102
102
|
if (!step) return;
|
|
103
103
|
// Avoid to print non-gherkin steps, when gherkin is running for --steps mode
|
|
104
104
|
if (outputLevel === 1) {
|
|
105
|
-
if (
|
|
105
|
+
if (step.hasBDDAncestor()) {
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
let stepLine = step.toString();
|
|
111
111
|
if (step.metaStep && outputLevel >= 1) {
|
|
112
|
-
this.stepShift += 2;
|
|
112
|
+
// this.stepShift += 2;
|
|
113
113
|
stepLine = colors.green(truncate(stepLine, this.spaceShift));
|
|
114
114
|
}
|
|
115
115
|
if (step.comment) {
|
package/lib/plugin/allure.js
CHANGED
|
@@ -261,13 +261,9 @@ module.exports = (config) => {
|
|
|
261
261
|
if (isHookSteps === false) {
|
|
262
262
|
startMetaStep(step.metaStep);
|
|
263
263
|
if (currentStep !== step) {
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
// generate the report
|
|
268
|
-
if (step.actor.includes('\u001b[36m')) {
|
|
269
|
-
step.actor = step.actor.replace('\u001b[36m', '').replace('\u001b[39m', '');
|
|
270
|
-
}
|
|
264
|
+
// In multi-session scenarios, actors' names will be highlighted with ANSI
|
|
265
|
+
// escape sequences which are invalid XML values
|
|
266
|
+
step.actor = step.actor.replace(ansiRegExp(), '');
|
|
271
267
|
reporter.startStep(step.toString());
|
|
272
268
|
currentStep = step;
|
|
273
269
|
}
|