codeceptjs 3.5.4-beta.1 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/README.md +0 -2
  3. package/docs/build/Appium.js +48 -7
  4. package/docs/build/GraphQL.js +25 -0
  5. package/docs/build/Nightmare.js +15 -6
  6. package/docs/build/Playwright.js +436 -197
  7. package/docs/build/Protractor.js +17 -8
  8. package/docs/build/Puppeteer.js +37 -20
  9. package/docs/build/TestCafe.js +19 -10
  10. package/docs/build/WebDriver.js +45 -37
  11. package/docs/changelog.md +375 -0
  12. package/docs/community-helpers.md +8 -4
  13. package/docs/examples.md +8 -2
  14. package/docs/helpers/Appium.md +39 -2
  15. package/docs/helpers/GraphQL.md +21 -0
  16. package/docs/helpers/Nightmare.md +1260 -0
  17. package/docs/helpers/Playwright.md +223 -119
  18. package/docs/helpers/Protractor.md +1711 -0
  19. package/docs/helpers/Puppeteer.md +31 -29
  20. package/docs/helpers/TestCafe.md +18 -17
  21. package/docs/helpers/WebDriver.md +34 -32
  22. package/docs/playwright.md +24 -1
  23. package/docs/webapi/dontSeeInField.mustache +1 -1
  24. package/docs/webapi/executeAsyncScript.mustache +2 -0
  25. package/docs/webapi/executeScript.mustache +2 -0
  26. package/docs/webapi/seeInField.mustache +1 -1
  27. package/docs/wiki/Books-&-Posts.md +0 -0
  28. package/docs/wiki/Community-Helpers-&-Plugins.md +8 -4
  29. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +46 -14
  30. package/docs/wiki/Examples.md +8 -2
  31. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -0
  32. package/docs/wiki/Home.md +0 -0
  33. package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
  34. package/docs/wiki/Release-Process.md +0 -0
  35. package/docs/wiki/Roadmap.md +0 -0
  36. package/docs/wiki/Tests.md +0 -0
  37. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -0
  38. package/docs/wiki/Videos.md +0 -0
  39. package/lib/codecept.js +1 -0
  40. package/lib/command/definitions.js +2 -7
  41. package/lib/command/init.js +40 -4
  42. package/lib/command/run-multiple/collection.js +17 -5
  43. package/lib/command/run-workers.js +4 -0
  44. package/lib/command/run.js +6 -0
  45. package/lib/helper/Appium.js +46 -5
  46. package/lib/helper/GraphQL.js +25 -0
  47. package/lib/helper/Nightmare.js +1415 -0
  48. package/lib/helper/Playwright.js +336 -62
  49. package/lib/helper/Protractor.js +1837 -0
  50. package/lib/helper/Puppeteer.js +31 -18
  51. package/lib/helper/TestCafe.js +15 -8
  52. package/lib/helper/WebDriver.js +39 -35
  53. package/lib/helper/clientscripts/nightmare.js +213 -0
  54. package/lib/helper/errors/ElementNotFound.js +2 -1
  55. package/lib/helper/scripts/highlightElement.js +1 -1
  56. package/lib/interfaces/bdd.js +1 -1
  57. package/lib/mochaFactory.js +2 -1
  58. package/lib/pause.js +6 -4
  59. package/lib/plugin/heal.js +2 -3
  60. package/lib/plugin/selenoid.js +6 -1
  61. package/lib/step.js +27 -10
  62. package/lib/utils.js +4 -0
  63. package/lib/workers.js +3 -1
  64. package/package.json +87 -87
  65. package/typings/promiseBasedTypes.d.ts +163 -126
  66. package/typings/types.d.ts +183 -144
  67. package/docs/build/Polly.js +0 -42
  68. package/docs/build/SeleniumWebdriver.js +0 -76
@@ -23,6 +23,7 @@ const {
23
23
  isModifierKey,
24
24
  clearString,
25
25
  requireWithFallback,
26
+ normalizeSpacesInString,
26
27
  } = require('../utils');
27
28
  const {
28
29
  isColorProperty,
@@ -76,7 +77,7 @@ const pathSeparator = path.sep;
76
77
  * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
77
78
  * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
78
79
  * @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
79
- * @prop {'load' | 'domcontentloaded' | 'networkidle'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-navigation).
80
+ * @prop {'load' | 'domcontentloaded' | 'commit'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `commit`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-url).
80
81
  * @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
81
82
  * @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
82
83
  * @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
@@ -93,7 +94,7 @@ const pathSeparator = path.sep;
93
94
  * @prop {string[]} [ignoreLog] - An array with console message types that are not logged to debug log. Default value is `['warning', 'log']`. E.g. you can set `[]` to log all messages. See all possible [values](https://playwright.dev/docs/api/class-consolemessage#console-message-type).
94
95
  * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
95
96
  * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
96
- * @prop {boolean} [highlightElement] - highlight the interacting elements
97
+ * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false
97
98
  */
98
99
  const config = {};
99
100
 
@@ -207,6 +208,7 @@ const config = {};
207
208
  * url: "http://localhost",
208
209
  * show: true // headless mode not supported for extensions
209
210
  * chromium: {
211
+ * // Note: due to this would launch persistent context, so to avoid the error when running tests with run-workers a timestamp would be appended to the defined folder name. For instance: playwright-tmp_1692715649511
210
212
  * userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
211
213
  * args: [
212
214
  * `--disable-extensions-except=${pathToExtension}`,
@@ -317,6 +319,12 @@ class Playwright extends Helper {
317
319
  this.recording = false;
318
320
  this.recordedAtLeastOnce = false;
319
321
 
322
+ // for websocket messages
323
+ this.webSocketMessages = [];
324
+ this.recordingWebSocketMessages = false;
325
+ this.recordedWebSocketMessagesAtLeastOnce = false;
326
+ this.cdpSession = null;
327
+
320
328
  // override defaults with config
321
329
  this._setConfig(config);
322
330
  }
@@ -343,7 +351,8 @@ class Playwright extends Helper {
343
351
  show: false,
344
352
  defaultPopupAction: 'accept',
345
353
  use: { actionTimeout: 0 },
346
- ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors
354
+ ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
355
+ highlightElement: false,
347
356
  };
348
357
 
349
358
  config = Object.assign(defaults, config);
@@ -388,7 +397,7 @@ class Playwright extends Helper {
388
397
  }
389
398
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
390
399
  this.isElectron = this.options.browser === 'electron';
391
- this.userDataDir = this.playwrightOptions.userDataDir;
400
+ this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined;
392
401
  this.isCDPConnection = this.playwrightOptions.cdpConnection;
393
402
  popupStore.defaultAction = this.options.defaultPopupAction;
394
403
  }
@@ -461,7 +470,7 @@ class Playwright extends Helper {
461
470
  this.isAuthenticated = false;
462
471
  if (this.isElectron) {
463
472
  this.browserContext = this.browser.context();
464
- } else if (this.userDataDir) {
473
+ } else if (this.playwrightOptions.userDataDir) {
465
474
  this.browserContext = this.browser;
466
475
  } else {
467
476
  const contextOptions = {
@@ -473,6 +482,7 @@ class Playwright extends Helper {
473
482
  contextOptions.httpCredentials = this.options.basicAuth;
474
483
  this.isAuthenticated = true;
475
484
  }
485
+ if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
476
486
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
477
487
  if (this.storageState) contextOptions.storageState = this.storageState;
478
488
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
@@ -487,8 +497,17 @@ class Playwright extends Helper {
487
497
  if (this.isElectron) {
488
498
  mainPage = await this.browser.firstWindow();
489
499
  } else {
490
- const existingPages = await this.browserContext.pages();
491
- mainPage = existingPages[0] || await this.browserContext.newPage();
500
+ try {
501
+ const existingPages = await this.browserContext.pages();
502
+ mainPage = existingPages[0] || await this.browserContext.newPage();
503
+ } catch (e) {
504
+ if (this.playwrightOptions.userDataDir) {
505
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
506
+ this.browserContext = this.browser;
507
+ const existingPages = await this.browserContext.pages();
508
+ mainPage = existingPages[0];
509
+ }
510
+ }
492
511
  }
493
512
  await targetCreatedHandler.call(this, mainPage);
494
513
 
@@ -519,13 +538,15 @@ class Playwright extends Helper {
519
538
 
520
539
  // close other sessions
521
540
  try {
522
- const contexts = await this.browser.contexts();
523
- const currentContext = contexts[0];
524
- if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
525
- this.storageState = await currentContext.storageState();
526
- }
541
+ if ((await this.browser)._type === 'Browser') {
542
+ const contexts = await this.browser.contexts();
543
+ const currentContext = contexts[0];
544
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
545
+ this.storageState = await currentContext.storageState();
546
+ }
527
547
 
528
- await Promise.all(contexts.map(c => c.close()));
548
+ await Promise.all(contexts.map(c => c.close()));
549
+ }
529
550
  } catch (e) {
530
551
  console.log(e);
531
552
  }
@@ -555,8 +576,16 @@ class Playwright extends Helper {
555
576
  browserContext = browser.context();
556
577
  page = await browser.firstWindow();
557
578
  } else {
558
- browserContext = await this.browser.newContext(Object.assign(this.options, config));
559
- page = await browserContext.newPage();
579
+ try {
580
+ browserContext = await this.browser.newContext(Object.assign(this.options, config));
581
+ page = await browserContext.newPage();
582
+ } catch (e) {
583
+ if (this.playwrightOptions.userDataDir) {
584
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
585
+ this.browser = browserContext;
586
+ page = await browserContext.pages()[0];
587
+ }
588
+ }
560
589
  }
561
590
 
562
591
  if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
@@ -569,10 +598,12 @@ class Playwright extends Helper {
569
598
  // is closed by _after
570
599
  },
571
600
  loadVars: async (context) => {
572
- this.browserContext = context;
573
- const existingPages = await context.pages();
574
- this.sessionPages[this.activeSessionName] = existingPages[0];
575
- return this._setPage(this.sessionPages[this.activeSessionName]);
601
+ if (context) {
602
+ this.browserContext = context;
603
+ const existingPages = await context.pages();
604
+ this.sessionPages[this.activeSessionName] = existingPages[0];
605
+ return this._setPage(this.sessionPages[this.activeSessionName]);
606
+ }
576
607
  },
577
608
  restoreVars: async (session) => {
578
609
  this.withinLocator = null;
@@ -763,7 +794,7 @@ class Playwright extends Helper {
763
794
  }
764
795
  throw err;
765
796
  }
766
- } else if (this.userDataDir) {
797
+ } else if (this.playwrightOptions.userDataDir) {
767
798
  this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
768
799
  } else {
769
800
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
@@ -819,8 +850,8 @@ class Playwright extends Helper {
819
850
  await this.switchTo(null);
820
851
  return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
821
852
  }
822
- await this.switchTo(locator);
823
- this.withinLocator = new Locator(locator);
853
+ await this.switchTo(frame);
854
+ this.withinLocator = new Locator(frame);
824
855
  return;
825
856
  }
826
857
 
@@ -1038,8 +1069,11 @@ class Playwright extends Helper {
1038
1069
  const body = document.body;
1039
1070
  const html = document.documentElement;
1040
1071
  window.scrollTo(0, Math.max(
1041
- body.scrollHeight, body.offsetHeight,
1042
- html.clientHeight, html.scrollHeight, html.offsetHeight,
1072
+ body.scrollHeight,
1073
+ body.offsetHeight,
1074
+ html.clientHeight,
1075
+ html.scrollHeight,
1076
+ html.offsetHeight,
1043
1077
  ));
1044
1078
  });
1045
1079
  }
@@ -1545,7 +1579,7 @@ class Playwright extends Helper {
1545
1579
 
1546
1580
  await el.clear();
1547
1581
 
1548
- highlightActiveElement.call(this, el, this.page);
1582
+ highlightActiveElement.call(this, el, await this._getContext());
1549
1583
 
1550
1584
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1551
1585
 
@@ -1590,7 +1624,7 @@ class Playwright extends Helper {
1590
1624
  async appendField(field, value) {
1591
1625
  const els = await findFields.call(this, field);
1592
1626
  assertElementExists(els, field, 'Field');
1593
- highlightActiveElement.call(this, els[0], this.page);
1627
+ highlightActiveElement.call(this, els[0], await this._getContext());
1594
1628
  await els[0].press('End');
1595
1629
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1596
1630
  return this._waitForAction();
@@ -1600,14 +1634,16 @@ class Playwright extends Helper {
1600
1634
  * {{> seeInField }}
1601
1635
  */
1602
1636
  async seeInField(field, value) {
1603
- return proceedSeeInField.call(this, 'assert', field, value);
1637
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1638
+ return proceedSeeInField.call(this, 'assert', field, _value);
1604
1639
  }
1605
1640
 
1606
1641
  /**
1607
1642
  * {{> dontSeeInField }}
1608
1643
  */
1609
1644
  async dontSeeInField(field, value) {
1610
- return proceedSeeInField.call(this, 'negate', field, value);
1645
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1646
+ return proceedSeeInField.call(this, 'negate', field, _value);
1611
1647
  }
1612
1648
 
1613
1649
  /**
@@ -1634,7 +1670,8 @@ class Playwright extends Helper {
1634
1670
  assertElementExists(els, select, 'Selectable field');
1635
1671
  const el = els[0];
1636
1672
 
1637
- highlightActiveElement.call(this, el, this.page);
1673
+ highlightActiveElement.call(this, el, await this._getContext());
1674
+
1638
1675
  if (!Array.isArray(option)) option = [option];
1639
1676
 
1640
1677
  await el.selectOption(option);
@@ -2515,14 +2552,17 @@ class Playwright extends Helper {
2515
2552
 
2516
2553
  // iframe by selector
2517
2554
  const els = await this._locate(locator);
2518
- // assertElementExists(els, locator);
2555
+ if (!els[0]) {
2556
+ throw new Error(`Element ${JSON.stringify(locator)} was not found by text|CSS|XPath`);
2557
+ }
2519
2558
 
2520
2559
  // get content of the first iframe
2521
- if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
2560
+ locator = new Locator(locator, 'css');
2561
+ if ((locator.frame && locator.frame === 'iframe') || locator.value.toLowerCase() === 'iframe') {
2522
2562
  contentFrame = await this.page.frames()[1];
2523
2563
  // get content of the iframe using its name
2524
- } else if (locator.toLowerCase().includes('name=')) {
2525
- const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
2564
+ } else if (locator.value.toLowerCase().includes('name=')) {
2565
+ const frameName = locator.value.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
2526
2566
  contentFrame = await this.page.frame(frameName);
2527
2567
  }
2528
2568
 
@@ -2530,7 +2570,7 @@ class Playwright extends Helper {
2530
2570
  this.context = contentFrame;
2531
2571
  this.contextLocator = null;
2532
2572
  } else {
2533
- this.context = els[0];
2573
+ this.context = this.page.frame(this.page.frames()[1].name());
2534
2574
  this.contextLocator = locator;
2535
2575
  }
2536
2576
  }
@@ -2553,13 +2593,15 @@ class Playwright extends Helper {
2553
2593
  }
2554
2594
 
2555
2595
  /**
2556
- * Waits for navigation to finish. By default takes configured `waitForNavigation` option.
2596
+ * Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
2557
2597
  *
2558
2598
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
2559
2599
  *
2560
2600
  * @param {*} options
2561
2601
  */
2562
2602
  async waitForNavigation(options = {}) {
2603
+ console.log(`waitForNavigation deprecated:
2604
+ * This method is inherently racy, please use 'waitForURL' instead.`);
2563
2605
  options = {
2564
2606
  timeout: this.options.getPageTimeout,
2565
2607
  waitUntil: this.options.waitForNavigation,
@@ -2568,6 +2610,23 @@ class Playwright extends Helper {
2568
2610
  return this.page.waitForNavigation(options);
2569
2611
  }
2570
2612
 
2613
+ /**
2614
+ * Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
2615
+ *
2616
+ * See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
2617
+ *
2618
+ * @param {string|RegExp} url - A glob pattern, regex pattern or predicate receiving URL to match while waiting for the navigation. Note that if the parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to the string.
2619
+ * @param {*} options
2620
+ */
2621
+ async waitForURL(url, options = {}) {
2622
+ options = {
2623
+ timeout: this.options.getPageTimeout,
2624
+ waitUntil: this.options.waitForNavigation,
2625
+ ...options,
2626
+ };
2627
+ return this.page.waitForURL(url, options);
2628
+ }
2629
+
2571
2630
  async waitUntilExists(locator, sec) {
2572
2631
  console.log(`waitUntilExists deprecated:
2573
2632
  * use 'waitForElement' to wait for element to be attached
@@ -2652,16 +2711,16 @@ class Playwright extends Helper {
2652
2711
  }
2653
2712
 
2654
2713
  /**
2655
- * Starts recording of network traffic.
2714
+ * Starts recording the network traffics.
2656
2715
  * This also resets recorded network requests.
2657
2716
  *
2658
2717
  * ```js
2659
2718
  * I.startRecordingTraffic();
2660
2719
  * ```
2661
2720
  *
2662
- * @return {Promise<void>}
2721
+ * @return {void}
2663
2722
  */
2664
- async startRecordingTraffic() {
2723
+ startRecordingTraffic() {
2665
2724
  this.flushNetworkTraffics();
2666
2725
  this.recording = true;
2667
2726
  this.recordedAtLeastOnce = true;
@@ -2672,31 +2731,62 @@ class Playwright extends Helper {
2672
2731
  method: request.method(),
2673
2732
  requestHeaders: request.headers(),
2674
2733
  requestPostData: request.postData(),
2734
+ response: request.response(),
2675
2735
  };
2676
2736
 
2677
2737
  this.debugSection('REQUEST: ', JSON.stringify(information));
2678
2738
 
2679
- information.requestPostData = JSON.parse(information.requestPostData);
2739
+ if (typeof information.requestPostData === 'object') {
2740
+ information.requestPostData = JSON.parse(information.requestPostData);
2741
+ }
2742
+
2680
2743
  this.requests.push(information);
2681
- return this._waitForAction();
2682
2744
  });
2683
2745
  }
2684
2746
 
2685
2747
  /**
2686
2748
  * Grab the recording network traffics
2687
2749
  *
2688
- * @return { Array<any> }
2750
+ * ```js
2751
+ * const traffics = await I.grabRecordedNetworkTraffics();
2752
+ * expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
2753
+ * expect(traffics[0].response.status).to.equal(200);
2754
+ * expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
2755
+ * ```
2756
+ *
2757
+ * @return { Promise<Array<any>> }
2689
2758
  *
2690
2759
  */
2691
- grabRecordedNetworkTraffics() {
2760
+ async grabRecordedNetworkTraffics() {
2692
2761
  if (!this.recording || !this.recordedAtLeastOnce) {
2693
2762
  throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
2694
2763
  }
2764
+
2765
+ const requests = await this.requests;
2766
+ const promises = requests.map(async (request) => request.response.then(
2767
+ async (response) => {
2768
+ let body;
2769
+ try {
2770
+ // There's no 'body' for some requests (redirect etc...)
2771
+ body = JSON.parse((await response.body()).toString());
2772
+ } catch (e) {
2773
+ // only interested in JSON, not HTML responses.
2774
+ }
2775
+
2776
+ request.response = {
2777
+ status: response.status(),
2778
+ statusText: response.statusText(),
2779
+ body,
2780
+ };
2781
+ },
2782
+ ));
2783
+ await Promise.all(promises);
2784
+
2695
2785
  return this.requests;
2696
2786
  }
2697
2787
 
2698
2788
  /**
2699
- * Blocks traffic for URL.
2789
+ * Blocks traffic of a given URL or a list of URLs.
2700
2790
  *
2701
2791
  * Examples:
2702
2792
  *
@@ -2707,16 +2797,30 @@ class Playwright extends Helper {
2707
2797
  * I.blockTraffic(/\.css$/);
2708
2798
  * ```
2709
2799
  *
2710
- * @param url URL to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
2800
+ * ```js
2801
+ * I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
2802
+ * ```
2803
+ *
2804
+ * @param {string|Array|RegExp} urls URL or a list of URLs to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
2711
2805
  */
2712
- async blockTraffic(url) {
2713
- this.page.route(url, (route) => {
2714
- route
2715
- .abort()
2716
- // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2717
- .catch((e) => {});
2718
- });
2719
- return this._waitForAction();
2806
+ blockTraffic(urls) {
2807
+ if (Array.isArray(urls)) {
2808
+ urls.forEach(url => {
2809
+ this.page.route(url, (route) => {
2810
+ route
2811
+ .abort()
2812
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2813
+ .catch((e) => {});
2814
+ });
2815
+ });
2816
+ } else {
2817
+ this.page.route(urls, (route) => {
2818
+ route
2819
+ .abort()
2820
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2821
+ .catch((e) => {});
2822
+ });
2823
+ }
2720
2824
  }
2721
2825
 
2722
2826
  /**
@@ -2735,7 +2839,7 @@ class Playwright extends Helper {
2735
2839
  * @param responseString string The string to return in fake response's body.
2736
2840
  * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
2737
2841
  */
2738
- async mockTraffic(urls, responseString, contentType = 'application/json') {
2842
+ mockTraffic(urls, responseString, contentType = 'application/json') {
2739
2843
  // Required to mock cross-domain requests
2740
2844
  const headers = { 'access-control-allow-origin': '*' };
2741
2845
 
@@ -2757,7 +2861,6 @@ class Playwright extends Helper {
2757
2861
  });
2758
2862
  });
2759
2863
  });
2760
- return this._waitForAction();
2761
2864
  }
2762
2865
 
2763
2866
  /**
@@ -2829,7 +2932,7 @@ class Playwright extends Helper {
2829
2932
  }
2830
2933
 
2831
2934
  if (!this.recording || !this.recordedAtLeastOnce) {
2832
- throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
2935
+ throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
2833
2936
  }
2834
2937
 
2835
2938
  for (let i = 0; i <= timeout * 2; i++) {
@@ -2837,7 +2940,9 @@ class Playwright extends Helper {
2837
2940
  if (found) {
2838
2941
  return true;
2839
2942
  }
2840
- await new Promise((done) => setTimeout(done, 1000));
2943
+ await new Promise((done) => {
2944
+ setTimeout(done, 1000);
2945
+ });
2841
2946
  }
2842
2947
 
2843
2948
  // check request post data
@@ -2974,6 +3079,163 @@ class Playwright extends Helper {
2974
3079
  });
2975
3080
  return dumpedTraffic;
2976
3081
  }
3082
+
3083
+ /**
3084
+ * Starts recording of websocket messages.
3085
+ * This also resets recorded websocket messages.
3086
+ *
3087
+ * ```js
3088
+ * await I.startRecordingWebSocketMessages();
3089
+ * ```
3090
+ *
3091
+ */
3092
+ async startRecordingWebSocketMessages() {
3093
+ this.flushWebSocketMessages();
3094
+ this.recordingWebSocketMessages = true;
3095
+ this.recordedWebSocketMessagesAtLeastOnce = true;
3096
+
3097
+ this.cdpSession = await this.getNewCDPSession();
3098
+ await this.cdpSession.send('Network.enable');
3099
+ await this.cdpSession.send('Page.enable');
3100
+
3101
+ this.cdpSession.on(
3102
+ 'Network.webSocketFrameReceived',
3103
+ (payload) => {
3104
+ this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
3105
+ },
3106
+ );
3107
+
3108
+ this.cdpSession.on(
3109
+ 'Network.webSocketFrameSent',
3110
+ (payload) => {
3111
+ this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
3112
+ },
3113
+ );
3114
+
3115
+ this.cdpSession.on(
3116
+ 'Network.webSocketFrameError',
3117
+ (payload) => {
3118
+ this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
3119
+ },
3120
+ );
3121
+ }
3122
+
3123
+ /**
3124
+ * Stops recording WS messages. Recorded WS messages is not flashed.
3125
+ *
3126
+ * ```js
3127
+ * await I.stopRecordingWebSocketMessages();
3128
+ * ```
3129
+ */
3130
+ async stopRecordingWebSocketMessages() {
3131
+ await this.cdpSession.send('Network.disable');
3132
+ await this.cdpSession.send('Page.disable');
3133
+ this.page.removeAllListeners('Network');
3134
+ this.recordingWebSocketMessages = false;
3135
+ }
3136
+
3137
+ /**
3138
+ * Grab the recording WS messages
3139
+ *
3140
+ * @return { Array<any> }
3141
+ *
3142
+ */
3143
+ grabWebSocketMessages() {
3144
+ if (!this.recordingWebSocketMessages) {
3145
+ if (!this.recordedWebSocketMessagesAtLeastOnce) {
3146
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
3147
+ }
3148
+ }
3149
+ return this.webSocketMessages;
3150
+ }
3151
+
3152
+ /**
3153
+ * Resets all recorded WS messages.
3154
+ */
3155
+ flushWebSocketMessages() {
3156
+ this.webSocketMessages = [];
3157
+ }
3158
+
3159
+ /**
3160
+ * Return a performance metric from the chrome cdp session.
3161
+ * Note: Chrome-only
3162
+ *
3163
+ * Examples:
3164
+ *
3165
+ * ```js
3166
+ * const metrics = await I.grabMetrics();
3167
+ *
3168
+ * // returned metrics
3169
+ *
3170
+ * [
3171
+ * { name: 'Timestamp', value: 1584904.203473 },
3172
+ * { name: 'AudioHandlers', value: 0 },
3173
+ * { name: 'AudioWorkletProcessors', value: 0 },
3174
+ * { name: 'Documents', value: 22 },
3175
+ * { name: 'Frames', value: 10 },
3176
+ * { name: 'JSEventListeners', value: 366 },
3177
+ * { name: 'LayoutObjects', value: 1240 },
3178
+ * { name: 'MediaKeySessions', value: 0 },
3179
+ * { name: 'MediaKeys', value: 0 },
3180
+ * { name: 'Nodes', value: 4505 },
3181
+ * { name: 'Resources', value: 141 },
3182
+ * { name: 'ContextLifecycleStateObservers', value: 34 },
3183
+ * { name: 'V8PerContextDatas', value: 4 },
3184
+ * { name: 'WorkerGlobalScopes', value: 0 },
3185
+ * { name: 'UACSSResources', value: 0 },
3186
+ * { name: 'RTCPeerConnections', value: 0 },
3187
+ * { name: 'ResourceFetchers', value: 22 },
3188
+ * { name: 'AdSubframes', value: 0 },
3189
+ * { name: 'DetachedScriptStates', value: 2 },
3190
+ * { name: 'ArrayBufferContents', value: 1 },
3191
+ * { name: 'LayoutCount', value: 0 },
3192
+ * { name: 'RecalcStyleCount', value: 0 },
3193
+ * { name: 'LayoutDuration', value: 0 },
3194
+ * { name: 'RecalcStyleDuration', value: 0 },
3195
+ * { name: 'DevToolsCommandDuration', value: 0.000013 },
3196
+ * { name: 'ScriptDuration', value: 0 },
3197
+ * { name: 'V8CompileDuration', value: 0 },
3198
+ * { name: 'TaskDuration', value: 0.000014 },
3199
+ * { name: 'TaskOtherDuration', value: 0.000001 },
3200
+ * { name: 'ThreadTime', value: 0.000046 },
3201
+ * { name: 'ProcessTime', value: 0.616852 },
3202
+ * { name: 'JSHeapUsedSize', value: 19004908 },
3203
+ * { name: 'JSHeapTotalSize', value: 26820608 },
3204
+ * { name: 'FirstMeaningfulPaint', value: 0 },
3205
+ * { name: 'DomContentLoaded', value: 1584903.690491 },
3206
+ * { name: 'NavigationStart', value: 1584902.841845 }
3207
+ * ]
3208
+ *
3209
+ * ```
3210
+ *
3211
+ * @return {Promise<Array<Object>>}
3212
+ */
3213
+ async grabMetrics() {
3214
+ const client = await this.page.context().newCDPSession(this.page);
3215
+ await client.send('Performance.enable');
3216
+ const perfMetricObject = await client.send('Performance.getMetrics');
3217
+ return perfMetricObject?.metrics;
3218
+ }
3219
+
3220
+ _getWebSocketMessage(payload) {
3221
+ if (payload.errorMessage) {
3222
+ return payload.errorMessage;
3223
+ }
3224
+
3225
+ return payload.response.payloadData;
3226
+ }
3227
+
3228
+ _getWebSocketLog(prefix, payload) {
3229
+ return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
3230
+ }
3231
+
3232
+ async getNewCDPSession() {
3233
+ return this.page.context().newCDPSession(this.page);
3234
+ }
3235
+
3236
+ _logWebsocketMessages(message) {
3237
+ this.webSocketMessages += message;
3238
+ }
2977
3239
  }
2978
3240
 
2979
3241
  module.exports = Playwright;
@@ -3028,7 +3290,7 @@ async function proceedClick(locator, context = null, options = {}) {
3028
3290
  assertElementExists(els, locator, 'Clickable element');
3029
3291
  }
3030
3292
 
3031
- highlightActiveElement.call(this, els[0], this.page);
3293
+ highlightActiveElement.call(this, els[0], await this._getContext());
3032
3294
 
3033
3295
  /*
3034
3296
  using the force true options itself but instead dispatching a click
@@ -3041,7 +3303,7 @@ async function proceedClick(locator, context = null, options = {}) {
3041
3303
  }
3042
3304
  const promises = [];
3043
3305
  if (options.waitForNavigation) {
3044
- promises.push(this.waitForNavigation());
3306
+ promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
3045
3307
  }
3046
3308
  promises.push(this._waitForAction());
3047
3309
 
@@ -3097,7 +3359,7 @@ async function proceedSee(assertType, text, context, strict = false) {
3097
3359
  if (strict) {
3098
3360
  return allText.map(elText => equals(description)[assertType](text, elText));
3099
3361
  }
3100
- return stringIncludes(description)[assertType](text, allText.join(' | '));
3362
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
3101
3363
  }
3102
3364
 
3103
3365
  async function findCheckable(locator, context) {
@@ -3206,7 +3468,16 @@ async function proceedSeeInField(assertType, field, value) {
3206
3468
  return proceedMultiple(els[0]);
3207
3469
  }
3208
3470
 
3209
- const fieldVal = await el.inputValue();
3471
+ let fieldVal;
3472
+
3473
+ try {
3474
+ fieldVal = await el.inputValue();
3475
+ } catch (e) {
3476
+ if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
3477
+ fieldVal = await el.innerText();
3478
+ }
3479
+ }
3480
+
3210
3481
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
3211
3482
  }
3212
3483
 
@@ -3247,7 +3518,10 @@ async function elementSelected(element) {
3247
3518
 
3248
3519
  function isFrameLocator(locator) {
3249
3520
  locator = new Locator(locator);
3250
- if (locator.isFrame()) return locator.value;
3521
+ if (locator.isFrame()) {
3522
+ const _locator = new Locator(locator.value);
3523
+ return _locator.value;
3524
+ }
3251
3525
  return false;
3252
3526
  }
3253
3527
 
@@ -3443,7 +3717,7 @@ async function saveTraceForContext(context, name) {
3443
3717
  }
3444
3718
 
3445
3719
  function highlightActiveElement(element, context) {
3446
- if (!this.options.enableHighlight && !store.debugMode) return;
3720
+ if (!this.options.highlightElement && !store.debugMode) return;
3447
3721
 
3448
3722
  highlightElement(element, context);
3449
3723
  }