codeceptjs 3.5.3 → 3.5.4

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/README.md +0 -2
  2. package/docs/build/Appium.js +8 -6
  3. package/docs/build/GraphQL.js +25 -0
  4. package/docs/build/Nightmare.js +11 -6
  5. package/docs/build/Playwright.js +425 -193
  6. package/docs/build/Protractor.js +13 -8
  7. package/docs/build/Puppeteer.js +20 -14
  8. package/docs/build/TestCafe.js +17 -10
  9. package/docs/build/WebDriver.js +41 -37
  10. package/docs/changelog.md +170 -1
  11. package/docs/community-helpers.md +8 -4
  12. package/docs/examples.md +8 -2
  13. package/docs/helpers/Appium.md +2 -2
  14. package/docs/helpers/GraphQL.md +21 -0
  15. package/docs/helpers/Nightmare.md +2 -2
  16. package/docs/helpers/Playwright.md +239 -122
  17. package/docs/helpers/Protractor.md +2 -2
  18. package/docs/helpers/Puppeteer.md +3 -3
  19. package/docs/helpers/TestCafe.md +2 -2
  20. package/docs/helpers/WebDriver.md +3 -3
  21. package/docs/playwright.md +24 -1
  22. package/docs/webapi/dontSeeInField.mustache +1 -1
  23. package/docs/webapi/seeInField.mustache +1 -1
  24. package/docs/wiki/Books-&-Posts.md +0 -0
  25. package/docs/wiki/Community-Helpers-&-Plugins.md +8 -4
  26. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +46 -14
  27. package/docs/wiki/Examples.md +8 -2
  28. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -0
  29. package/docs/wiki/Home.md +0 -0
  30. package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
  31. package/docs/wiki/Release-Process.md +0 -0
  32. package/docs/wiki/Roadmap.md +0 -0
  33. package/docs/wiki/Tests.md +0 -0
  34. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -0
  35. package/docs/wiki/Videos.md +0 -0
  36. package/lib/command/definitions.js +2 -7
  37. package/lib/command/run-multiple/collection.js +17 -5
  38. package/lib/helper/Appium.js +6 -4
  39. package/lib/helper/GraphQL.js +25 -0
  40. package/lib/helper/Nightmare.js +9 -4
  41. package/lib/helper/Playwright.js +422 -190
  42. package/lib/helper/Protractor.js +11 -6
  43. package/lib/helper/Puppeteer.js +18 -12
  44. package/lib/helper/TestCafe.js +15 -8
  45. package/lib/helper/WebDriver.js +39 -35
  46. package/lib/helper/errors/ElementNotFound.js +2 -1
  47. package/lib/helper/extras/PlaywrightReact.js +9 -0
  48. package/lib/helper/scripts/highlightElement.js +1 -1
  49. package/lib/interfaces/bdd.js +1 -1
  50. package/lib/mochaFactory.js +2 -1
  51. package/lib/pause.js +5 -4
  52. package/lib/plugin/heal.js +2 -3
  53. package/lib/plugin/selenoid.js +6 -1
  54. package/lib/step.js +27 -10
  55. package/lib/utils.js +4 -0
  56. package/lib/workers.js +3 -1
  57. package/package.json +14 -14
  58. package/typings/promiseBasedTypes.d.ts +145 -126
  59. package/typings/types.d.ts +152 -133
  60. package/CHANGELOG.md +0 -2563
  61. package/docs/build/Polly.js +0 -42
  62. 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 = {
@@ -487,8 +496,17 @@ class Playwright extends Helper {
487
496
  if (this.isElectron) {
488
497
  mainPage = await this.browser.firstWindow();
489
498
  } else {
490
- const existingPages = await this.browserContext.pages();
491
- mainPage = existingPages[0] || await this.browserContext.newPage();
499
+ try {
500
+ const existingPages = await this.browserContext.pages();
501
+ mainPage = existingPages[0] || await this.browserContext.newPage();
502
+ } catch (e) {
503
+ if (this.playwrightOptions.userDataDir) {
504
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
505
+ this.browserContext = this.browser;
506
+ const existingPages = await this.browserContext.pages();
507
+ mainPage = existingPages[0];
508
+ }
509
+ }
492
510
  }
493
511
  await targetCreatedHandler.call(this, mainPage);
494
512
 
@@ -519,13 +537,15 @@ class Playwright extends Helper {
519
537
 
520
538
  // close other sessions
521
539
  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
- }
540
+ if ((await this.browser)._type === 'Browser') {
541
+ const contexts = await this.browser.contexts();
542
+ const currentContext = contexts[0];
543
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
544
+ this.storageState = await currentContext.storageState();
545
+ }
527
546
 
528
- await Promise.all(contexts.map(c => c.close()));
547
+ await Promise.all(contexts.map(c => c.close()));
548
+ }
529
549
  } catch (e) {
530
550
  console.log(e);
531
551
  }
@@ -555,8 +575,16 @@ class Playwright extends Helper {
555
575
  browserContext = browser.context();
556
576
  page = await browser.firstWindow();
557
577
  } else {
558
- browserContext = await this.browser.newContext(Object.assign(this.options, config));
559
- page = await browserContext.newPage();
578
+ try {
579
+ browserContext = await this.browser.newContext(Object.assign(this.options, config));
580
+ page = await browserContext.newPage();
581
+ } catch (e) {
582
+ if (this.playwrightOptions.userDataDir) {
583
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
584
+ this.browser = browserContext;
585
+ page = await browserContext.pages()[0];
586
+ }
587
+ }
560
588
  }
561
589
 
562
590
  if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
@@ -569,10 +597,12 @@ class Playwright extends Helper {
569
597
  // is closed by _after
570
598
  },
571
599
  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]);
600
+ if (context) {
601
+ this.browserContext = context;
602
+ const existingPages = await context.pages();
603
+ this.sessionPages[this.activeSessionName] = existingPages[0];
604
+ return this._setPage(this.sessionPages[this.activeSessionName]);
605
+ }
576
606
  },
577
607
  restoreVars: async (session) => {
578
608
  this.withinLocator = null;
@@ -763,7 +793,7 @@ class Playwright extends Helper {
763
793
  }
764
794
  throw err;
765
795
  }
766
- } else if (this.userDataDir) {
796
+ } else if (this.playwrightOptions.userDataDir) {
767
797
  this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
768
798
  } else {
769
799
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
@@ -824,9 +854,9 @@ class Playwright extends Helper {
824
854
  return;
825
855
  }
826
856
 
827
- const els = await this._locate(locator);
828
- assertElementExists(els, locator);
829
- this.context = els[0];
857
+ const el = await this._locateElement(locator);
858
+ assertElementExists(el, locator);
859
+ this.context = el;
830
860
  this.contextLocator = locator;
831
861
 
832
862
  this.withinLocator = new Locator(locator);
@@ -929,11 +959,11 @@ class Playwright extends Helper {
929
959
  *
930
960
  */
931
961
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
932
- const els = await this._locate(locator);
933
- assertElementExists(els, locator);
962
+ const el = await this._locateElement(locator);
963
+ assertElementExists(el, locator);
934
964
 
935
965
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
936
- const { x, y } = await clickablePoint(els[0]);
966
+ const { x, y } = await clickablePoint(el);
937
967
  await this.page.mouse.move(x + offsetX, y + offsetY);
938
968
  return this._waitForAction();
939
969
  }
@@ -943,9 +973,8 @@ class Playwright extends Helper {
943
973
  *
944
974
  */
945
975
  async focus(locator, options = {}) {
946
- const els = await this._locate(locator);
947
- assertElementExists(els, locator, 'Element to focus');
948
- const el = els[0];
976
+ const el = await this._locateElement(locator);
977
+ assertElementExists(el, locator, 'Element to focus');
949
978
 
950
979
  await el.focus(options);
951
980
  return this._waitForAction();
@@ -956,12 +985,10 @@ class Playwright extends Helper {
956
985
  *
957
986
  */
958
987
  async blur(locator, options = {}) {
959
- const els = await this._locate(locator);
960
- assertElementExists(els, locator, 'Element to blur');
961
- // TODO: locator change required after #3677 implementation
962
- const elXpath = await getXPathForElement(els[0]);
988
+ const el = await this._locateElement(locator);
989
+ assertElementExists(el, locator, 'Element to blur');
963
990
 
964
- await this.page.locator(elXpath).blur(options);
991
+ await el.blur(options);
965
992
  return this._waitForAction();
966
993
  }
967
994
 
@@ -1041,8 +1068,11 @@ class Playwright extends Helper {
1041
1068
  const body = document.body;
1042
1069
  const html = document.documentElement;
1043
1070
  window.scrollTo(0, Math.max(
1044
- body.scrollHeight, body.offsetHeight,
1045
- html.clientHeight, html.scrollHeight, html.offsetHeight,
1071
+ body.scrollHeight,
1072
+ body.offsetHeight,
1073
+ html.clientHeight,
1074
+ html.scrollHeight,
1075
+ html.offsetHeight,
1046
1076
  ));
1047
1077
  });
1048
1078
  }
@@ -1058,10 +1088,10 @@ class Playwright extends Helper {
1058
1088
  }
1059
1089
 
1060
1090
  if (locator) {
1061
- const els = await this._locate(locator);
1062
- assertElementExists(els, locator, 'Element');
1063
- await els[0].scrollIntoViewIfNeeded();
1064
- const elementCoordinates = await clickablePoint(els[0]);
1091
+ const el = await this._locateElement(locator);
1092
+ assertElementExists(el, locator, 'Element');
1093
+ await el.scrollIntoViewIfNeeded();
1094
+ const elementCoordinates = await clickablePoint(el);
1065
1095
  await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
1066
1096
  } else {
1067
1097
  await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
@@ -1129,7 +1159,20 @@ class Playwright extends Helper {
1129
1159
  }
1130
1160
 
1131
1161
  /**
1132
- * Find a checkbox by providing human readable text:
1162
+ * Get the first element by different locator types, including strict locator
1163
+ * Should be used in custom helpers:
1164
+ *
1165
+ * ```js
1166
+ * const element = await this.helpers['Playwright']._locateElement({name: 'password'});
1167
+ * ```
1168
+ */
1169
+ async _locateElement(locator) {
1170
+ const context = await this.context || await this._getContext();
1171
+ return findElement(context, locator);
1172
+ }
1173
+
1174
+ /**
1175
+ * Find a checkbox by providing human-readable text:
1133
1176
  * NOTE: Assumes the checkable element exists
1134
1177
  *
1135
1178
  * ```js
@@ -1144,7 +1187,7 @@ class Playwright extends Helper {
1144
1187
  }
1145
1188
 
1146
1189
  /**
1147
- * Find a clickable element by providing human readable text:
1190
+ * Find a clickable element by providing human-readable text:
1148
1191
  *
1149
1192
  * ```js
1150
1193
  * this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -1156,7 +1199,7 @@ class Playwright extends Helper {
1156
1199
  }
1157
1200
 
1158
1201
  /**
1159
- * Find field elements by providing human readable text:
1202
+ * Find field elements by providing human-readable text:
1160
1203
  *
1161
1204
  * ```js
1162
1205
  * this.helpers['Playwright']._locateFields('Your email').then // ...
@@ -1532,15 +1575,10 @@ class Playwright extends Helper {
1532
1575
  const els = await findFields.call(this, field);
1533
1576
  assertElementExists(els, field, 'Field');
1534
1577
  const el = els[0];
1535
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
1536
- const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
1537
- if (tag === 'INPUT' || tag === 'TEXTAREA') {
1538
- await this._evaluateHandeInContext(el => el.value = '', el);
1539
- } else if (editable) {
1540
- await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1541
- }
1542
1578
 
1543
- highlightActiveElement.call(this, el, this.page);
1579
+ await el.clear();
1580
+
1581
+ highlightActiveElement.call(this, el, await this._getContext());
1544
1582
 
1545
1583
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1546
1584
 
@@ -1565,21 +1603,16 @@ class Playwright extends Helper {
1565
1603
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1566
1604
  */
1567
1605
  async clearField(locator, options = {}) {
1568
- let result;
1569
- const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
1606
+ const els = await findFields.call(this, locator);
1607
+ assertElementExists(els, locator, 'Field to clear');
1570
1608
 
1571
- if (isNewClearMethodPresent) {
1572
- const els = await findFields.call(this, locator);
1573
- assertElementExists(els, locator, 'Field to clear');
1574
- // TODO: locator change required after #3677 implementation
1575
- const elXpath = await getXPathForElement(els[0]);
1609
+ const el = els[0];
1576
1610
 
1577
- await this.page.locator(elXpath).clear(options);
1578
- result = await this._waitForAction();
1579
- } else {
1580
- result = await this.fillField(locator, '');
1581
- }
1582
- return result;
1611
+ highlightActiveElement.call(this, el, this.page);
1612
+
1613
+ await el.clear();
1614
+
1615
+ return this._waitForAction();
1583
1616
  }
1584
1617
 
1585
1618
  /**
@@ -1590,7 +1623,7 @@ class Playwright extends Helper {
1590
1623
  async appendField(field, value) {
1591
1624
  const els = await findFields.call(this, field);
1592
1625
  assertElementExists(els, field, 'Field');
1593
- highlightActiveElement.call(this, els[0], this.page);
1626
+ highlightActiveElement.call(this, els[0], await this._getContext());
1594
1627
  await els[0].press('End');
1595
1628
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1596
1629
  return this._waitForAction();
@@ -1600,14 +1633,16 @@ class Playwright extends Helper {
1600
1633
  * {{> seeInField }}
1601
1634
  */
1602
1635
  async seeInField(field, value) {
1603
- return proceedSeeInField.call(this, 'assert', field, value);
1636
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1637
+ return proceedSeeInField.call(this, 'assert', field, _value);
1604
1638
  }
1605
1639
 
1606
1640
  /**
1607
1641
  * {{> dontSeeInField }}
1608
1642
  */
1609
1643
  async dontSeeInField(field, value) {
1610
- return proceedSeeInField.call(this, 'negate', field, value);
1644
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1645
+ return proceedSeeInField.call(this, 'negate', field, _value);
1611
1646
  }
1612
1647
 
1613
1648
  /**
@@ -1633,29 +1668,12 @@ class Playwright extends Helper {
1633
1668
  const els = await findFields.call(this, select);
1634
1669
  assertElementExists(els, select, 'Selectable field');
1635
1670
  const el = els[0];
1636
- if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1637
- throw new Error('Element is not <select>');
1638
- }
1639
- highlightActiveElement.call(this, el, this.page);
1640
- if (!Array.isArray(option)) option = [option];
1641
1671
 
1642
- for (const key in option) {
1643
- const opt = xpathLocator.literal(option[key]);
1644
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
1645
- if (optEl.length) {
1646
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1647
- continue;
1648
- }
1649
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
1650
- if (optEl.length) {
1651
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1652
- }
1653
- }
1654
- await this._evaluateHandeInContext((element) => {
1655
- element.dispatchEvent(new Event('input', { bubbles: true }));
1656
- element.dispatchEvent(new Event('change', { bubbles: true }));
1657
- }, el);
1672
+ highlightActiveElement.call(this, el, await this._getContext());
1673
+
1674
+ if (!Array.isArray(option)) option = [option];
1658
1675
 
1676
+ await el.selectOption(option);
1659
1677
  return this._waitForAction();
1660
1678
  }
1661
1679
 
@@ -1906,7 +1924,7 @@ class Playwright extends Helper {
1906
1924
  const els = await this._locate(locator);
1907
1925
  const texts = [];
1908
1926
  for (const el of els) {
1909
- texts.push(await (await el.getProperty('innerText')).jsonValue());
1927
+ texts.push(await (await el.innerText()));
1910
1928
  }
1911
1929
  this.debug(`Matched ${els.length} elements`);
1912
1930
  return texts;
@@ -1928,7 +1946,7 @@ class Playwright extends Helper {
1928
1946
  async grabValueFromAll(locator) {
1929
1947
  const els = await findFields.call(this, locator);
1930
1948
  this.debug(`Matched ${els.length} elements`);
1931
- return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
1949
+ return Promise.all(els.map(el => el.inputValue()));
1932
1950
  }
1933
1951
 
1934
1952
  /**
@@ -1947,7 +1965,7 @@ class Playwright extends Helper {
1947
1965
  async grabHTMLFromAll(locator) {
1948
1966
  const els = await this._locate(locator);
1949
1967
  this.debug(`Matched ${els.length} elements`);
1950
- return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
1968
+ return Promise.all(els.map(el => el.innerHTML()));
1951
1969
  }
1952
1970
 
1953
1971
  /**
@@ -1968,7 +1986,7 @@ class Playwright extends Helper {
1968
1986
  async grabCssPropertyFromAll(locator, cssProperty) {
1969
1987
  const els = await this._locate(locator);
1970
1988
  this.debug(`Matched ${els.length} elements`);
1971
- const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
1989
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
1972
1990
 
1973
1991
  return cssValues;
1974
1992
  }
@@ -1984,21 +2002,20 @@ class Playwright extends Helper {
1984
2002
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
1985
2003
  const elemAmount = res.length;
1986
2004
  const commands = [];
1987
- res.forEach((el) => {
1988
- Object.keys(cssPropertiesCamelCase).forEach((prop) => {
1989
- commands.push(el.$eval('xpath=.', (el) => {
1990
- const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
1991
- return JSON.parse(JSON.stringify(style));
1992
- }, el)
1993
- .then((props) => {
1994
- if (isColorProperty(prop)) {
1995
- return convertColorToRGBA(props[prop]);
1996
- }
1997
- return props[prop];
1998
- }));
2005
+ let props = [];
2006
+
2007
+ for (const element of res) {
2008
+ const cssProperties = await element.evaluate((el) => getComputedStyle(el));
2009
+
2010
+ Object.keys(cssPropertiesCamelCase).forEach(prop => {
2011
+ if (isColorProperty(prop)) {
2012
+ props.push(convertColorToRGBA(cssProperties[prop]));
2013
+ } else {
2014
+ props.push(cssProperties[prop]);
2015
+ }
1999
2016
  });
2000
- });
2001
- let props = await Promise.all(commands);
2017
+ }
2018
+
2002
2019
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
2003
2020
  if (!Array.isArray(props)) props = [props];
2004
2021
  let chunked = chunkArray(props, values.length);
@@ -2024,7 +2041,7 @@ class Playwright extends Helper {
2024
2041
  res.forEach((el) => {
2025
2042
  Object.keys(attributes).forEach((prop) => {
2026
2043
  commands.push(el
2027
- .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
2044
+ .evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
2028
2045
  });
2029
2046
  });
2030
2047
  let attrs = await Promise.all(commands);
@@ -2045,11 +2062,11 @@ class Playwright extends Helper {
2045
2062
  *
2046
2063
  */
2047
2064
  async dragSlider(locator, offsetX = 0) {
2048
- const src = await this._locate(locator);
2065
+ const src = await this._locateElement(locator);
2049
2066
  assertElementExists(src, locator, 'Slider Element');
2050
2067
 
2051
2068
  // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
2052
- const sliderSource = await clickablePoint(src[0]);
2069
+ const sliderSource = await clickablePoint(src);
2053
2070
 
2054
2071
  // Drag start point
2055
2072
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2083,8 +2100,7 @@ class Playwright extends Helper {
2083
2100
  const array = [];
2084
2101
 
2085
2102
  for (let index = 0; index < els.length; index++) {
2086
- const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
2087
- array.push(await a.jsonValue());
2103
+ array.push(await els[index].getAttribute(attr));
2088
2104
  }
2089
2105
 
2090
2106
  return array;
@@ -2097,10 +2113,9 @@ class Playwright extends Helper {
2097
2113
  async saveElementScreenshot(locator, fileName) {
2098
2114
  const outputFile = screenshotOutputFolder(fileName);
2099
2115
 
2100
- const res = await this._locate(locator);
2116
+ const res = await this._locateElement(locator);
2101
2117
  assertElementExists(res, locator);
2102
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
2103
- const elem = res[0];
2118
+ const elem = res;
2104
2119
  this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
2105
2120
  return elem.screenshot({ path: outputFile, type: 'png' });
2106
2121
  }
@@ -2526,16 +2541,26 @@ class Playwright extends Helper {
2526
2541
  }
2527
2542
  return;
2528
2543
  }
2544
+ let contentFrame;
2545
+
2529
2546
  if (!locator) {
2530
- this.context = this.page;
2547
+ this.context = await this.page.frames()[0];
2531
2548
  this.contextLocator = null;
2532
2549
  return;
2533
2550
  }
2534
2551
 
2535
2552
  // iframe by selector
2536
2553
  const els = await this._locate(locator);
2537
- assertElementExists(els, locator);
2538
- const contentFrame = await els[0].contentFrame();
2554
+ // assertElementExists(els, locator);
2555
+
2556
+ // get content of the first iframe
2557
+ if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
2558
+ contentFrame = await this.page.frames()[1];
2559
+ // get content of the iframe using its name
2560
+ } else if (locator.toLowerCase().includes('name=')) {
2561
+ const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
2562
+ contentFrame = await this.page.frame(frameName);
2563
+ }
2539
2564
 
2540
2565
  if (contentFrame) {
2541
2566
  this.context = contentFrame;
@@ -2564,13 +2589,15 @@ class Playwright extends Helper {
2564
2589
  }
2565
2590
 
2566
2591
  /**
2567
- * Waits for navigation to finish. By default takes configured `waitForNavigation` option.
2592
+ * Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
2568
2593
  *
2569
2594
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
2570
2595
  *
2571
2596
  * @param {*} options
2572
2597
  */
2573
2598
  async waitForNavigation(options = {}) {
2599
+ console.log(`waitForNavigation deprecated:
2600
+ * This method is inherently racy, please use 'waitForURL' instead.`);
2574
2601
  options = {
2575
2602
  timeout: this.options.getPageTimeout,
2576
2603
  waitUntil: this.options.waitForNavigation,
@@ -2579,6 +2606,23 @@ class Playwright extends Helper {
2579
2606
  return this.page.waitForNavigation(options);
2580
2607
  }
2581
2608
 
2609
+ /**
2610
+ * Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
2611
+ *
2612
+ * See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
2613
+ *
2614
+ * @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.
2615
+ * @param {*} options
2616
+ */
2617
+ async waitForURL(url, options = {}) {
2618
+ options = {
2619
+ timeout: this.options.getPageTimeout,
2620
+ waitUntil: this.options.waitForNavigation,
2621
+ ...options,
2622
+ };
2623
+ return this.page.waitForURL(url, options);
2624
+ }
2625
+
2582
2626
  async waitUntilExists(locator, sec) {
2583
2627
  console.log(`waitUntilExists deprecated:
2584
2628
  * use 'waitForElement' to wait for element to be attached
@@ -2624,9 +2668,9 @@ class Playwright extends Helper {
2624
2668
  * {{> grabElementBoundingRect }}
2625
2669
  */
2626
2670
  async grabElementBoundingRect(locator, prop) {
2627
- const els = await this._locate(locator);
2628
- assertElementExists(els, locator);
2629
- const rect = await els[0].boundingBox();
2671
+ const el = await this._locateElement(locator);
2672
+ assertElementExists(el, locator);
2673
+ const rect = await el.boundingBox();
2630
2674
  if (prop) return rect[prop];
2631
2675
  return rect;
2632
2676
  }
@@ -2663,16 +2707,16 @@ class Playwright extends Helper {
2663
2707
  }
2664
2708
 
2665
2709
  /**
2666
- * Starts recording of network traffic.
2710
+ * Starts recording the network traffics.
2667
2711
  * This also resets recorded network requests.
2668
2712
  *
2669
2713
  * ```js
2670
2714
  * I.startRecordingTraffic();
2671
2715
  * ```
2672
2716
  *
2673
- * @return {Promise<void>}
2717
+ * @return {void}
2674
2718
  */
2675
- async startRecordingTraffic() {
2719
+ startRecordingTraffic() {
2676
2720
  this.flushNetworkTraffics();
2677
2721
  this.recording = true;
2678
2722
  this.recordedAtLeastOnce = true;
@@ -2683,31 +2727,62 @@ class Playwright extends Helper {
2683
2727
  method: request.method(),
2684
2728
  requestHeaders: request.headers(),
2685
2729
  requestPostData: request.postData(),
2730
+ response: request.response(),
2686
2731
  };
2687
2732
 
2688
2733
  this.debugSection('REQUEST: ', JSON.stringify(information));
2689
2734
 
2690
- information.requestPostData = JSON.parse(information.requestPostData);
2735
+ if (typeof information.requestPostData === 'object') {
2736
+ information.requestPostData = JSON.parse(information.requestPostData);
2737
+ }
2738
+
2691
2739
  this.requests.push(information);
2692
- return this._waitForAction();
2693
2740
  });
2694
2741
  }
2695
2742
 
2696
2743
  /**
2697
2744
  * Grab the recording network traffics
2698
2745
  *
2699
- * @return { Array<any> }
2746
+ * ```js
2747
+ * const traffics = await I.grabRecordedNetworkTraffics();
2748
+ * expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
2749
+ * expect(traffics[0].response.status).to.equal(200);
2750
+ * expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
2751
+ * ```
2752
+ *
2753
+ * @return { Promise<Array<any>> }
2700
2754
  *
2701
2755
  */
2702
- grabRecordedNetworkTraffics() {
2756
+ async grabRecordedNetworkTraffics() {
2703
2757
  if (!this.recording || !this.recordedAtLeastOnce) {
2704
2758
  throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
2705
2759
  }
2760
+
2761
+ const requests = await this.requests;
2762
+ const promises = requests.map(async (request) => request.response.then(
2763
+ async (response) => {
2764
+ let body;
2765
+ try {
2766
+ // There's no 'body' for some requests (redirect etc...)
2767
+ body = JSON.parse((await response.body()).toString());
2768
+ } catch (e) {
2769
+ // only interested in JSON, not HTML responses.
2770
+ }
2771
+
2772
+ request.response = {
2773
+ status: response.status(),
2774
+ statusText: response.statusText(),
2775
+ body,
2776
+ };
2777
+ },
2778
+ ));
2779
+ await Promise.all(promises);
2780
+
2706
2781
  return this.requests;
2707
2782
  }
2708
2783
 
2709
2784
  /**
2710
- * Blocks traffic for URL.
2785
+ * Blocks traffic of a given URL or a list of URLs.
2711
2786
  *
2712
2787
  * Examples:
2713
2788
  *
@@ -2718,16 +2793,30 @@ class Playwright extends Helper {
2718
2793
  * I.blockTraffic(/\.css$/);
2719
2794
  * ```
2720
2795
  *
2721
- * @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.
2796
+ * ```js
2797
+ * I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
2798
+ * ```
2799
+ *
2800
+ * @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.
2722
2801
  */
2723
- async blockTraffic(url) {
2724
- this.page.route(url, (route) => {
2725
- route
2726
- .abort()
2727
- // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2728
- .catch((e) => {});
2729
- });
2730
- return this._waitForAction();
2802
+ blockTraffic(urls) {
2803
+ if (Array.isArray(urls)) {
2804
+ urls.forEach(url => {
2805
+ this.page.route(url, (route) => {
2806
+ route
2807
+ .abort()
2808
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2809
+ .catch((e) => {});
2810
+ });
2811
+ });
2812
+ } else {
2813
+ this.page.route(urls, (route) => {
2814
+ route
2815
+ .abort()
2816
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2817
+ .catch((e) => {});
2818
+ });
2819
+ }
2731
2820
  }
2732
2821
 
2733
2822
  /**
@@ -2746,7 +2835,7 @@ class Playwright extends Helper {
2746
2835
  * @param responseString string The string to return in fake response's body.
2747
2836
  * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
2748
2837
  */
2749
- async mockTraffic(urls, responseString, contentType = 'application/json') {
2838
+ mockTraffic(urls, responseString, contentType = 'application/json') {
2750
2839
  // Required to mock cross-domain requests
2751
2840
  const headers = { 'access-control-allow-origin': '*' };
2752
2841
 
@@ -2768,7 +2857,6 @@ class Playwright extends Helper {
2768
2857
  });
2769
2858
  });
2770
2859
  });
2771
- return this._waitForAction();
2772
2860
  }
2773
2861
 
2774
2862
  /**
@@ -2840,7 +2928,7 @@ class Playwright extends Helper {
2840
2928
  }
2841
2929
 
2842
2930
  if (!this.recording || !this.recordedAtLeastOnce) {
2843
- throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
2931
+ throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
2844
2932
  }
2845
2933
 
2846
2934
  for (let i = 0; i <= timeout * 2; i++) {
@@ -2848,7 +2936,9 @@ class Playwright extends Helper {
2848
2936
  if (found) {
2849
2937
  return true;
2850
2938
  }
2851
- await new Promise((done) => setTimeout(done, 1000));
2939
+ await new Promise((done) => {
2940
+ setTimeout(done, 1000);
2941
+ });
2852
2942
  }
2853
2943
 
2854
2944
  // check request post data
@@ -2985,6 +3075,163 @@ class Playwright extends Helper {
2985
3075
  });
2986
3076
  return dumpedTraffic;
2987
3077
  }
3078
+
3079
+ /**
3080
+ * Starts recording of websocket messages.
3081
+ * This also resets recorded websocket messages.
3082
+ *
3083
+ * ```js
3084
+ * await I.startRecordingWebSocketMessages();
3085
+ * ```
3086
+ *
3087
+ */
3088
+ async startRecordingWebSocketMessages() {
3089
+ this.flushWebSocketMessages();
3090
+ this.recordingWebSocketMessages = true;
3091
+ this.recordedWebSocketMessagesAtLeastOnce = true;
3092
+
3093
+ this.cdpSession = await this.getNewCDPSession();
3094
+ await this.cdpSession.send('Network.enable');
3095
+ await this.cdpSession.send('Page.enable');
3096
+
3097
+ this.cdpSession.on(
3098
+ 'Network.webSocketFrameReceived',
3099
+ (payload) => {
3100
+ this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
3101
+ },
3102
+ );
3103
+
3104
+ this.cdpSession.on(
3105
+ 'Network.webSocketFrameSent',
3106
+ (payload) => {
3107
+ this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
3108
+ },
3109
+ );
3110
+
3111
+ this.cdpSession.on(
3112
+ 'Network.webSocketFrameError',
3113
+ (payload) => {
3114
+ this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
3115
+ },
3116
+ );
3117
+ }
3118
+
3119
+ /**
3120
+ * Stops recording WS messages. Recorded WS messages is not flashed.
3121
+ *
3122
+ * ```js
3123
+ * await I.stopRecordingWebSocketMessages();
3124
+ * ```
3125
+ */
3126
+ async stopRecordingWebSocketMessages() {
3127
+ await this.cdpSession.send('Network.disable');
3128
+ await this.cdpSession.send('Page.disable');
3129
+ this.page.removeAllListeners('Network');
3130
+ this.recordingWebSocketMessages = false;
3131
+ }
3132
+
3133
+ /**
3134
+ * Grab the recording WS messages
3135
+ *
3136
+ * @return { Array<any> }
3137
+ *
3138
+ */
3139
+ grabWebSocketMessages() {
3140
+ if (!this.recordingWebSocketMessages) {
3141
+ if (!this.recordedWebSocketMessagesAtLeastOnce) {
3142
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
3143
+ }
3144
+ }
3145
+ return this.webSocketMessages;
3146
+ }
3147
+
3148
+ /**
3149
+ * Resets all recorded WS messages.
3150
+ */
3151
+ flushWebSocketMessages() {
3152
+ this.webSocketMessages = [];
3153
+ }
3154
+
3155
+ /**
3156
+ * Return a performance metric from the chrome cdp session.
3157
+ * Note: Chrome-only
3158
+ *
3159
+ * Examples:
3160
+ *
3161
+ * ```js
3162
+ * const metrics = await I.grabMetrics();
3163
+ *
3164
+ * // returned metrics
3165
+ *
3166
+ * [
3167
+ * { name: 'Timestamp', value: 1584904.203473 },
3168
+ * { name: 'AudioHandlers', value: 0 },
3169
+ * { name: 'AudioWorkletProcessors', value: 0 },
3170
+ * { name: 'Documents', value: 22 },
3171
+ * { name: 'Frames', value: 10 },
3172
+ * { name: 'JSEventListeners', value: 366 },
3173
+ * { name: 'LayoutObjects', value: 1240 },
3174
+ * { name: 'MediaKeySessions', value: 0 },
3175
+ * { name: 'MediaKeys', value: 0 },
3176
+ * { name: 'Nodes', value: 4505 },
3177
+ * { name: 'Resources', value: 141 },
3178
+ * { name: 'ContextLifecycleStateObservers', value: 34 },
3179
+ * { name: 'V8PerContextDatas', value: 4 },
3180
+ * { name: 'WorkerGlobalScopes', value: 0 },
3181
+ * { name: 'UACSSResources', value: 0 },
3182
+ * { name: 'RTCPeerConnections', value: 0 },
3183
+ * { name: 'ResourceFetchers', value: 22 },
3184
+ * { name: 'AdSubframes', value: 0 },
3185
+ * { name: 'DetachedScriptStates', value: 2 },
3186
+ * { name: 'ArrayBufferContents', value: 1 },
3187
+ * { name: 'LayoutCount', value: 0 },
3188
+ * { name: 'RecalcStyleCount', value: 0 },
3189
+ * { name: 'LayoutDuration', value: 0 },
3190
+ * { name: 'RecalcStyleDuration', value: 0 },
3191
+ * { name: 'DevToolsCommandDuration', value: 0.000013 },
3192
+ * { name: 'ScriptDuration', value: 0 },
3193
+ * { name: 'V8CompileDuration', value: 0 },
3194
+ * { name: 'TaskDuration', value: 0.000014 },
3195
+ * { name: 'TaskOtherDuration', value: 0.000001 },
3196
+ * { name: 'ThreadTime', value: 0.000046 },
3197
+ * { name: 'ProcessTime', value: 0.616852 },
3198
+ * { name: 'JSHeapUsedSize', value: 19004908 },
3199
+ * { name: 'JSHeapTotalSize', value: 26820608 },
3200
+ * { name: 'FirstMeaningfulPaint', value: 0 },
3201
+ * { name: 'DomContentLoaded', value: 1584903.690491 },
3202
+ * { name: 'NavigationStart', value: 1584902.841845 }
3203
+ * ]
3204
+ *
3205
+ * ```
3206
+ *
3207
+ * @return {Promise<Array<Object>>}
3208
+ */
3209
+ async grabMetrics() {
3210
+ const client = await this.page.context().newCDPSession(this.page);
3211
+ await client.send('Performance.enable');
3212
+ const perfMetricObject = await client.send('Performance.getMetrics');
3213
+ return perfMetricObject?.metrics;
3214
+ }
3215
+
3216
+ _getWebSocketMessage(payload) {
3217
+ if (payload.errorMessage) {
3218
+ return payload.errorMessage;
3219
+ }
3220
+
3221
+ return payload.response.payloadData;
3222
+ }
3223
+
3224
+ _getWebSocketLog(prefix, payload) {
3225
+ return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
3226
+ }
3227
+
3228
+ async getNewCDPSession() {
3229
+ return this.page.context().newCDPSession(this.page);
3230
+ }
3231
+
3232
+ _logWebsocketMessages(message) {
3233
+ this.webSocketMessages += message;
3234
+ }
2988
3235
  }
2989
3236
 
2990
3237
  module.exports = Playwright;
@@ -2997,42 +3244,19 @@ function buildLocatorString(locator) {
2997
3244
  }
2998
3245
  return locator.simplify();
2999
3246
  }
3000
- // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
3001
- async function getXPathForElement(elementHandle) {
3002
- function calculateIndex(node) {
3003
- let index = 1;
3004
- let sibling = node.previousElementSibling;
3005
- while (sibling) {
3006
- if (sibling.tagName === node.tagName) {
3007
- index++;
3008
- }
3009
- sibling = sibling.previousElementSibling;
3010
- }
3011
- return index;
3012
- }
3013
3247
 
3014
- function generateXPath(node) {
3015
- const segments = [];
3016
- while (node && node.nodeType === Node.ELEMENT_NODE) {
3017
- if (node.hasAttribute('id')) {
3018
- segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
3019
- break;
3020
- } else {
3021
- const index = calculateIndex(node);
3022
- segments.unshift(`${node.localName}[${index}]`);
3023
- node = node.parentNode;
3024
- }
3025
- }
3026
- return `//${segments.join('/')}`;
3027
- }
3248
+ async function findElements(matcher, locator) {
3249
+ if (locator.react) return findReact(matcher, locator);
3250
+ locator = new Locator(locator, 'css');
3028
3251
 
3029
- return elementHandle.evaluate(generateXPath);
3252
+ return matcher.locator(buildLocatorString(locator)).all();
3030
3253
  }
3031
3254
 
3032
- async function findElements(matcher, locator) {
3255
+ async function findElement(matcher, locator) {
3033
3256
  if (locator.react) return findReact(matcher, locator);
3034
3257
  locator = new Locator(locator, 'css');
3035
- return matcher.$$(buildLocatorString(locator));
3258
+
3259
+ return matcher.locator(buildLocatorString(locator));
3036
3260
  }
3037
3261
 
3038
3262
  async function getVisibleElements(elements) {
@@ -3062,8 +3286,7 @@ async function proceedClick(locator, context = null, options = {}) {
3062
3286
  assertElementExists(els, locator, 'Clickable element');
3063
3287
  }
3064
3288
 
3065
- const element = els[0];
3066
- highlightActiveElement.call(this, els[0], this.page);
3289
+ highlightActiveElement.call(this, els[0], await this._getContext());
3067
3290
 
3068
3291
  /*
3069
3292
  using the force true options itself but instead dispatching a click
@@ -3076,7 +3299,7 @@ async function proceedClick(locator, context = null, options = {}) {
3076
3299
  }
3077
3300
  const promises = [];
3078
3301
  if (options.waitForNavigation) {
3079
- promises.push(this.waitForNavigation());
3302
+ promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
3080
3303
  }
3081
3304
  promises.push(this._waitForAction());
3082
3305
 
@@ -3111,28 +3334,28 @@ async function findClickable(matcher, locator) {
3111
3334
  async function proceedSee(assertType, text, context, strict = false) {
3112
3335
  let description;
3113
3336
  let allText;
3337
+
3114
3338
  if (!context) {
3115
3339
  let el = await this.context;
3116
-
3117
3340
  if (el && !el.getProperty) {
3118
3341
  // Fallback to body
3119
- el = await this.context.$('body');
3342
+ el = await this.page.$('body');
3120
3343
  }
3121
3344
 
3122
- allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
3345
+ allText = [await el.innerText()];
3123
3346
  description = 'web application';
3124
3347
  } else {
3125
3348
  const locator = new Locator(context, 'css');
3126
3349
  description = `element ${locator.toString()}`;
3127
3350
  const els = await this._locate(locator);
3128
3351
  assertElementExists(els, locator.toString());
3129
- allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
3352
+ allText = await Promise.all(els.map(el => el.innerText()));
3130
3353
  }
3131
3354
 
3132
3355
  if (strict) {
3133
3356
  return allText.map(elText => equals(description)[assertType](text, elText));
3134
3357
  }
3135
- return stringIncludes(description)[assertType](text, allText.join(' | '));
3358
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
3136
3359
  }
3137
3360
 
3138
3361
  async function findCheckable(locator, context) {
@@ -3194,15 +3417,15 @@ async function proceedSeeInField(assertType, field, value) {
3194
3417
  const els = await findFields.call(this, field);
3195
3418
  assertElementExists(els, field, 'Field');
3196
3419
  const el = els[0];
3197
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
3198
- const fieldType = await el.getProperty('type').then(el => el.jsonValue());
3420
+ const tag = await el.evaluate(e => e.tagName);
3421
+ const fieldType = await el.getAttribute('type');
3199
3422
 
3200
3423
  const proceedMultiple = async (elements) => {
3201
3424
  const fields = Array.isArray(elements) ? elements : [elements];
3202
3425
 
3203
3426
  const elementValues = [];
3204
3427
  for (const element of fields) {
3205
- elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
3428
+ elementValues.push(await element.inputValue());
3206
3429
  }
3207
3430
 
3208
3431
  if (typeof value === 'boolean') {
@@ -3216,8 +3439,8 @@ async function proceedSeeInField(assertType, field, value) {
3216
3439
  };
3217
3440
 
3218
3441
  if (tag === 'SELECT') {
3219
- if (await el.getProperty('multiple')) {
3220
- const selectedOptions = await el.$$('option:checked');
3442
+ if (await el.getAttribute('multiple')) {
3443
+ const selectedOptions = await el.all('option:checked');
3221
3444
  if (!selectedOptions.length) return null;
3222
3445
 
3223
3446
  const options = await filterFieldsByValue(selectedOptions, value, true);
@@ -3241,14 +3464,23 @@ async function proceedSeeInField(assertType, field, value) {
3241
3464
  return proceedMultiple(els[0]);
3242
3465
  }
3243
3466
 
3244
- const fieldVal = await el.inputValue();
3467
+ let fieldVal;
3468
+
3469
+ try {
3470
+ fieldVal = await el.inputValue();
3471
+ } catch (e) {
3472
+ if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
3473
+ fieldVal = await el.innerText();
3474
+ }
3475
+ }
3476
+
3245
3477
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
3246
3478
  }
3247
3479
 
3248
3480
  async function filterFieldsByValue(elements, value, onlySelected) {
3249
3481
  const matches = [];
3250
3482
  for (const element of elements) {
3251
- const val = await element.getProperty('value').then(el => el.jsonValue());
3483
+ const val = await element.getAttribute('value');
3252
3484
  let isSelected = true;
3253
3485
  if (onlySelected) {
3254
3486
  isSelected = await elementSelected(element);
@@ -3272,12 +3504,12 @@ async function filterFieldsBySelectionState(elements, state) {
3272
3504
  }
3273
3505
 
3274
3506
  async function elementSelected(element) {
3275
- const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
3507
+ const type = await element.getAttribute('type');
3276
3508
 
3277
3509
  if (type === 'checkbox' || type === 'radio') {
3278
3510
  return element.isChecked();
3279
3511
  }
3280
- return element.getProperty('selected').then(el => el.jsonValue());
3512
+ return element.getAttribute('selected');
3281
3513
  }
3282
3514
 
3283
3515
  function isFrameLocator(locator) {
@@ -3478,7 +3710,7 @@ async function saveTraceForContext(context, name) {
3478
3710
  }
3479
3711
 
3480
3712
  function highlightActiveElement(element, context) {
3481
- if (!this.options.enableHighlight && !store.debugMode) return;
3713
+ if (!this.options.highlightElement && !store.debugMode) return;
3482
3714
 
3483
3715
  highlightElement(element, context);
3484
3716
  }