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
@@ -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
  }
@@ -524,7 +571,7 @@ class Playwright extends Helper {
524
571
  page.setDefaultNavigationTimeout(this.options.getPageTimeout);
525
572
  this.context = await this.page;
526
573
  this.contextLocator = null;
527
- if (this.config.browser === 'chrome') {
574
+ if (this.options.browser === 'chrome') {
528
575
  await page.bringToFront();
529
576
  }
530
577
  }
@@ -540,6 +587,7 @@ class Playwright extends Helper {
540
587
  if (!page) {
541
588
  return;
542
589
  }
590
+ page.removeAllListeners('dialog');
543
591
  page.on('dialog', async (dialog) => {
544
592
  popupStore.popup = dialog;
545
593
  const action = popupStore.actionType || this.options.defaultPopupAction;
@@ -586,7 +634,7 @@ class Playwright extends Helper {
586
634
  this.browser = await playwright._electron.launch(this.playwrightOptions);
587
635
  } else if (this.isRemoteBrowser) {
588
636
  try {
589
- this.browser = await playwright[this.options.browser].connect(this.playwrightOptions.browserWSEndpoint);
637
+ this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
590
638
  } catch (err) {
591
639
  if (err.toString().indexOf('ECONNREFUSED')) {
592
640
  throw new RemoteBrowserConnectionRefused(err);
@@ -604,26 +652,6 @@ class Playwright extends Helper {
604
652
  this.debugSection('Url', target.url());
605
653
  });
606
654
 
607
- if (this.isElectron) {
608
- this.browserContext = this.browser.context();
609
- } else if (this.userDataDir) {
610
- this.browserContext = this.browser;
611
- } else {
612
- 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
613
- }
614
-
615
- let mainPage;
616
- if (this.isElectron) {
617
- mainPage = await this.browser.firstWindow();
618
- } else {
619
- const existingPages = await this.browserContext.pages();
620
- mainPage = existingPages[0] || await this.browserContext.newPage();
621
- }
622
- targetCreatedHandler.call(this, mainPage);
623
-
624
- await this._setPage(mainPage);
625
- await this.closeOtherTabs();
626
-
627
655
  this.isRunning = true;
628
656
  }
629
657
 
@@ -707,9 +735,9 @@ class Playwright extends Helper {
707
735
  url = this.options.url + url;
708
736
  }
709
737
 
710
- if (this.config.basicAuth && (this.isAuthenticated !== true)) {
738
+ if (this.options.basicAuth && (this.isAuthenticated !== true)) {
711
739
  if (url.includes(this.options.url)) {
712
- await this.browserContext.setHTTPCredentials(this.config.basicAuth);
740
+ await this.browserContext.setHTTPCredentials(this.options.basicAuth);
713
741
  this.isAuthenticated = true;
714
742
  }
715
743
  }
@@ -772,7 +800,7 @@ class Playwright extends Helper {
772
800
  if (!customHeaders) {
773
801
  throw new Error('Cannot send empty headers.');
774
802
  }
775
- return this.page.setExtraHTTPHeaders(customHeaders);
803
+ return this.browserContext.setExtraHTTPHeaders(customHeaders);
776
804
  }
777
805
 
778
806
  /**
@@ -1162,7 +1190,7 @@ class Playwright extends Helper {
1162
1190
  */
1163
1191
  async seeElement(locator) {
1164
1192
  let els = await this._locate(locator);
1165
- els = await Promise.all(els.map(el => el.boundingBox()));
1193
+ els = await Promise.all(els.map(el => el.isVisible()));
1166
1194
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
1167
1195
  }
1168
1196
 
@@ -1178,7 +1206,7 @@ class Playwright extends Helper {
1178
1206
  */
1179
1207
  async dontSeeElement(locator) {
1180
1208
  let els = await this._locate(locator);
1181
- els = await Promise.all(els.map(el => el.boundingBox()));
1209
+ els = await Promise.all(els.map(el => el.isVisible()));
1182
1210
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
1183
1211
  }
1184
1212
 
@@ -1797,7 +1825,7 @@ class Playwright extends Helper {
1797
1825
  */
1798
1826
  async grabNumberOfVisibleElements(locator) {
1799
1827
  let els = await this._locate(locator);
1800
- els = await Promise.all(els.map(el => el.boundingBox()));
1828
+ els = await Promise.all(els.map(el => el.isVisible()));
1801
1829
  return els.filter(v => v).length;
1802
1830
  }
1803
1831
 
@@ -2108,6 +2136,7 @@ class Playwright extends Helper {
2108
2136
  async clearCookie() {
2109
2137
  // Playwright currently doesn't support to delete a certain cookie
2110
2138
  // https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browsercontext
2139
+ if (!this.browserContext) return;
2111
2140
  return this.browserContext.clearCookies();
2112
2141
  }
2113
2142
 
@@ -2539,8 +2568,42 @@ class Playwright extends Helper {
2539
2568
  return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
2540
2569
  }
2541
2570
 
2542
- async _failed() {
2571
+ async _failed(test) {
2543
2572
  await this._withinEnd();
2573
+
2574
+ if (!test.artifacts) {
2575
+ test.artifacts = {};
2576
+ }
2577
+
2578
+ if (this.options.recordVideo && this.page.video()) {
2579
+ test.artifacts.video = await this.page.video().path();
2580
+ }
2581
+
2582
+ if (this.options.trace) {
2583
+ const path = `${global.output_dir}/trace/${clearString(test.title).slice(0, 255)}.zip`;
2584
+ await this.browserContext.tracing.stop({ path });
2585
+ test.artifacts.trace = path;
2586
+ }
2587
+ }
2588
+
2589
+ async _passed(test) {
2590
+ if (this.options.recordVideo && this.page.video()) {
2591
+ if (this.options.keepVideoForPassedTests) {
2592
+ test.artifacts.video = await this.page.video().path();
2593
+ } else {
2594
+ this.page.video().delete().catch(e => {});
2595
+ }
2596
+ }
2597
+
2598
+ if (this.options.trace) {
2599
+ if (this.options.keepTraceForPassedTests) {
2600
+ const path = `${global.output_dir}/trace/${clearString(test.title)}.zip`;
2601
+ await this.browserContext.tracing.stop({ path });
2602
+ test.artifacts.trace = path;
2603
+ } else {
2604
+ await this.browserContext.tracing.stop();
2605
+ }
2606
+ }
2544
2607
  }
2545
2608
 
2546
2609
  /**
@@ -3123,6 +3186,39 @@ class Playwright extends Helper {
3123
3186
  if (prop) return rect[prop];
3124
3187
  return rect;
3125
3188
  }
3189
+
3190
+ /**
3191
+ * Mocks network request using [`browserContext.route`](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) of Playwright
3192
+ *
3193
+ * ```js
3194
+ * I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
3195
+ * ```
3196
+ * This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
3197
+ *
3198
+ * @param {string} [url] URL, regex or pattern for to match URL
3199
+ * @param {function} [handler] a function to process request
3200
+ *
3201
+ */
3202
+ async mockRoute(url, handler) {
3203
+ return this.browserContext.route(...arguments);
3204
+ }
3205
+
3206
+ /**
3207
+ * Stops network mocking created by `mockRoute`.
3208
+ *
3209
+ * ```js
3210
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
3211
+ * I.stopMockingRoute(/(\.png$)|(\.jpg$)/, previouslySetHandler);
3212
+ * ```
3213
+ * If no handler is passed, all mock requests for the rote are disabled.
3214
+ *
3215
+ * @param {string} [url] URL, regex or pattern for to match URL
3216
+ * @param {function} [handler] a function to process request
3217
+ *
3218
+ */
3219
+ async stopMockingRoute(url, handler) {
3220
+ return this.browserContext.unroute(...arguments);
3221
+ }
3126
3222
  }
3127
3223
 
3128
3224
  module.exports = Playwright;
@@ -3138,6 +3234,7 @@ function buildLocatorString(locator) {
3138
3234
  }
3139
3235
 
3140
3236
  async function findElements(matcher, locator) {
3237
+ if (locator.react) return findReact(matcher, locator);
3141
3238
  locator = new Locator(locator, 'css');
3142
3239
  return matcher.$$(buildLocatorString(locator));
3143
3240
  }
@@ -3186,6 +3283,8 @@ async function proceedClick(locator, context = null, options = {}) {
3186
3283
  }
3187
3284
 
3188
3285
  async function findClickable(matcher, locator) {
3286
+ if (locator.react) return findReact(matcher, locator);
3287
+
3189
3288
  locator = new Locator(locator);
3190
3289
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
3191
3290
 
@@ -3292,6 +3391,15 @@ async function findFields(locator) {
3292
3391
  }
3293
3392
 
3294
3393
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3394
+ // modern drag and drop in Playwright
3395
+ if (this.page.dragAndDrop) {
3396
+ const source = new Locator(sourceLocator);
3397
+ const dest = new Locator(destinationLocator);
3398
+ if (source.isBasic() && dest.isBasic()) {
3399
+ return this.page.dragAndDrop(source.simplify(), dest.simplify());
3400
+ }
3401
+ }
3402
+
3295
3403
  const src = await this._locate(sourceLocator);
3296
3404
  assertElementExists(src, sourceLocator, 'Source Element');
3297
3405
 
@@ -3451,11 +3559,20 @@ async function targetCreatedHandler(page) {
3451
3559
  await page.setUserAgent(this.options.userAgent);
3452
3560
  }
3453
3561
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
3454
- const dimensions = this.options.windowSize.split('x');
3455
- const width = parseInt(dimensions[0], 10);
3456
- const height = parseInt(dimensions[1], 10);
3457
- await page.setViewportSize({ width, height });
3562
+ await page.setViewportSize(parseWindowSize(this.options.windowSize));
3563
+ }
3564
+ }
3565
+
3566
+ function parseWindowSize(windowSize) {
3567
+ if (!windowSize) return { width: 800, height: 600 };
3568
+ const dimensions = windowSize.split('x');
3569
+ if (dimensions.length < 2 || windowSize === 'maximize') {
3570
+ console.log('Invalid window size, setting window to default values');
3571
+ return { width: 800, height: 600 }; // invalid size
3458
3572
  }
3573
+ const width = parseInt(dimensions[0], 10);
3574
+ const height = parseInt(dimensions[1], 10);
3575
+ return { width, height };
3459
3576
  }
3460
3577
 
3461
3578
  // 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) {
@@ -129,8 +129,9 @@ const consoleLogStore = new Console();
129
129
  * }
130
130
  * }
131
131
  * ```
132
+ * > Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
132
133
  *
133
- * #### Example #5: Target URL with provided basic authentication
134
+ * #### Example #5: Target URL with provided basic authentication
134
135
  *
135
136
  * ```js
136
137
  * {
@@ -143,10 +144,25 @@ const consoleLogStore = new Console();
143
144
  * }
144
145
  * }
145
146
  * ```
147
+ * #### Troubleshooting
146
148
  *
149
+ * Error Message: `No usable sandbox!`
150
+ *
151
+ * When running Puppeteer on CI try to disable sandbox if you see that message
152
+ *
153
+ * ```
154
+ * helpers: {
155
+ * Puppeteer: {
156
+ * url: 'http://localhost',
157
+ * show: false,
158
+ * chrome: {
159
+ * args: ['--no-sandbox', '--disable-setuid-sandbox']
160
+ * }
161
+ * },
162
+ * }
163
+ * ```
147
164
  *
148
165
  *
149
- * Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
150
166
  *
151
167
  * ## Access From Helpers
152
168
  *
@@ -701,7 +717,7 @@ class Puppeteer extends Helper {
701
717
  assertElementExists(els);
702
718
 
703
719
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
704
- const { x, y } = await els[0]._clickablePoint();
720
+ const { x, y } = await getClickablePoint(els[0]);
705
721
  await this.page.mouse.move(x + offsetX, y + offsetY);
706
722
  return this._waitForAction();
707
723
  }
@@ -790,7 +806,7 @@ class Puppeteer extends Helper {
790
806
  const els = await this._locate(locator);
791
807
  assertElementExists(els, locator, 'Element');
792
808
  await els[0]._scrollIntoViewIfNeeded();
793
- const elementCoordinates = await els[0]._clickablePoint();
809
+ const elementCoordinates = await getClickablePoint(els[0]);
794
810
  await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
795
811
  } else {
796
812
  await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
@@ -1047,7 +1063,10 @@ class Puppeteer extends Helper {
1047
1063
  */
1048
1064
  async seeElement(locator) {
1049
1065
  let els = await this._locate(locator);
1050
- els = await Promise.all(els.map(el => el.boundingBox()));
1066
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1067
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1068
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1069
+
1051
1070
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
1052
1071
  }
1053
1072
 
@@ -1063,7 +1082,10 @@ class Puppeteer extends Helper {
1063
1082
  */
1064
1083
  async dontSeeElement(locator) {
1065
1084
  let els = await this._locate(locator);
1066
- els = await Promise.all(els.map(el => el.boundingBox()));
1085
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1086
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1087
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1088
+
1067
1089
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
1068
1090
  }
1069
1091
 
@@ -1598,7 +1620,7 @@ class Puppeteer extends Helper {
1598
1620
  * {{ react }}
1599
1621
  */
1600
1622
  async fillField(field, value) {
1601
- const els = await findFields.call(this, field);
1623
+ const els = await findVisibleFields.call(this, field);
1602
1624
  assertElementExists(els, field, 'Field');
1603
1625
  const el = els[0];
1604
1626
  const tag = await el.getProperty('tagName').then(el => el.jsonValue());
@@ -1640,7 +1662,7 @@ class Puppeteer extends Helper {
1640
1662
  * {{ react }}
1641
1663
  */
1642
1664
  async appendField(field, value) {
1643
- const els = await findFields.call(this, field);
1665
+ const els = await findVisibleFields.call(this, field);
1644
1666
  assertElementExists(els, field, 'Field');
1645
1667
  await els[0].press('End');
1646
1668
  await els[0].type(value, { delay: this.options.pressKeyDelay });
@@ -1732,7 +1754,7 @@ class Puppeteer extends Helper {
1732
1754
  *
1733
1755
  */
1734
1756
  async selectOption(select, option) {
1735
- const els = await findFields.call(this, select);
1757
+ const els = await findVisibleFields.call(this, select);
1736
1758
  assertElementExists(els, select, 'Selectable field');
1737
1759
  const el = els[0];
1738
1760
  if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
@@ -1774,7 +1796,10 @@ class Puppeteer extends Helper {
1774
1796
  */
1775
1797
  async grabNumberOfVisibleElements(locator) {
1776
1798
  let els = await this._locate(locator);
1777
- els = await Promise.all(els.map(el => el.boundingBox()));
1799
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1800
+ // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1801
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1802
+
1778
1803
  return els.filter(v => v).length;
1779
1804
  }
1780
1805
 
@@ -2452,8 +2477,8 @@ class Puppeteer extends Helper {
2452
2477
  const src = await this._locate(locator);
2453
2478
  assertElementExists(src, locator, 'Slider Element');
2454
2479
 
2455
- // Note: Using private api ._clickablePoint because the .BoundingBox does not take into account iframe offsets!
2456
- const sliderSource = await src[0]._clickablePoint();
2480
+ // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
2481
+ const sliderSource = await getClickablePoint(src[0]);
2457
2482
 
2458
2483
  // Drag start point
2459
2484
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -3202,7 +3227,7 @@ class Puppeteer extends Helper {
3202
3227
  module.exports = Puppeteer;
3203
3228
 
3204
3229
  async function findElements(matcher, locator) {
3205
- if (locator.react) return findReact(matcher, locator);
3230
+ if (locator.react) return findReact(matcher.executionContext(), locator);
3206
3231
  locator = new Locator(locator, 'css');
3207
3232
  if (!locator.isXPath()) return matcher.$$(locator.simplify());
3208
3233
  return matcher.$x(locator.value);
@@ -3231,7 +3256,7 @@ async function proceedClick(locator, context = null, options = {}) {
3231
3256
  }
3232
3257
 
3233
3258
  async function findClickable(matcher, locator) {
3234
- if (locator.react) return findReact(matcher, locator);
3259
+ if (locator.react) return findReact(matcher.executionContext(), locator);
3235
3260
  locator = new Locator(locator);
3236
3261
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
3237
3262
 
@@ -3314,6 +3339,12 @@ async function proceedIsChecked(assertType, option) {
3314
3339
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
3315
3340
  }
3316
3341
 
3342
+ async function findVisibleFields(locator) {
3343
+ const els = await findFields.call(this, locator);
3344
+ const visible = await Promise.all(els.map(el => el.boundingBox()));
3345
+ return els.filter((el, index) => visible[index]);
3346
+ }
3347
+
3317
3348
  async function findFields(locator) {
3318
3349
  const matchedLocator = new Locator(locator);
3319
3350
  if (!matchedLocator.isFuzzy()) {
@@ -3344,9 +3375,9 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3344
3375
  const dst = await this._locate(destinationLocator);
3345
3376
  assertElementExists(dst, destinationLocator, 'Destination Element');
3346
3377
 
3347
- // Note: Using private api ._clickablePoint becaues the .BoundingBox does not take into account iframe offsets!
3348
- const dragSource = await src[0]._clickablePoint();
3349
- const dragDestination = await dst[0]._clickablePoint();
3378
+ // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
3379
+ const dragSource = await getClickablePoint(src[0]);
3380
+ const dragDestination = await getClickablePoint(dst[0]);
3350
3381
 
3351
3382
  // Drag start point
3352
3383
  await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 });
@@ -3360,7 +3391,7 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3360
3391
  }
3361
3392
 
3362
3393
  async function proceedSeeInField(assertType, field, value) {
3363
- const els = await findFields.call(this, field);
3394
+ const els = await findVisibleFields.call(this, field);
3364
3395
  assertElementExists(els, field, 'Field');
3365
3396
  const el = els[0];
3366
3397
  const tag = await el.getProperty('tagName').then(el => el.jsonValue());
@@ -3493,6 +3524,13 @@ async function targetCreatedHandler(page) {
3493
3524
  }
3494
3525
  }
3495
3526
 
3527
+ // BC compatibility for Puppeteer < 10
3528
+ async function getClickablePoint(el) {
3529
+ if (el.clickablePoint) return el.clickablePoint();
3530
+ if (el._clickablePoint) return el._clickablePoint();
3531
+ return null;
3532
+ }
3533
+
3496
3534
  // List of key values to key definitions
3497
3535
  // https://github.com/GoogleChrome/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
3498
3536
  const keyDefinitionMap = {