codeceptjs 3.0.6 → 3.1.2

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 (62) hide show
  1. package/CHANGELOG.md +92 -8
  2. package/README.md +9 -1
  3. package/bin/codecept.js +28 -17
  4. package/docs/build/Appium.js +69 -0
  5. package/docs/build/GraphQL.js +9 -10
  6. package/docs/build/Playwright.js +271 -63
  7. package/docs/build/Protractor.js +2 -0
  8. package/docs/build/Puppeteer.js +56 -18
  9. package/docs/build/REST.js +16 -3
  10. package/docs/build/WebDriver.js +82 -16
  11. package/docs/changelog.md +93 -9
  12. package/docs/configuration.md +15 -2
  13. package/docs/email.md +8 -8
  14. package/docs/examples.md +3 -3
  15. package/docs/helpers/Appium.md +66 -68
  16. package/docs/helpers/MockRequest.md +3 -3
  17. package/docs/helpers/Playwright.md +269 -203
  18. package/docs/helpers/Puppeteer.md +17 -1
  19. package/docs/helpers/REST.md +23 -9
  20. package/docs/helpers/WebDriver.md +3 -2
  21. package/docs/locators.md +27 -0
  22. package/docs/mobile.md +2 -1
  23. package/docs/nightmare.md +0 -5
  24. package/docs/parallel.md +14 -7
  25. package/docs/playwright.md +178 -11
  26. package/docs/plugins.md +61 -69
  27. package/docs/react.md +1 -1
  28. package/docs/reports.md +5 -4
  29. package/lib/actor.js +1 -2
  30. package/lib/codecept.js +13 -2
  31. package/lib/command/definitions.js +8 -1
  32. package/lib/command/interactive.js +4 -2
  33. package/lib/command/run-multiple/collection.js +4 -0
  34. package/lib/container.js +3 -3
  35. package/lib/helper/Appium.js +41 -0
  36. package/lib/helper/GraphQL.js +9 -10
  37. package/lib/helper/Playwright.js +218 -70
  38. package/lib/helper/Protractor.js +2 -0
  39. package/lib/helper/Puppeteer.js +56 -18
  40. package/lib/helper/REST.js +12 -0
  41. package/lib/helper/WebDriver.js +82 -16
  42. package/lib/helper/errors/ConnectionRefused.js +1 -1
  43. package/lib/helper/extras/Popup.js +1 -1
  44. package/lib/helper/extras/React.js +44 -32
  45. package/lib/interfaces/gherkin.js +1 -0
  46. package/lib/listener/exit.js +2 -4
  47. package/lib/listener/helpers.js +3 -4
  48. package/lib/locator.js +7 -0
  49. package/lib/mochaFactory.js +11 -6
  50. package/lib/output.js +5 -2
  51. package/lib/plugin/allure.js +7 -18
  52. package/lib/plugin/commentStep.js +1 -1
  53. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  54. package/lib/plugin/customLocator.js +2 -2
  55. package/lib/plugin/screenshotOnFail.js +5 -0
  56. package/lib/plugin/subtitles.js +88 -0
  57. package/lib/plugin/tryTo.js +1 -1
  58. package/lib/step.js +4 -2
  59. package/lib/ui.js +6 -2
  60. package/package.json +5 -4
  61. package/typings/index.d.ts +44 -21
  62. package/typings/types.d.ts +137 -16
@@ -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;
@@ -35,9 +37,10 @@ let defaultSelectorEnginesInitialized = false;
35
37
 
36
38
  const popupStore = new Popup();
37
39
  const consoleLogStore = new Console();
38
- const availableBrowsers = ['chromium', 'webkit', 'firefox'];
40
+ const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'];
39
41
 
40
42
  const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine');
43
+
41
44
  /**
42
45
  * Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside:
43
46
  *
@@ -58,11 +61,13 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
58
61
  * This helper should be configured in codecept.json or codecept.conf.js
59
62
  *
60
63
  * * `url`: base url of website to be tested
61
- * * `browser`: a browser to test on, either: `chromium`, `firefox`, `webkit`. Default: chromium.
64
+ * * `browser`: a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
62
65
  * * `show`: (optional, default: false) - show browser window.
63
66
  * * `restart`: (optional, default: true) - restart browser between tests.
64
67
  * * `disableScreenshots`: (optional, default: false) - don't save screenshot on failure.
65
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.
66
71
  * * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
67
72
  * * `uniqueScreenshotNames`: (optional, default: false) - option to prevent screenshot override if you have scenarios with the same name in different suites.
68
73
  * * `keepBrowserState`: (optional, default: false) - keep browser state between tests when `restart` is set to false.
@@ -77,6 +82,23 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
77
82
  * * `userAgent`: (optional) user-agent string.
78
83
  * * `manualStart`: (optional, default: false) - do not start browser before a test, start it manually inside a helper with `this.helpers["Playwright"]._startBrowser()`.
79
84
  * * `chromium`: (optional) pass additional chromium options
85
+ * * `electron`: (optional) pass additional electron options
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
80
102
  *
81
103
  * #### Example #1: Wait for 0 network connections.
82
104
  *
@@ -121,7 +143,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
121
143
  * }
122
144
  * ```
123
145
  *
124
- * #### Example #4: Connect to remote browser by specifying [websocket endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target)
146
+ * #### Example #4: Connect to remote browser by specifying [websocket endpoint](https://playwright.dev/docs/api/class-browsertype#browsertypeconnectparams)
125
147
  *
126
148
  * ```js
127
149
  * {
@@ -129,7 +151,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
129
151
  * Playwright: {
130
152
  * url: "http://localhost",
131
153
  * chromium: {
132
- * browserWSEndpoint: "ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a"
154
+ * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
133
155
  * }
134
156
  * }
135
157
  * }
@@ -147,6 +169,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
147
169
  * url: "http://localhost",
148
170
  * show: true // headless mode not supported for extensions
149
171
  * chromium: {
172
+ * userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
150
173
  * args: [
151
174
  * `--disable-extensions-except=${pathToExtension}`,
152
175
  * `--load-extension=${pathToExtension}`
@@ -206,6 +229,9 @@ class Playwright extends Helper {
206
229
  this.isAuthenticated = false;
207
230
  this.sessionPages = {};
208
231
  this.activeSessionName = '';
232
+ this.isElectron = false;
233
+ this.electronSessions = [];
234
+ this.storageState = null;
209
235
 
210
236
  // override defaults with config
211
237
  this._setConfig(config);
@@ -215,7 +241,6 @@ class Playwright extends Helper {
215
241
  const defaults = {
216
242
  // options to emulate context
217
243
  emulate: {},
218
-
219
244
  browser: 'chromium',
220
245
  waitForAction: 100,
221
246
  waitForTimeout: 1000,
@@ -244,10 +269,16 @@ class Playwright extends Helper {
244
269
  }
245
270
 
246
271
  _getOptionsForBrowser(config) {
247
- return config[config.browser] ? {
248
- ...config[config.browser],
249
- wsEndpoint: config[config.browser].browserWSEndpoint,
250
- } : {};
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 {};
251
282
  }
252
283
 
253
284
  _setConfig(config) {
@@ -256,7 +287,15 @@ class Playwright extends Helper {
256
287
  headless: !this.options.show,
257
288
  ...this._getOptionsForBrowser(config),
258
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
+ }
259
296
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
297
+ this.isElectron = this.options.browser === 'electron';
298
+ this.userDataDir = this.playwrightOptions.userDataDir;
260
299
  popupStore.defaultAction = this.options.defaultPopupAction;
261
300
  }
262
301
 
@@ -268,7 +307,7 @@ class Playwright extends Helper {
268
307
  },
269
308
  {
270
309
  name: 'browser',
271
- message: 'Browser in which testing will be performed. Possible options: chromium, firefox or webkit',
310
+ message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
272
311
  default: 'chromium',
273
312
  },
274
313
  ];
@@ -312,47 +351,67 @@ class Playwright extends Helper {
312
351
  return err.message.includes('context');
313
352
  },
314
353
  });
315
- if (this.options.restart && !this.options.manualStart) return this._startBrowser();
316
- 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
+ if (this.isElectron) {
358
+ this.browserContext = this.browser.context();
359
+ } else if (this.userDataDir) {
360
+ this.browserContext = this.browser;
361
+ } else {
362
+ const contextOptions = {
363
+ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors,
364
+ acceptDownloads: true,
365
+ ...this.options.emulate,
366
+ };
367
+ if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
368
+ if (this.storageState) contextOptions.storageState = this.storageState;
369
+ this.browserContext = await this.browser.newContext(contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
370
+ }
371
+
372
+ let mainPage;
373
+ if (this.isElectron) {
374
+ mainPage = await this.browser.firstWindow();
375
+ } else {
376
+ const existingPages = await this.browserContext.pages();
377
+ mainPage = existingPages[0] || await this.browserContext.newPage();
378
+ }
379
+ targetCreatedHandler.call(this, mainPage);
380
+
381
+ await this._setPage(mainPage);
382
+
383
+ if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true });
384
+
317
385
  return this.browser;
318
386
  }
319
387
 
320
388
  async _after() {
321
389
  if (!this.isRunning) return;
322
390
 
323
- // close other sessions
324
- const contexts = await this.browser.contexts();
325
- contexts.shift();
326
-
327
- await Promise.all(contexts.map(c => c.close()));
391
+ if (this.isElectron) {
392
+ this.browser.close();
393
+ this.electronSessions.forEach(session => session.close());
394
+ return;
395
+ }
328
396
 
329
397
  if (this.options.restart) {
330
398
  this.isRunning = false;
331
399
  return this._stopBrowser();
332
400
  }
333
401
 
334
- // ensure current page is in default context
335
- if (this.page) {
336
- const existingPages = await this.browserContext.pages();
337
- await this._setPage(existingPages[0]);
338
- }
339
-
340
- if (this.options.keepBrowserState) return;
402
+ // close other sessions
403
+ try {
404
+ const contexts = await this.browser.contexts();
405
+ const currentContext = contexts[0];
406
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
407
+ this.storageState = await currentContext.storageState();
408
+ }
341
409
 
342
- if (!this.options.keepCookies) {
343
- this.debugSection('Session', 'cleaning cookies and localStorage');
344
- await this.clearCookie();
410
+ await Promise.all(contexts.map(c => c.close()));
411
+ } catch (e) {
412
+ console.log(e);
345
413
  }
346
- const currentUrl = await this.grabCurrentUrl();
347
414
 
348
- if (currentUrl.startsWith('http')) {
349
- await this.executeScript('localStorage.clear();').catch((err) => {
350
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
351
- });
352
- await this.executeScript('sessionStorage.clear();').catch((err) => {
353
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
354
- });
355
- }
356
415
  // await this.closeOtherTabs();
357
416
  return this.browser;
358
417
  }
@@ -371,12 +430,22 @@ class Playwright extends Helper {
371
430
  this.debugSection('New Context', config ? JSON.stringify(config) : 'opened');
372
431
  this.activeSessionName = sessionName;
373
432
 
374
- const bc = await this.browser.newContext(config);
375
- const page = await bc.newPage();
433
+ let browserContext;
434
+ let page;
435
+ if (this.isElectron) {
436
+ const browser = await playwright._electron.launch(this.playwrightOptions);
437
+ this.electronSessions.push(browser);
438
+ browserContext = browser.context();
439
+ page = await browser.firstWindow();
440
+ } else {
441
+ browserContext = await this.browser.newContext(config);
442
+ page = await browserContext.newPage();
443
+ }
444
+
376
445
  targetCreatedHandler.call(this, page);
377
446
  this._setPage(page);
378
447
  // Create a new page inside context.
379
- return bc;
448
+ return browserContext;
380
449
  },
381
450
  stop: async () => {
382
451
  // is closed by _after
@@ -496,6 +565,7 @@ class Playwright extends Helper {
496
565
  if (!page) return;
497
566
  page.setDefaultNavigationTimeout(this.options.getPageTimeout);
498
567
  this.context = await this.page;
568
+ this.contextLocator = null;
499
569
  if (this.config.browser === 'chrome') {
500
570
  await page.bringToFront();
501
571
  }
@@ -512,6 +582,7 @@ class Playwright extends Helper {
512
582
  if (!page) {
513
583
  return;
514
584
  }
585
+ page.removeAllListeners('dialog');
515
586
  page.on('dialog', async (dialog) => {
516
587
  popupStore.popup = dialog;
517
588
  const action = popupStore.actionType || this.options.defaultPopupAction;
@@ -554,7 +625,9 @@ class Playwright extends Helper {
554
625
  }
555
626
 
556
627
  async _startBrowser() {
557
- if (this.isRemoteBrowser) {
628
+ if (this.isElectron) {
629
+ this.browser = await playwright._electron.launch(this.playwrightOptions);
630
+ } else if (this.isRemoteBrowser) {
558
631
  try {
559
632
  this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
560
633
  } catch (err) {
@@ -563,6 +636,8 @@ class Playwright extends Helper {
563
636
  }
564
637
  throw err;
565
638
  }
639
+ } else if (this.userDataDir) {
640
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
566
641
  } else {
567
642
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
568
643
  }
@@ -571,19 +646,14 @@ class Playwright extends Helper {
571
646
  this.browser.on('targetchanged', (target) => {
572
647
  this.debugSection('Url', target.url());
573
648
  });
574
- 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
575
-
576
- const existingPages = await this.browserContext.pages();
577
-
578
- const mainPage = existingPages[0] || await this.browserContext.newPage();
579
- targetCreatedHandler.call(this, mainPage);
580
-
581
- await this._setPage(mainPage);
582
- await this.closeOtherTabs();
583
649
 
584
650
  this.isRunning = true;
585
651
  }
586
652
 
653
+ _getType() {
654
+ return this.browser._type;
655
+ }
656
+
587
657
  async _stopBrowser() {
588
658
  this.withinLocator = null;
589
659
  this._setPage(null);
@@ -618,6 +688,7 @@ class Playwright extends Helper {
618
688
  const els = await this._locate(locator);
619
689
  assertElementExists(els, locator);
620
690
  this.context = els[0];
691
+ this.contextLocator = locator;
621
692
 
622
693
  this.withinLocator = new Locator(locator);
623
694
  }
@@ -625,6 +696,7 @@ class Playwright extends Helper {
625
696
  async _withinEnd() {
626
697
  this.withinLocator = null;
627
698
  this.context = await this.page;
699
+ this.contextLocator = null;
628
700
  }
629
701
 
630
702
  _extractDataFromPerformanceTiming(timing, ...dataNames) {
@@ -651,6 +723,9 @@ class Playwright extends Helper {
651
723
  * @param {string} url url path or global url.
652
724
  */
653
725
  async amOnPage(url) {
726
+ if (this.isElectron) {
727
+ throw new Error('Cannot open pages inside an Electron container');
728
+ }
654
729
  if (!(/^\w+\:\/\//.test(url))) {
655
730
  url = this.options.url + url;
656
731
  }
@@ -984,6 +1059,9 @@ class Playwright extends Helper {
984
1059
  * @param {number} [num=1]
985
1060
  */
986
1061
  async switchToNextTab(num = 1) {
1062
+ if (this.isElectron) {
1063
+ throw new Error('Cannot switch tabs inside an Electron container');
1064
+ }
987
1065
  const pages = await this.browserContext.pages();
988
1066
 
989
1067
  const index = pages.indexOf(this.page);
@@ -1007,6 +1085,9 @@ class Playwright extends Helper {
1007
1085
  * @param {number} [num=1]
1008
1086
  */
1009
1087
  async switchToPreviousTab(num = 1) {
1088
+ if (this.isElectron) {
1089
+ throw new Error('Cannot switch tabs inside an Electron container');
1090
+ }
1010
1091
  const pages = await this.browserContext.pages();
1011
1092
  const index = pages.indexOf(this.page);
1012
1093
  this.withinLocator = null;
@@ -1028,6 +1109,9 @@ class Playwright extends Helper {
1028
1109
  * ```
1029
1110
  */
1030
1111
  async closeCurrentTab() {
1112
+ if (this.isElectron) {
1113
+ throw new Error('Cannot close current tab inside an Electron container');
1114
+ }
1031
1115
  const oldPage = this.page;
1032
1116
  await this.switchToPreviousTab();
1033
1117
  await oldPage.close();
@@ -1066,6 +1150,9 @@ class Playwright extends Helper {
1066
1150
  * ```
1067
1151
  */
1068
1152
  async openNewTab(options) {
1153
+ if (this.isElectron) {
1154
+ throw new Error('Cannot open new tabs inside an Electron container');
1155
+ }
1069
1156
  await this._setPage(await this.browserContext.newPage(options));
1070
1157
  return this._waitForAction();
1071
1158
  }
@@ -1098,7 +1185,7 @@ class Playwright extends Helper {
1098
1185
  */
1099
1186
  async seeElement(locator) {
1100
1187
  let els = await this._locate(locator);
1101
- els = await Promise.all(els.map(el => el.boundingBox()));
1188
+ els = await Promise.all(els.map(el => el.isVisible()));
1102
1189
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
1103
1190
  }
1104
1191
 
@@ -1114,7 +1201,7 @@ class Playwright extends Helper {
1114
1201
  */
1115
1202
  async dontSeeElement(locator) {
1116
1203
  let els = await this._locate(locator);
1117
- els = await Promise.all(els.map(el => el.boundingBox()));
1204
+ els = await Promise.all(els.map(el => el.isVisible()));
1118
1205
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
1119
1206
  }
1120
1207
 
@@ -1733,7 +1820,7 @@ class Playwright extends Helper {
1733
1820
  */
1734
1821
  async grabNumberOfVisibleElements(locator) {
1735
1822
  let els = await this._locate(locator);
1736
- els = await Promise.all(els.map(el => el.boundingBox()));
1823
+ els = await Promise.all(els.map(el => el.isVisible()));
1737
1824
  return els.filter(v => v).length;
1738
1825
  }
1739
1826
 
@@ -2044,6 +2131,7 @@ class Playwright extends Helper {
2044
2131
  async clearCookie() {
2045
2132
  // Playwright currently doesn't support to delete a certain cookie
2046
2133
  // https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browsercontext
2134
+ if (!this.browserContext) return;
2047
2135
  return this.browserContext.clearCookies();
2048
2136
  }
2049
2137
 
@@ -2079,6 +2167,22 @@ class Playwright extends Helper {
2079
2167
  return context.evaluate.apply(context, [fn, arg]);
2080
2168
  }
2081
2169
 
2170
+ /**
2171
+ * Grab Locator if called within Context
2172
+ *
2173
+ * @param {*} locator
2174
+ */
2175
+ _contextLocator(locator) {
2176
+ locator = buildLocatorString(new Locator(locator, 'css'));
2177
+
2178
+ if (this.contextLocator) {
2179
+ const contextLocator = buildLocatorString(new Locator(this.contextLocator, 'css'));
2180
+ locator = `${contextLocator} >> ${locator}`;
2181
+ }
2182
+
2183
+ return locator;
2184
+ }
2185
+
2082
2186
  /**
2083
2187
  * Retrieves a text from an element located by CSS or XPath and returns it to test.
2084
2188
  * Resumes test execution, so **should be used inside async with `await`** operator.
@@ -2094,10 +2198,11 @@ class Playwright extends Helper {
2094
2198
  *
2095
2199
  */
2096
2200
  async grabTextFrom(locator) {
2097
- const texts = await this.grabTextFromAll(locator);
2098
- assertElementExists(texts, locator);
2099
- this.debugSection('Text', texts[0]);
2100
- return texts[0];
2201
+ locator = this._contextLocator(locator);
2202
+ const text = await this.page.textContent(locator);
2203
+ assertElementExists(text, locator);
2204
+ this.debugSection('Text', text);
2205
+ return text;
2101
2206
  }
2102
2207
 
2103
2208
  /**
@@ -2458,8 +2563,37 @@ class Playwright extends Helper {
2458
2563
  return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2459
2564
  }
2460
2565
 
2461
- async _failed() {
2566
+ async _failed(test) {
2462
2567
  await this._withinEnd();
2568
+ if (this.options.recordVideo && this.page.video()) {
2569
+ test.artifacts.video = await this.page.video().path();
2570
+ }
2571
+
2572
+ if (this.options.trace) {
2573
+ const path = `${global.output_dir}/trace/${clearString(test.title).slice(0, 255)}.zip`;
2574
+ await this.browserContext.tracing.stop({ path });
2575
+ test.artifacts.trace = path;
2576
+ }
2577
+ }
2578
+
2579
+ async _passed(test) {
2580
+ if (this.options.recordVideo && this.page.video()) {
2581
+ if (this.options.keepVideoForPassedTests) {
2582
+ test.artifacts.video = await this.page.video().path();
2583
+ } else {
2584
+ this.page.video().delete().catch(e => {});
2585
+ }
2586
+ }
2587
+
2588
+ if (this.options.trace) {
2589
+ if (this.options.keepTraceForPassedTests) {
2590
+ const path = `${global.output_dir}/trace/${clearString(test.title)}.zip`;
2591
+ await this.browserContext.tracing.stop({ path });
2592
+ test.artifacts.trace = path;
2593
+ } else {
2594
+ await this.browserContext.tracing.stop();
2595
+ }
2596
+ }
2463
2597
  }
2464
2598
 
2465
2599
  /**
@@ -2850,6 +2984,7 @@ class Playwright extends Helper {
2850
2984
 
2851
2985
  if (locator >= 0 && locator < childFrames.length) {
2852
2986
  this.context = childFrames[locator];
2987
+ this.contextLocator = locator;
2853
2988
  } else {
2854
2989
  throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath');
2855
2990
  }
@@ -2857,6 +2992,7 @@ class Playwright extends Helper {
2857
2992
  }
2858
2993
  if (!locator) {
2859
2994
  this.context = this.page;
2995
+ this.contextLocator = null;
2860
2996
  return;
2861
2997
  }
2862
2998
 
@@ -2867,8 +3003,10 @@ class Playwright extends Helper {
2867
3003
 
2868
3004
  if (contentFrame) {
2869
3005
  this.context = contentFrame;
3006
+ this.contextLocator = null;
2870
3007
  } else {
2871
3008
  this.context = els[0];
3009
+ this.contextLocator = locator;
2872
3010
  }
2873
3011
  }
2874
3012
 
@@ -3038,6 +3176,39 @@ class Playwright extends Helper {
3038
3176
  if (prop) return rect[prop];
3039
3177
  return rect;
3040
3178
  }
3179
+
3180
+ /**
3181
+ * Mocks network request using [`browserContext.route`](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) of Playwright
3182
+ *
3183
+ * ```js
3184
+ * I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
3185
+ * ```
3186
+ * This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
3187
+ *
3188
+ * @param {string} [url] URL, regex or pattern for to match URL
3189
+ * @param {function} [handler] a function to process request
3190
+ *
3191
+ */
3192
+ async mockRoute(url, handler) {
3193
+ return this.browserContext.route(...arguments);
3194
+ }
3195
+
3196
+ /**
3197
+ * Stops network mocking created by `mockRoute`.
3198
+ *
3199
+ * ```js
3200
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
3201
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/, previouslySetHandler);
3202
+ * ```
3203
+ * If no handler is passed, all mock requests for the rote are disabled.
3204
+ *
3205
+ * @param {string} [url] URL, regex or pattern for to match URL
3206
+ * @param {function} [handler] a function to process request
3207
+ *
3208
+ */
3209
+ async stopMockingRoute(url, handler) {
3210
+ return this.browserContext.unroute(...arguments);
3211
+ }
3041
3212
  }
3042
3213
 
3043
3214
  module.exports = Playwright;
@@ -3053,10 +3224,24 @@ function buildLocatorString(locator) {
3053
3224
  }
3054
3225
 
3055
3226
  async function findElements(matcher, locator) {
3227
+ if (locator.react) return findReact(matcher, locator);
3056
3228
  locator = new Locator(locator, 'css');
3057
3229
  return matcher.$$(buildLocatorString(locator));
3058
3230
  }
3059
3231
 
3232
+ async function getVisibleElements(elements) {
3233
+ const visibleElements = [];
3234
+ for (const element of elements) {
3235
+ if (await element.isVisible()) {
3236
+ visibleElements.push(element);
3237
+ }
3238
+ }
3239
+ if (visibleElements.length === 0) {
3240
+ return elements;
3241
+ }
3242
+ return visibleElements;
3243
+ }
3244
+
3060
3245
  async function proceedClick(locator, context = null, options = {}) {
3061
3246
  let matcher = await this._getContext();
3062
3247
  if (context) {
@@ -3076,7 +3261,8 @@ async function proceedClick(locator, context = null, options = {}) {
3076
3261
  if (options.force) {
3077
3262
  await els[0].dispatchEvent('click');
3078
3263
  } else {
3079
- await els[0].click(options);
3264
+ const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0];
3265
+ await element.click(options);
3080
3266
  }
3081
3267
  const promises = [];
3082
3268
  if (options.waitForNavigation) {
@@ -3087,6 +3273,8 @@ async function proceedClick(locator, context = null, options = {}) {
3087
3273
  }
3088
3274
 
3089
3275
  async function findClickable(matcher, locator) {
3276
+ if (locator.react) return findReact(matcher, locator);
3277
+
3090
3278
  locator = new Locator(locator);
3091
3279
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
3092
3280
 
@@ -3193,6 +3381,15 @@ async function findFields(locator) {
3193
3381
  }
3194
3382
 
3195
3383
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3384
+ // modern drag and drop in Playwright
3385
+ if (this.page.dragAndDrop) {
3386
+ const source = new Locator(sourceLocator);
3387
+ const dest = new Locator(destinationLocator);
3388
+ if (source.isBasic() && dest.isBasic()) {
3389
+ return this.page.dragAndDrop(source.simplify(), dest.simplify());
3390
+ }
3391
+ }
3392
+
3196
3393
  const src = await this._locate(sourceLocator);
3197
3394
  assertElementExists(src, sourceLocator, 'Source Element');
3198
3395
 
@@ -3334,11 +3531,13 @@ async function targetCreatedHandler(page) {
3334
3531
  // we are inside iframe?
3335
3532
  const frameEl = await this.context.frameElement();
3336
3533
  this.context = await frameEl.contentFrame();
3534
+ this.contextLocator = null;
3337
3535
  return;
3338
3536
  }
3339
3537
  // if context element was in iframe - keep it
3340
3538
  // if (await this.context.ownerFrame()) return;
3341
3539
  this.context = page;
3540
+ this.contextLocator = null;
3342
3541
  });
3343
3542
  });
3344
3543
  page.on('console', (msg) => {
@@ -3349,12 +3548,21 @@ async function targetCreatedHandler(page) {
3349
3548
  if (this.options.userAgent) {
3350
3549
  await page.setUserAgent(this.options.userAgent);
3351
3550
  }
3352
- if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
3353
- const dimensions = this.options.windowSize.split('x');
3354
- const width = parseInt(dimensions[0], 10);
3355
- const height = parseInt(dimensions[1], 10);
3356
- await page.setViewportSize({ width, height });
3551
+ if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
3552
+ await page.setViewportSize(parseWindowSize(this.options.windowSize));
3553
+ }
3554
+ }
3555
+
3556
+ function parseWindowSize(windowSize) {
3557
+ if (!windowSize) return { width: 800, height: 600 };
3558
+ const dimensions = windowSize.split('x');
3559
+ if (dimensions.length < 2 || windowSize === 'maximize') {
3560
+ console.log('Invalid window size, setting window to default values');
3561
+ return { width: 800, height: 600 }; // invalid size
3357
3562
  }
3563
+ const width = parseInt(dimensions[0], 10);
3564
+ const height = parseInt(dimensions[1], 10);
3565
+ return { width, height };
3358
3566
  }
3359
3567
 
3360
3568
  // List of key values to key definitions
@@ -124,6 +124,8 @@ class Protractor extends Helper {
124
124
 
125
125
  this.isRunning = false;
126
126
  this._setConfig(config);
127
+
128
+ console.log('Protractor helper is deprecated as well as Protractor itself.\nThis helper will be removed in next major release');
127
129
  }
128
130
 
129
131
  _validateConfig(config) {