codeceptjs 3.0.7 → 3.1.3

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 (57) hide show
  1. package/CHANGELOG.md +96 -2
  2. package/README.md +9 -1
  3. package/bin/codecept.js +27 -17
  4. package/docs/bdd.md +55 -1
  5. package/docs/build/Appium.js +76 -4
  6. package/docs/build/Playwright.js +186 -69
  7. package/docs/build/Protractor.js +2 -0
  8. package/docs/build/Puppeteer.js +56 -18
  9. package/docs/build/REST.js +12 -0
  10. package/docs/build/WebDriver.js +1 -3
  11. package/docs/changelog.md +96 -2
  12. package/docs/commands.md +21 -7
  13. package/docs/configuration.md +15 -2
  14. package/docs/helpers/Appium.md +96 -94
  15. package/docs/helpers/Playwright.md +259 -202
  16. package/docs/helpers/Puppeteer.md +17 -1
  17. package/docs/helpers/REST.md +23 -9
  18. package/docs/helpers/WebDriver.md +2 -2
  19. package/docs/mobile.md +2 -1
  20. package/docs/playwright.md +156 -6
  21. package/docs/plugins.md +61 -69
  22. package/docs/react.md +1 -1
  23. package/docs/reports.md +21 -3
  24. package/lib/actor.js +2 -3
  25. package/lib/codecept.js +13 -2
  26. package/lib/command/definitions.js +8 -1
  27. package/lib/command/run-multiple/collection.js +4 -0
  28. package/lib/config.js +1 -1
  29. package/lib/container.js +3 -3
  30. package/lib/data/dataTableArgument.js +35 -0
  31. package/lib/helper/Appium.js +49 -4
  32. package/lib/helper/Playwright.js +186 -69
  33. package/lib/helper/Protractor.js +2 -0
  34. package/lib/helper/Puppeteer.js +56 -18
  35. package/lib/helper/REST.js +12 -0
  36. package/lib/helper/WebDriver.js +1 -3
  37. package/lib/helper/errors/ConnectionRefused.js +1 -1
  38. package/lib/helper/extras/Popup.js +1 -1
  39. package/lib/helper/extras/React.js +44 -32
  40. package/lib/index.js +2 -0
  41. package/lib/interfaces/gherkin.js +8 -1
  42. package/lib/listener/exit.js +2 -4
  43. package/lib/listener/helpers.js +4 -4
  44. package/lib/locator.js +7 -0
  45. package/lib/mochaFactory.js +13 -9
  46. package/lib/output.js +2 -2
  47. package/lib/plugin/allure.js +7 -18
  48. package/lib/plugin/commentStep.js +1 -1
  49. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  50. package/lib/plugin/customLocator.js +2 -2
  51. package/lib/plugin/subtitles.js +88 -0
  52. package/lib/plugin/tryTo.js +1 -1
  53. package/lib/recorder.js +5 -3
  54. package/lib/step.js +4 -2
  55. package/package.json +4 -3
  56. package/typings/index.d.ts +2 -0
  57. package/typings/types.d.ts +158 -18
package/lib/codecept.js CHANGED
@@ -130,7 +130,16 @@ class Codecept {
130
130
  let patterns = [pattern];
131
131
  if (!pattern) {
132
132
  patterns = [];
133
- if (this.config.tests && !this.opts.features) patterns.push(this.config.tests);
133
+
134
+ // If the user wants to test a specific set of test files as an array or string.
135
+ if (this.config.tests && !this.opts.features) {
136
+ if (Array.isArray(this.config.tests)) {
137
+ patterns.push(...this.config.tests);
138
+ } else {
139
+ patterns.push(this.config.tests);
140
+ }
141
+ }
142
+
134
143
  if (this.config.gherkin.features && !this.opts.tests) {
135
144
  if (Array.isArray(this.config.gherkin.features)) {
136
145
  this.config.gherkin.features.forEach(feature => {
@@ -147,7 +156,9 @@ class Codecept {
147
156
  if (!fsPath.isAbsolute(file)) {
148
157
  file = fsPath.join(global.codecept_dir, file);
149
158
  }
150
- this.testFiles.push(fsPath.resolve(file));
159
+ if (!this.testFiles.includes(fsPath.resolve(file))) {
160
+ this.testFiles.push(fsPath.resolve(file));
161
+ }
151
162
  });
152
163
  }
153
164
  }
@@ -194,7 +194,14 @@ function getPath(originalPath, targetFolderPath, testsPath) {
194
194
  else if (parsedPath.ext === '.ts') parsedPath.base = parsedPath.name;
195
195
 
196
196
  if (!parsedPath.dir.startsWith('.')) return path.posix.join(parsedPath.dir, parsedPath.base);
197
- const relativePath = path.posix.relative(targetFolderPath, path.posix.join(testsPath, parsedPath.dir, parsedPath.base));
197
+ const relativePath = path.posix.relative(
198
+ targetFolderPath.split(path.sep).join(path.posix.sep),
199
+ path.posix.join(
200
+ testsPath.split(path.sep).join(path.posix.sep),
201
+ parsedPath.dir.split(path.sep).join(path.posix.sep),
202
+ parsedPath.base.split(path.sep).join(path.posix.sep),
203
+ ),
204
+ );
198
205
 
199
206
  return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
200
207
  }
@@ -80,6 +80,10 @@ class Collection {
80
80
  return;
81
81
  }
82
82
 
83
+ if (runConfig.gherkin && config.gherkin.features) {
84
+ patterns.push(runConfig.gherkin.features);
85
+ }
86
+
83
87
  createChunks(runConfig, patterns).forEach((runChunkConfig, index) => {
84
88
  const run = createRun(`${runName}:chunk${index + 1}`, runChunkConfig);
85
89
  run.setOriginalName(runName);
package/lib/config.js CHANGED
@@ -128,7 +128,7 @@ function loadConfigFile(configFile) {
128
128
  const extensionName = path.extname(configFile);
129
129
 
130
130
  // .conf.js config file
131
- if (extensionName === '.js' || extensionName === '.ts') {
131
+ if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
132
132
  return Config.create(require(configFile).config);
133
133
  }
134
134
 
package/lib/container.js CHANGED
@@ -215,7 +215,7 @@ function createSupportObjects(config) {
215
215
  newObj._init();
216
216
  }
217
217
  } catch (err) {
218
- throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}`);
218
+ throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}\n${err.stack}`);
219
219
  }
220
220
  return newObj;
221
221
  }
@@ -288,7 +288,7 @@ function createPlugins(config, options = {}) {
288
288
  }
289
289
  plugins[pluginName] = require(module)(config[pluginName]);
290
290
  } catch (err) {
291
- throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}`);
291
+ throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`);
292
292
  }
293
293
  }
294
294
  return plugins;
@@ -380,7 +380,7 @@ function loadSupportObject(modulePath, supportObjectName) {
380
380
 
381
381
  return obj;
382
382
  } catch (err) {
383
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}`);
383
+ throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
384
384
  }
385
385
  }
386
386
 
@@ -1,4 +1,9 @@
1
+ /**
2
+ * DataTableArgument class to store the Cucumber data table from
3
+ * a step as an object with methods that can be used to access the data.
4
+ */
1
5
  class DataTableArgument {
6
+ /** @param {*} gherkinDataTable */
2
7
  constructor(gherkinDataTable) {
3
8
  this.rawData = gherkinDataTable.rows.map((row) => {
4
9
  return row.cells.map((cell) => {
@@ -7,16 +12,25 @@ class DataTableArgument {
7
12
  });
8
13
  }
9
14
 
15
+ /** Returns the table as a 2-D array
16
+ * @returns {string[][]}
17
+ */
10
18
  raw() {
11
19
  return this.rawData.slice(0);
12
20
  }
13
21
 
22
+ /** Returns the table as a 2-D array, without the first row
23
+ * @returns {string[][]}
24
+ */
14
25
  rows() {
15
26
  const copy = this.raw();
16
27
  copy.shift();
17
28
  return copy;
18
29
  }
19
30
 
31
+ /** Returns an array of objects where each row is converted to an object (column header is the key)
32
+ * @returns {any[]}
33
+ */
20
34
  hashes() {
21
35
  const copy = this.raw();
22
36
  const header = copy.shift();
@@ -26,6 +40,27 @@ class DataTableArgument {
26
40
  return r;
27
41
  });
28
42
  }
43
+
44
+ /** Returns an object where each row corresponds to an entry
45
+ * (first column is the key, second column is the value)
46
+ * @returns {Record<string, string>}
47
+ */
48
+ rowsHash() {
49
+ const rows = this.raw();
50
+ const everyRowHasTwoColumns = rows.every((row) => row.length === 2);
51
+ if (!everyRowHasTwoColumns) {
52
+ throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns');
53
+ }
54
+ /** @type {Record<string, string>} */
55
+ const result = {};
56
+ rows.forEach((x) => (result[x[0]] = x[1]));
57
+ return result;
58
+ }
59
+
60
+ /** Transposed the data */
61
+ transpose() {
62
+ this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i]));
63
+ }
29
64
  }
30
65
 
31
66
  module.exports = DataTableArgument;
@@ -481,10 +481,11 @@ class Appium extends Webdriver {
481
481
  * ```js
482
482
  * I.removeApp('appName', 'com.example.android.apis');
483
483
  * ```
484
- * @param {string} appId
485
- * @param {string} bundleId String ID of bundle
486
484
  *
487
485
  * Appium: support only Android
486
+ *
487
+ * @param {string} appId
488
+ * @param {string} [bundleId] ID of bundle
488
489
  */
489
490
  async removeApp(appId, bundleId) {
490
491
  onlyForApps.call(this, 'Android');
@@ -820,9 +821,10 @@ class Appium extends Webdriver {
820
821
  * I.hideDeviceKeyboard('pressKey', 'Done');
821
822
  * ```
822
823
  *
823
- * @param {'tapOutside' | 'pressKey'} strategy desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
824
- *
825
824
  * Appium: support Android and iOS
825
+ *
826
+ * @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
827
+ * @param {string} [key] Optional key
826
828
  */
827
829
  async hideDeviceKeyboard(strategy, key) {
828
830
  onlyForApps.call(this);
@@ -1162,6 +1164,8 @@ class Appium extends Webdriver {
1162
1164
  * ```
1163
1165
  *
1164
1166
  * Appium: support Android and iOS
1167
+ *
1168
+ * @param {Array} actions Array of touch actions
1165
1169
  */
1166
1170
  async touchPerform(actions) {
1167
1171
  onlyForApps.call(this);
@@ -1352,6 +1356,33 @@ class Appium extends Webdriver {
1352
1356
  return super.grabTextFrom(parseLocator.call(this, locator));
1353
1357
  }
1354
1358
 
1359
+ /**
1360
+ * {{> grabNumberOfVisibleElements }}
1361
+ */
1362
+ async grabNumberOfVisibleElements(locator) {
1363
+ if (this.isWeb) return super.grabNumberOfVisibleElements(locator);
1364
+ return super.grabNumberOfVisibleElements(parseLocator.call(this, locator));
1365
+ }
1366
+
1367
+ /**
1368
+ * Can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1369
+ *
1370
+ * {{> grabAttributeFrom }}
1371
+ */
1372
+ async grabAttributeFrom(locator, attr) {
1373
+ if (this.isWeb) return super.grabAttributeFrom(locator, attr);
1374
+ return super.grabAttributeFrom(parseLocator.call(this, locator), attr);
1375
+ }
1376
+
1377
+ /**
1378
+ * Can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
1379
+ * {{> grabAttributeFromAll }}
1380
+ */
1381
+ async grabAttributeFromAll(locator, attr) {
1382
+ if (this.isWeb) return super.grabAttributeFromAll(locator, attr);
1383
+ return super.grabAttributeFromAll(parseLocator.call(this, locator), attr);
1384
+ }
1385
+
1355
1386
  /**
1356
1387
  * {{> grabValueFromAll }}
1357
1388
  *
@@ -1370,6 +1401,20 @@ class Appium extends Webdriver {
1370
1401
  return super.grabValueFrom(parseLocator.call(this, locator));
1371
1402
  }
1372
1403
 
1404
+ /**
1405
+ * Saves a screenshot to ouput folder (set in codecept.json or codecept.conf.js).
1406
+ * Filename is relative to output folder.
1407
+ *
1408
+ * ```js
1409
+ * I.saveScreenshot('debug.png');
1410
+ * ```
1411
+ *
1412
+ * @param {string} fileName file name to save.
1413
+ */
1414
+ async saveScreenshot(fileName) {
1415
+ return super.saveScreenshot(fileName, false);
1416
+ }
1417
+
1373
1418
  /**
1374
1419
  * {{> scrollIntoView }}
1375
1420
  *
@@ -19,6 +19,7 @@ const {
19
19
  screenshotOutputFolder,
20
20
  getNormalizedKeyAttributeValue,
21
21
  isModifierKey,
22
+ clearString,
22
23
  } = require('../utils');
23
24
  const {
24
25
  isColorProperty,
@@ -28,6 +29,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
28
29
  const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
29
30
  const Popup = require('./extras/Popup');
30
31
  const Console = require('./extras/Console');
32
+ const findReact = require('./extras/React');
31
33
 
32
34
  let playwright;
33
35
  let perfTiming;
@@ -64,6 +66,8 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
64
66
  * * `restart`: (optional, default: true) - restart browser between tests.
65
67
  * * `disableScreenshots`: (optional, default: false) - don't save screenshot on failure.
66
68
  * * `emulate`: (optional, default: {}) launch browser in device emulation mode.
69
+ * * `video`: (optional, default: false) enables video recording for failed tests; videos are saved into `output/videos` folder
70
+ * * `trace`: (optional, default: false) record [tracing information](https://playwright.dev/docs/trace-viewer) with screenshots and snapshots.
67
71
  * * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
68
72
  * * `uniqueScreenshotNames`: (optional, default: false) - option to prevent screenshot override if you have scenarios with the same name in different suites.
69
73
  * * `keepBrowserState`: (optional, default: false) - keep browser state between tests when `restart` is set to false.
@@ -80,6 +84,22 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
80
84
  * * `chromium`: (optional) pass additional chromium options
81
85
  * * `electron`: (optional) pass additional electron options
82
86
  *
87
+ * #### Video Recording Customization
88
+ *
89
+ * By default, video is saved to `output/video` dir. You can customize this path by passing `dir` option to `recordVideo` option.
90
+ *
91
+ * * `video`: enables video recording for failed tests; videos are saved into `output/videos` folder
92
+ * * `keepVideoForPassedTests`: - save videos for passed tests
93
+ * * `recordVideo`: [additional options for videos customization](https://playwright.dev/docs/next/api/class-browser#browser-new-context)
94
+ *
95
+ * #### Trace Recording Customization
96
+ *
97
+ * Trace recording provides a complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
98
+ * Traces will be saved to `output/trace`
99
+ *
100
+ * * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
101
+ * * `keepTraceForPassedTests`: - save trace for passed tests
102
+ *
83
103
  * #### Example #1: Wait for 0 network connections.
84
104
  *
85
105
  * ```js
@@ -131,7 +151,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
131
151
  * Playwright: {
132
152
  * url: "http://localhost",
133
153
  * chromium: {
134
- * browserWSEndpoint: { wsEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a' }
154
+ * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
135
155
  * }
136
156
  * }
137
157
  * }
@@ -211,6 +231,7 @@ class Playwright extends Helper {
211
231
  this.activeSessionName = '';
212
232
  this.isElectron = false;
213
233
  this.electronSessions = [];
234
+ this.storageState = null;
214
235
 
215
236
  // override defaults with config
216
237
  this._setConfig(config);
@@ -220,7 +241,6 @@ class Playwright extends Helper {
220
241
  const defaults = {
221
242
  // options to emulate context
222
243
  emulate: {},
223
-
224
244
  browser: 'chromium',
225
245
  waitForAction: 100,
226
246
  waitForTimeout: 1000,
@@ -249,10 +269,16 @@ class Playwright extends Helper {
249
269
  }
250
270
 
251
271
  _getOptionsForBrowser(config) {
252
- return config[config.browser] ? {
253
- ...config[config.browser],
254
- wsEndpoint: config[config.browser].browserWSEndpoint,
255
- } : {};
272
+ if (config[config.browser]) {
273
+ if (config[config.browser].browserWSEndpoint && config[config.browser].browserWSEndpoint.wsEndpoint) {
274
+ config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint;
275
+ }
276
+ return {
277
+ ...config[config.browser],
278
+ wsEndpoint: config[config.browser].browserWSEndpoint,
279
+ };
280
+ }
281
+ return {};
256
282
  }
257
283
 
258
284
  _setConfig(config) {
@@ -261,6 +287,12 @@ class Playwright extends Helper {
261
287
  headless: !this.options.show,
262
288
  ...this._getOptionsForBrowser(config),
263
289
  };
290
+ if (this.options.video) {
291
+ this.options.recordVideo = { size: parseWindowSize(this.options.windowSize) };
292
+ }
293
+ if (this.options.recordVideo && !this.options.recordVideo.dir) {
294
+ this.options.recordVideo.dir = `${global.output_dir}/videos/`;
295
+ }
264
296
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
265
297
  this.isElectron = this.options.browser === 'electron';
266
298
  this.userDataDir = this.playwrightOptions.userDataDir;
@@ -319,8 +351,42 @@ class Playwright extends Helper {
319
351
  return err.message.includes('context');
320
352
  },
321
353
  });
322
- if (this.options.restart && !this.options.manualStart) return this._startBrowser();
323
- if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
354
+ if (this.options.restart && !this.options.manualStart) await this._startBrowser();
355
+ if (!this.isRunning && !this.options.manualStart) await this._startBrowser();
356
+
357
+ this.isAuthenticated = false;
358
+ if (this.isElectron) {
359
+ this.browserContext = this.browser.context();
360
+ } else if (this.userDataDir) {
361
+ this.browserContext = this.browser;
362
+ } else {
363
+ const contextOptions = {
364
+ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors,
365
+ acceptDownloads: true,
366
+ ...this.options.emulate,
367
+ };
368
+ if (this.options.basicAuth) {
369
+ contextOptions.httpCredentials = this.options.basicAuth;
370
+ this.isAuthenticated = true;
371
+ }
372
+ if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
373
+ if (this.storageState) contextOptions.storageState = this.storageState;
374
+ this.browserContext = await this.browser.newContext(contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
375
+ }
376
+
377
+ let mainPage;
378
+ if (this.isElectron) {
379
+ mainPage = await this.browser.firstWindow();
380
+ } else {
381
+ const existingPages = await this.browserContext.pages();
382
+ mainPage = existingPages[0] || await this.browserContext.newPage();
383
+ }
384
+ targetCreatedHandler.call(this, mainPage);
385
+
386
+ await this._setPage(mainPage);
387
+
388
+ if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true });
389
+
324
390
  return this.browser;
325
391
  }
326
392
 
@@ -333,43 +399,24 @@ class Playwright extends Helper {
333
399
  return;
334
400
  }
335
401
 
402
+ if (this.options.restart) {
403
+ this.isRunning = false;
404
+ return this._stopBrowser();
405
+ }
406
+
336
407
  // close other sessions
337
408
  try {
338
409
  const contexts = await this.browser.contexts();
339
- contexts.shift();
410
+ const currentContext = contexts[0];
411
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
412
+ this.storageState = await currentContext.storageState();
413
+ }
340
414
 
341
415
  await Promise.all(contexts.map(c => c.close()));
342
416
  } catch (e) {
343
417
  console.log(e);
344
418
  }
345
419
 
346
- if (this.options.restart) {
347
- this.isRunning = false;
348
- return this._stopBrowser();
349
- }
350
-
351
- // ensure current page is in default context
352
- if (this.page) {
353
- const existingPages = await this.browserContext.pages();
354
- await this._setPage(existingPages[0]);
355
- }
356
-
357
- if (this.options.keepBrowserState) return;
358
-
359
- if (!this.options.keepCookies) {
360
- this.debugSection('Session', 'cleaning cookies and localStorage');
361
- await this.clearCookie();
362
- }
363
- const currentUrl = await this.grabCurrentUrl();
364
-
365
- if (currentUrl.startsWith('http')) {
366
- await this.executeScript('localStorage.clear();').catch((err) => {
367
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
368
- });
369
- await this.executeScript('sessionStorage.clear();').catch((err) => {
370
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
371
- });
372
- }
373
420
  // await this.closeOtherTabs();
374
421
  return this.browser;
375
422
  }
@@ -517,7 +564,7 @@ class Playwright extends Helper {
517
564
  page.setDefaultNavigationTimeout(this.options.getPageTimeout);
518
565
  this.context = await this.page;
519
566
  this.contextLocator = null;
520
- if (this.config.browser === 'chrome') {
567
+ if (this.options.browser === 'chrome') {
521
568
  await page.bringToFront();
522
569
  }
523
570
  }
@@ -533,6 +580,7 @@ class Playwright extends Helper {
533
580
  if (!page) {
534
581
  return;
535
582
  }
583
+ page.removeAllListeners('dialog');
536
584
  page.on('dialog', async (dialog) => {
537
585
  popupStore.popup = dialog;
538
586
  const action = popupStore.actionType || this.options.defaultPopupAction;
@@ -579,7 +627,7 @@ class Playwright extends Helper {
579
627
  this.browser = await playwright._electron.launch(this.playwrightOptions);
580
628
  } else if (this.isRemoteBrowser) {
581
629
  try {
582
- this.browser = await playwright[this.options.browser].connect(this.playwrightOptions.browserWSEndpoint);
630
+ this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
583
631
  } catch (err) {
584
632
  if (err.toString().indexOf('ECONNREFUSED')) {
585
633
  throw new RemoteBrowserConnectionRefused(err);
@@ -597,26 +645,6 @@ class Playwright extends Helper {
597
645
  this.debugSection('Url', target.url());
598
646
  });
599
647
 
600
- if (this.isElectron) {
601
- this.browserContext = this.browser.context();
602
- } else if (this.userDataDir) {
603
- this.browserContext = this.browser;
604
- } else {
605
- this.browserContext = await this.browser.newContext({ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors, acceptDownloads: true, ...this.options.emulate });// Adding the HTTPSError ignore in the context so that we can ignore those errors
606
- }
607
-
608
- let mainPage;
609
- if (this.isElectron) {
610
- mainPage = await this.browser.firstWindow();
611
- } else {
612
- const existingPages = await this.browserContext.pages();
613
- mainPage = existingPages[0] || await this.browserContext.newPage();
614
- }
615
- targetCreatedHandler.call(this, mainPage);
616
-
617
- await this._setPage(mainPage);
618
- await this.closeOtherTabs();
619
-
620
648
  this.isRunning = true;
621
649
  }
622
650
 
@@ -691,9 +719,9 @@ class Playwright extends Helper {
691
719
  url = this.options.url + url;
692
720
  }
693
721
 
694
- if (this.config.basicAuth && (this.isAuthenticated !== true)) {
722
+ if (this.options.basicAuth && (this.isAuthenticated !== true)) {
695
723
  if (url.includes(this.options.url)) {
696
- await this.browserContext.setHTTPCredentials(this.config.basicAuth);
724
+ await this.browserContext.setHTTPCredentials(this.options.basicAuth);
697
725
  this.isAuthenticated = true;
698
726
  }
699
727
  }
@@ -752,7 +780,7 @@ class Playwright extends Helper {
752
780
  if (!customHeaders) {
753
781
  throw new Error('Cannot send empty headers.');
754
782
  }
755
- return this.page.setExtraHTTPHeaders(customHeaders);
783
+ return this.browserContext.setExtraHTTPHeaders(customHeaders);
756
784
  }
757
785
 
758
786
  /**
@@ -1050,7 +1078,7 @@ class Playwright extends Helper {
1050
1078
  */
1051
1079
  async seeElement(locator) {
1052
1080
  let els = await this._locate(locator);
1053
- els = await Promise.all(els.map(el => el.boundingBox()));
1081
+ els = await Promise.all(els.map(el => el.isVisible()));
1054
1082
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
1055
1083
  }
1056
1084
 
@@ -1060,7 +1088,7 @@ class Playwright extends Helper {
1060
1088
  */
1061
1089
  async dontSeeElement(locator) {
1062
1090
  let els = await this._locate(locator);
1063
- els = await Promise.all(els.map(el => el.boundingBox()));
1091
+ els = await Promise.all(els.map(el => el.isVisible()));
1064
1092
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
1065
1093
  }
1066
1094
 
@@ -1364,7 +1392,7 @@ class Playwright extends Helper {
1364
1392
  */
1365
1393
  async grabNumberOfVisibleElements(locator) {
1366
1394
  let els = await this._locate(locator);
1367
- els = await Promise.all(els.map(el => el.boundingBox()));
1395
+ els = await Promise.all(els.map(el => el.isVisible()));
1368
1396
  return els.filter(v => v).length;
1369
1397
  }
1370
1398
 
@@ -1531,6 +1559,7 @@ class Playwright extends Helper {
1531
1559
  async clearCookie() {
1532
1560
  // Playwright currently doesn't support to delete a certain cookie
1533
1561
  // https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browsercontext
1562
+ if (!this.browserContext) return;
1534
1563
  return this.browserContext.clearCookies();
1535
1564
  }
1536
1565
 
@@ -1825,8 +1854,42 @@ class Playwright extends Helper {
1825
1854
  return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
1826
1855
  }
1827
1856
 
1828
- async _failed() {
1857
+ async _failed(test) {
1829
1858
  await this._withinEnd();
1859
+
1860
+ if (!test.artifacts) {
1861
+ test.artifacts = {};
1862
+ }
1863
+
1864
+ if (this.options.recordVideo && this.page.video()) {
1865
+ test.artifacts.video = await this.page.video().path();
1866
+ }
1867
+
1868
+ if (this.options.trace) {
1869
+ const path = `${global.output_dir}/trace/${clearString(test.title).slice(0, 255)}.zip`;
1870
+ await this.browserContext.tracing.stop({ path });
1871
+ test.artifacts.trace = path;
1872
+ }
1873
+ }
1874
+
1875
+ async _passed(test) {
1876
+ if (this.options.recordVideo && this.page.video()) {
1877
+ if (this.options.keepVideoForPassedTests) {
1878
+ test.artifacts.video = await this.page.video().path();
1879
+ } else {
1880
+ this.page.video().delete().catch(e => {});
1881
+ }
1882
+ }
1883
+
1884
+ if (this.options.trace) {
1885
+ if (this.options.keepTraceForPassedTests) {
1886
+ const path = `${global.output_dir}/trace/${clearString(test.title)}.zip`;
1887
+ await this.browserContext.tracing.stop({ path });
1888
+ test.artifacts.trace = path;
1889
+ } else {
1890
+ await this.browserContext.tracing.stop();
1891
+ }
1892
+ }
1830
1893
  }
1831
1894
 
1832
1895
  /**
@@ -2234,6 +2297,39 @@ class Playwright extends Helper {
2234
2297
  if (prop) return rect[prop];
2235
2298
  return rect;
2236
2299
  }
2300
+
2301
+ /**
2302
+ * Mocks network request using [`browserContext.route`](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) of Playwright
2303
+ *
2304
+ * ```js
2305
+ * I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
2306
+ * ```
2307
+ * This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
2308
+ *
2309
+ * @param {string} [url] URL, regex or pattern for to match URL
2310
+ * @param {function} [handler] a function to process request
2311
+ *
2312
+ */
2313
+ async mockRoute(url, handler) {
2314
+ return this.browserContext.route(...arguments);
2315
+ }
2316
+
2317
+ /**
2318
+ * Stops network mocking created by `mockRoute`.
2319
+ *
2320
+ * ```js
2321
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
2322
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/, previouslySetHandler);
2323
+ * ```
2324
+ * If no handler is passed, all mock requests for the rote are disabled.
2325
+ *
2326
+ * @param {string} [url] URL, regex or pattern for to match URL
2327
+ * @param {function} [handler] a function to process request
2328
+ *
2329
+ */
2330
+ async stopMockingRoute(url, handler) {
2331
+ return this.browserContext.unroute(...arguments);
2332
+ }
2237
2333
  }
2238
2334
 
2239
2335
  module.exports = Playwright;
@@ -2249,6 +2345,7 @@ function buildLocatorString(locator) {
2249
2345
  }
2250
2346
 
2251
2347
  async function findElements(matcher, locator) {
2348
+ if (locator.react) return findReact(matcher, locator);
2252
2349
  locator = new Locator(locator, 'css');
2253
2350
  return matcher.$$(buildLocatorString(locator));
2254
2351
  }
@@ -2297,6 +2394,8 @@ async function proceedClick(locator, context = null, options = {}) {
2297
2394
  }
2298
2395
 
2299
2396
  async function findClickable(matcher, locator) {
2397
+ if (locator.react) return findReact(matcher, locator);
2398
+
2300
2399
  locator = new Locator(locator);
2301
2400
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
2302
2401
 
@@ -2403,6 +2502,15 @@ async function findFields(locator) {
2403
2502
  }
2404
2503
 
2405
2504
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2505
+ // modern drag and drop in Playwright
2506
+ if (this.page.dragAndDrop) {
2507
+ const source = new Locator(sourceLocator);
2508
+ const dest = new Locator(destinationLocator);
2509
+ if (source.isBasic() && dest.isBasic()) {
2510
+ return this.page.dragAndDrop(source.simplify(), dest.simplify());
2511
+ }
2512
+ }
2513
+
2406
2514
  const src = await this._locate(sourceLocator);
2407
2515
  assertElementExists(src, sourceLocator, 'Source Element');
2408
2516
 
@@ -2562,11 +2670,20 @@ async function targetCreatedHandler(page) {
2562
2670
  await page.setUserAgent(this.options.userAgent);
2563
2671
  }
2564
2672
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
2565
- const dimensions = this.options.windowSize.split('x');
2566
- const width = parseInt(dimensions[0], 10);
2567
- const height = parseInt(dimensions[1], 10);
2568
- await page.setViewportSize({ width, height });
2673
+ await page.setViewportSize(parseWindowSize(this.options.windowSize));
2674
+ }
2675
+ }
2676
+
2677
+ function parseWindowSize(windowSize) {
2678
+ if (!windowSize) return { width: 800, height: 600 };
2679
+ const dimensions = windowSize.split('x');
2680
+ if (dimensions.length < 2 || windowSize === 'maximize') {
2681
+ console.log('Invalid window size, setting window to default values');
2682
+ return { width: 800, height: 600 }; // invalid size
2569
2683
  }
2684
+ const width = parseInt(dimensions[0], 10);
2685
+ const height = parseInt(dimensions[1], 10);
2686
+ return { width, height };
2570
2687
  }
2571
2688
 
2572
2689
  // List of key values to key definitions