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.
Files changed (50) hide show
  1. package/CHANGELOG.md +71 -6
  2. package/docs/build/Appium.js +1 -1
  3. package/docs/build/Nightmare.js +4 -5
  4. package/docs/build/Playwright.js +75 -24
  5. package/docs/build/Protractor.js +1 -1
  6. package/docs/build/Puppeteer.js +3 -3
  7. package/docs/build/REST.js +20 -1
  8. package/docs/build/TestCafe.js +1 -1
  9. package/docs/build/WebDriver.js +3 -3
  10. package/docs/changelog.md +71 -6
  11. package/docs/data.md +5 -5
  12. package/docs/detox.md +2 -2
  13. package/docs/docker.md +11 -11
  14. package/docs/examples.md +4 -4
  15. package/docs/helpers/Appium.md +1 -1
  16. package/docs/helpers/Nightmare.md +4 -5
  17. package/docs/helpers/Playwright.md +81 -61
  18. package/docs/helpers/Protractor.md +1 -1
  19. package/docs/helpers/Puppeteer.md +1 -1
  20. package/docs/helpers/REST.md +9 -0
  21. package/docs/helpers/TestCafe.md +1 -1
  22. package/docs/helpers/WebDriver.md +1 -1
  23. package/docs/locators.md +2 -2
  24. package/docs/mobile-react-native-locators.md +2 -2
  25. package/docs/mobile.md +3 -3
  26. package/docs/pageobjects.md +3 -1
  27. package/docs/reports.md +3 -3
  28. package/docs/typescript.md +47 -5
  29. package/docs/webapi/fillField.mustache +1 -1
  30. package/lib/cli.js +16 -10
  31. package/lib/command/init.js +15 -7
  32. package/lib/command/interactive.js +6 -7
  33. package/lib/command/run.js +1 -1
  34. package/lib/config.js +6 -1
  35. package/lib/container.js +1 -1
  36. package/lib/helper/Nightmare.js +1 -1
  37. package/lib/helper/Playwright.js +40 -24
  38. package/lib/helper/REST.js +20 -1
  39. package/lib/helper/WebDriver.js +2 -2
  40. package/lib/interfaces/gherkin.js +17 -3
  41. package/lib/output.js +2 -2
  42. package/lib/plugin/allure.js +3 -7
  43. package/lib/plugin/autoDelay.js +1 -1
  44. package/lib/plugin/screenshotOnFail.js +1 -2
  45. package/lib/step.js +2 -1
  46. package/lib/within.js +1 -1
  47. package/lib/workers.js +13 -1
  48. package/package.json +6 -6
  49. package/typings/index.d.ts +7 -2
  50. 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
- * {permalink: /'foo'} matches `<div id="foo">`
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 `{ permalink: /'user' }` finds element with id="user"
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: /'//*[@view-tag="someButton"]', ios: '~someButton'})
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: /`//*[@view-tag="${id}"]`,
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: /'android.widget.Button',
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: /'//android.widget.Button', ios: '//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'});
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
-
@@ -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
- "attachments": true //add screenshot for a failed test
363
+ "mochaFile": "./output/result.xml",
364
+ "attachments": true //add screenshot for a failed test
365
+ }
366
366
  }
367
367
  }
368
368
  }
@@ -19,10 +19,10 @@ Example:
19
19
 
20
20
  ![Quick Info](/img/Quick_info.gif)
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 {string} value text value to fill.
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
- output.stepShift = 3;
81
- const printMetaStep = (metaStep) => {
82
- if (!metaStep) return currentMetaStep.shift();
83
- if (currentMetaStep.indexOf(metaStep.toString()) >= 0) return; // step is the same
84
- if (metaStep.metaStep) {
85
- printMetaStep(metaStep.metaStep);
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
- currentMetaStep.unshift(metaStep.toString());
88
- output.step(metaStep);
89
- };
90
- printMetaStep(step.metaStep);
94
+ }
95
+ currentMetaStep = metaSteps;
96
+ output.stepShift = 3 + 2 * shift;
91
97
  output.step(step);
92
98
  });
93
99
 
@@ -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("npm i " + packages.join(' ') + ' --save-dev');
217
+ print(`npm i ${packages.join(' ')} --save-dev`);
210
218
  print();
211
219
  print('Things to do after missing packages installed:');
212
- print('☑ Generate type definitions', colors.green('npx codeceptjs def'));
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' and re-install codeceptjs:");
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
- codecept.runBootstrap((err) => {
18
- if (err) {
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
  };
@@ -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
- throw new Error(`Can not load config from ${jsConfig} or ${jsonConfig}\nCodeceptJS is not initialized in this dir. Execute 'codeceptjs init' to start`);
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("WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation");
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}`);
@@ -803,7 +803,7 @@ class Nightmare extends Helper {
803
803
  }
804
804
 
805
805
  /**
806
- * {{> grabValueFrom }}
806
+ * {{> grabValueFromAll }}
807
807
  */
808
808
  async grabValueFromAll(locator) {
809
809
  locator = new Locator(locator, 'css');
@@ -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.$('body');
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.mainFrame().$('body');
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
- * Checks that title is equal to provided one.
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 res = await Promise.all(els.map(el => el.$eval('xpath=.', el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
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
- // playwright combined selectors
1776
- const waiter = context.waitForSelector(`${buildLocatorString(locator)} >> __disabled=false`, { timeout: waitTimeout });
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
- // uses a custom selector engine for finding value properties on elements
1790
- const waiter = context.waitForSelector(`${buildLocatorString(locator)} >> __value=${value}`, { timeout: waitTimeout, state: 'visible' });
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 = await this.page.mainFrame().$('body');
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 [Pupeteer's reference](https://github.com/microsoft/Playwright/blob/master/docs/api.md#pagewaitfornavigationoptions)
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 = context;
2452
+ this.context = page;
2437
2453
  });
2438
2454
  });
2439
2455
  page.on('console', (msg) => {
@@ -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(request));
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
@@ -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 awailable in W3C specification');
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) step = step.metaStep; // assign metastep to metastep for nested steps
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
- event.dispatcher.on(event.step.before, setMetaStep);
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 (!step.isMetaStep() && step.hasBDDAncestor()) {
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) {
@@ -261,13 +261,9 @@ module.exports = (config) => {
261
261
  if (isHookSteps === false) {
262
262
  startMetaStep(step.metaStep);
263
263
  if (currentStep !== step) {
264
- // The reason we need to do this cause
265
- // the step actor contains this and hence
266
- // the allure reporter could not parse and
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
  }