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;
@@ -771,7 +801,7 @@ class Playwright extends Helper {
771
801
  }
772
802
  throw err;
773
803
  }
774
- } else if (this.userDataDir) {
804
+ } else if (this.playwrightOptions.userDataDir) {
775
805
  this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
776
806
  } else {
777
807
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
@@ -832,9 +862,9 @@ class Playwright extends Helper {
832
862
  return;
833
863
  }
834
864
 
835
- const els = await this._locate(locator);
836
- assertElementExists(els, locator);
837
- this.context = els[0];
865
+ const el = await this._locateElement(locator);
866
+ assertElementExists(el, locator);
867
+ this.context = el;
838
868
  this.contextLocator = locator;
839
869
 
840
870
  this.withinLocator = new Locator(locator);
@@ -965,11 +995,11 @@ class Playwright extends Helper {
965
995
  *
966
996
  */
967
997
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
968
- const els = await this._locate(locator);
969
- assertElementExists(els, locator);
998
+ const el = await this._locateElement(locator);
999
+ assertElementExists(el, locator);
970
1000
 
971
1001
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
972
- const { x, y } = await clickablePoint(els[0]);
1002
+ const { x, y } = await clickablePoint(el);
973
1003
  await this.page.mouse.move(x + offsetX, y + offsetY);
974
1004
  return this._waitForAction();
975
1005
  }
@@ -991,9 +1021,8 @@ class Playwright extends Helper {
991
1021
  *
992
1022
  */
993
1023
  async focus(locator, options = {}) {
994
- const els = await this._locate(locator);
995
- assertElementExists(els, locator, 'Element to focus');
996
- const el = els[0];
1024
+ const el = await this._locateElement(locator);
1025
+ assertElementExists(el, locator, 'Element to focus');
997
1026
 
998
1027
  await el.focus(options);
999
1028
  return this._waitForAction();
@@ -1021,12 +1050,10 @@ class Playwright extends Helper {
1021
1050
  *
1022
1051
  */
1023
1052
  async blur(locator, options = {}) {
1024
- const els = await this._locate(locator);
1025
- assertElementExists(els, locator, 'Element to blur');
1026
- // TODO: locator change required after #3677 implementation
1027
- const elXpath = await getXPathForElement(els[0]);
1053
+ const el = await this._locateElement(locator);
1054
+ assertElementExists(el, locator, 'Element to blur');
1028
1055
 
1029
- await this.page.locator(elXpath).blur(options);
1056
+ await el.blur(options);
1030
1057
  return this._waitForAction();
1031
1058
  }
1032
1059
 
@@ -1133,8 +1160,11 @@ class Playwright extends Helper {
1133
1160
  const body = document.body;
1134
1161
  const html = document.documentElement;
1135
1162
  window.scrollTo(0, Math.max(
1136
- body.scrollHeight, body.offsetHeight,
1137
- html.clientHeight, html.scrollHeight, html.offsetHeight,
1163
+ body.scrollHeight,
1164
+ body.offsetHeight,
1165
+ html.clientHeight,
1166
+ html.scrollHeight,
1167
+ html.offsetHeight,
1138
1168
  ));
1139
1169
  });
1140
1170
  }
@@ -1162,10 +1192,10 @@ class Playwright extends Helper {
1162
1192
  }
1163
1193
 
1164
1194
  if (locator) {
1165
- const els = await this._locate(locator);
1166
- assertElementExists(els, locator, 'Element');
1167
- await els[0].scrollIntoViewIfNeeded();
1168
- const elementCoordinates = await clickablePoint(els[0]);
1195
+ const el = await this._locateElement(locator);
1196
+ assertElementExists(el, locator, 'Element');
1197
+ await el.scrollIntoViewIfNeeded();
1198
+ const elementCoordinates = await clickablePoint(el);
1169
1199
  await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
1170
1200
  } else {
1171
1201
  await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
@@ -1272,7 +1302,20 @@ class Playwright extends Helper {
1272
1302
  }
1273
1303
 
1274
1304
  /**
1275
- * Find a checkbox by providing human readable text:
1305
+ * Get the first element by different locator types, including strict locator
1306
+ * Should be used in custom helpers:
1307
+ *
1308
+ * ```js
1309
+ * const element = await this.helpers['Playwright']._locateElement({name: 'password'});
1310
+ * ```
1311
+ */
1312
+ async _locateElement(locator) {
1313
+ const context = await this.context || await this._getContext();
1314
+ return findElement(context, locator);
1315
+ }
1316
+
1317
+ /**
1318
+ * Find a checkbox by providing human-readable text:
1276
1319
  * NOTE: Assumes the checkable element exists
1277
1320
  *
1278
1321
  * ```js
@@ -1287,7 +1330,7 @@ class Playwright extends Helper {
1287
1330
  }
1288
1331
 
1289
1332
  /**
1290
- * Find a clickable element by providing human readable text:
1333
+ * Find a clickable element by providing human-readable text:
1291
1334
  *
1292
1335
  * ```js
1293
1336
  * this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -1299,7 +1342,7 @@ class Playwright extends Helper {
1299
1342
  }
1300
1343
 
1301
1344
  /**
1302
- * Find field elements by providing human readable text:
1345
+ * Find field elements by providing human-readable text:
1303
1346
  *
1304
1347
  * ```js
1305
1348
  * this.helpers['Playwright']._locateFields('Your email').then // ...
@@ -1962,15 +2005,10 @@ class Playwright extends Helper {
1962
2005
  const els = await findFields.call(this, field);
1963
2006
  assertElementExists(els, field, 'Field');
1964
2007
  const el = els[0];
1965
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
1966
- const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
1967
- if (tag === 'INPUT' || tag === 'TEXTAREA') {
1968
- await this._evaluateHandeInContext(el => el.value = '', el);
1969
- } else if (editable) {
1970
- await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1971
- }
1972
2008
 
1973
- highlightActiveElement.call(this, el, this.page);
2009
+ await el.clear();
2010
+
2011
+ highlightActiveElement.call(this, el, await this._getContext());
1974
2012
 
1975
2013
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1976
2014
 
@@ -1995,21 +2033,16 @@ class Playwright extends Helper {
1995
2033
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1996
2034
  */
1997
2035
  async clearField(locator, options = {}) {
1998
- let result;
1999
- const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
2036
+ const els = await findFields.call(this, locator);
2037
+ assertElementExists(els, locator, 'Field to clear');
2000
2038
 
2001
- if (isNewClearMethodPresent) {
2002
- const els = await findFields.call(this, locator);
2003
- assertElementExists(els, locator, 'Field to clear');
2004
- // TODO: locator change required after #3677 implementation
2005
- const elXpath = await getXPathForElement(els[0]);
2039
+ const el = els[0];
2006
2040
 
2007
- await this.page.locator(elXpath).clear(options);
2008
- result = await this._waitForAction();
2009
- } else {
2010
- result = await this.fillField(locator, '');
2011
- }
2012
- return result;
2041
+ highlightActiveElement.call(this, el, this.page);
2042
+
2043
+ await el.clear();
2044
+
2045
+ return this._waitForAction();
2013
2046
  }
2014
2047
 
2015
2048
  /**
@@ -2031,7 +2064,7 @@ class Playwright extends Helper {
2031
2064
  async appendField(field, value) {
2032
2065
  const els = await findFields.call(this, field);
2033
2066
  assertElementExists(els, field, 'Field');
2034
- highlightActiveElement.call(this, els[0], this.page);
2067
+ highlightActiveElement.call(this, els[0], await this._getContext());
2035
2068
  await els[0].press('End');
2036
2069
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
2037
2070
  return this._waitForAction();
@@ -2048,12 +2081,13 @@ class Playwright extends Helper {
2048
2081
  * I.seeInField('#searchform input','Search');
2049
2082
  * ```
2050
2083
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2051
- * @param {string} value value to check.
2084
+ * @param {CodeceptJS.StringOrSecret} value value to check.
2052
2085
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
2053
2086
  *
2054
2087
  */
2055
2088
  async seeInField(field, value) {
2056
- return proceedSeeInField.call(this, 'assert', field, value);
2089
+ const _value = (typeof value === 'boolean') ? value : value.toString();
2090
+ return proceedSeeInField.call(this, 'assert', field, _value);
2057
2091
  }
2058
2092
 
2059
2093
  /**
@@ -2066,12 +2100,13 @@ class Playwright extends Helper {
2066
2100
  * ```
2067
2101
  *
2068
2102
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2069
- * @param {string} value value to check.
2103
+ * @param {CodeceptJS.StringOrSecret} value value to check.
2070
2104
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
2071
2105
  *
2072
2106
  */
2073
2107
  async dontSeeInField(field, value) {
2074
- return proceedSeeInField.call(this, 'negate', field, value);
2108
+ const _value = (typeof value === 'boolean') ? value : value.toString();
2109
+ return proceedSeeInField.call(this, 'negate', field, _value);
2075
2110
  }
2076
2111
 
2077
2112
  /**
@@ -2130,29 +2165,12 @@ class Playwright extends Helper {
2130
2165
  const els = await findFields.call(this, select);
2131
2166
  assertElementExists(els, select, 'Selectable field');
2132
2167
  const el = els[0];
2133
- if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
2134
- throw new Error('Element is not <select>');
2135
- }
2136
- highlightActiveElement.call(this, el, this.page);
2137
- if (!Array.isArray(option)) option = [option];
2138
2168
 
2139
- for (const key in option) {
2140
- const opt = xpathLocator.literal(option[key]);
2141
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
2142
- if (optEl.length) {
2143
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
2144
- continue;
2145
- }
2146
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
2147
- if (optEl.length) {
2148
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
2149
- }
2150
- }
2151
- await this._evaluateHandeInContext((element) => {
2152
- element.dispatchEvent(new Event('input', { bubbles: true }));
2153
- element.dispatchEvent(new Event('change', { bubbles: true }));
2154
- }, el);
2169
+ highlightActiveElement.call(this, el, await this._getContext());
2170
+
2171
+ if (!Array.isArray(option)) option = [option];
2155
2172
 
2173
+ await el.selectOption(option);
2156
2174
  return this._waitForAction();
2157
2175
  }
2158
2176
 
@@ -2598,7 +2616,7 @@ class Playwright extends Helper {
2598
2616
  const els = await this._locate(locator);
2599
2617
  const texts = [];
2600
2618
  for (const el of els) {
2601
- texts.push(await (await el.getProperty('innerText')).jsonValue());
2619
+ texts.push(await (await el.innerText()));
2602
2620
  }
2603
2621
  this.debug(`Matched ${els.length} elements`);
2604
2622
  return texts;
@@ -2637,7 +2655,7 @@ class Playwright extends Helper {
2637
2655
  async grabValueFromAll(locator) {
2638
2656
  const els = await findFields.call(this, locator);
2639
2657
  this.debug(`Matched ${els.length} elements`);
2640
- return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
2658
+ return Promise.all(els.map(el => el.inputValue()));
2641
2659
  }
2642
2660
 
2643
2661
  /**
@@ -2675,7 +2693,7 @@ class Playwright extends Helper {
2675
2693
  async grabHTMLFromAll(locator) {
2676
2694
  const els = await this._locate(locator);
2677
2695
  this.debug(`Matched ${els.length} elements`);
2678
- return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
2696
+ return Promise.all(els.map(el => el.innerHTML()));
2679
2697
  }
2680
2698
 
2681
2699
  /**
@@ -2717,7 +2735,7 @@ class Playwright extends Helper {
2717
2735
  async grabCssPropertyFromAll(locator, cssProperty) {
2718
2736
  const els = await this._locate(locator);
2719
2737
  this.debug(`Matched ${els.length} elements`);
2720
- const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
2738
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
2721
2739
 
2722
2740
  return cssValues;
2723
2741
  }
@@ -2742,21 +2760,20 @@ class Playwright extends Helper {
2742
2760
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
2743
2761
  const elemAmount = res.length;
2744
2762
  const commands = [];
2745
- res.forEach((el) => {
2746
- Object.keys(cssPropertiesCamelCase).forEach((prop) => {
2747
- commands.push(el.$eval('xpath=.', (el) => {
2748
- const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
2749
- return JSON.parse(JSON.stringify(style));
2750
- }, el)
2751
- .then((props) => {
2752
- if (isColorProperty(prop)) {
2753
- return convertColorToRGBA(props[prop]);
2754
- }
2755
- return props[prop];
2756
- }));
2763
+ let props = [];
2764
+
2765
+ for (const element of res) {
2766
+ const cssProperties = await element.evaluate((el) => getComputedStyle(el));
2767
+
2768
+ Object.keys(cssPropertiesCamelCase).forEach(prop => {
2769
+ if (isColorProperty(prop)) {
2770
+ props.push(convertColorToRGBA(cssProperties[prop]));
2771
+ } else {
2772
+ props.push(cssProperties[prop]);
2773
+ }
2757
2774
  });
2758
- });
2759
- let props = await Promise.all(commands);
2775
+ }
2776
+
2760
2777
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
2761
2778
  if (!Array.isArray(props)) props = [props];
2762
2779
  let chunked = chunkArray(props, values.length);
@@ -2791,7 +2808,7 @@ class Playwright extends Helper {
2791
2808
  res.forEach((el) => {
2792
2809
  Object.keys(attributes).forEach((prop) => {
2793
2810
  commands.push(el
2794
- .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
2811
+ .evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
2795
2812
  });
2796
2813
  });
2797
2814
  let attrs = await Promise.all(commands);
@@ -2823,11 +2840,11 @@ class Playwright extends Helper {
2823
2840
  *
2824
2841
  */
2825
2842
  async dragSlider(locator, offsetX = 0) {
2826
- const src = await this._locate(locator);
2843
+ const src = await this._locateElement(locator);
2827
2844
  assertElementExists(src, locator, 'Slider Element');
2828
2845
 
2829
2846
  // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
2830
- const sliderSource = await clickablePoint(src[0]);
2847
+ const sliderSource = await clickablePoint(src);
2831
2848
 
2832
2849
  // Drag start point
2833
2850
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2880,8 +2897,7 @@ class Playwright extends Helper {
2880
2897
  const array = [];
2881
2898
 
2882
2899
  for (let index = 0; index < els.length; index++) {
2883
- const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
2884
- array.push(await a.jsonValue());
2900
+ array.push(await els[index].getAttribute(attr));
2885
2901
  }
2886
2902
 
2887
2903
  return array;
@@ -2904,10 +2920,9 @@ class Playwright extends Helper {
2904
2920
  async saveElementScreenshot(locator, fileName) {
2905
2921
  const outputFile = screenshotOutputFolder(fileName);
2906
2922
 
2907
- const res = await this._locate(locator);
2923
+ const res = await this._locateElement(locator);
2908
2924
  assertElementExists(res, locator);
2909
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
2910
- const elem = res[0];
2925
+ const elem = res;
2911
2926
  this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
2912
2927
  return elem.screenshot({ path: outputFile, type: 'png' });
2913
2928
  }
@@ -3472,16 +3487,26 @@ class Playwright extends Helper {
3472
3487
  }
3473
3488
  return;
3474
3489
  }
3490
+ let contentFrame;
3491
+
3475
3492
  if (!locator) {
3476
- this.context = this.page;
3493
+ this.context = await this.page.frames()[0];
3477
3494
  this.contextLocator = null;
3478
3495
  return;
3479
3496
  }
3480
3497
 
3481
3498
  // iframe by selector
3482
3499
  const els = await this._locate(locator);
3483
- assertElementExists(els, locator);
3484
- const contentFrame = await els[0].contentFrame();
3500
+ // assertElementExists(els, locator);
3501
+
3502
+ // get content of the first iframe
3503
+ if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
3504
+ contentFrame = await this.page.frames()[1];
3505
+ // get content of the iframe using its name
3506
+ } else if (locator.toLowerCase().includes('name=')) {
3507
+ const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
3508
+ contentFrame = await this.page.frame(frameName);
3509
+ }
3485
3510
 
3486
3511
  if (contentFrame) {
3487
3512
  this.context = contentFrame;
@@ -3527,13 +3552,15 @@ class Playwright extends Helper {
3527
3552
  }
3528
3553
 
3529
3554
  /**
3530
- * Waits for navigation to finish. By default takes configured `waitForNavigation` option.
3555
+ * Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
3531
3556
  *
3532
3557
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
3533
3558
  *
3534
3559
  * @param {*} options
3535
3560
  */
3536
3561
  async waitForNavigation(options = {}) {
3562
+ console.log(`waitForNavigation deprecated:
3563
+ * This method is inherently racy, please use 'waitForURL' instead.`);
3537
3564
  options = {
3538
3565
  timeout: this.options.getPageTimeout,
3539
3566
  waitUntil: this.options.waitForNavigation,
@@ -3542,6 +3569,23 @@ class Playwright extends Helper {
3542
3569
  return this.page.waitForNavigation(options);
3543
3570
  }
3544
3571
 
3572
+ /**
3573
+ * Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
3574
+ *
3575
+ * See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
3576
+ *
3577
+ * @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.
3578
+ * @param {*} options
3579
+ */
3580
+ async waitForURL(url, options = {}) {
3581
+ options = {
3582
+ timeout: this.options.getPageTimeout,
3583
+ waitUntil: this.options.waitForNavigation,
3584
+ ...options,
3585
+ };
3586
+ return this.page.waitForURL(url, options);
3587
+ }
3588
+
3545
3589
  async waitUntilExists(locator, sec) {
3546
3590
  console.log(`waitUntilExists deprecated:
3547
3591
  * use 'waitForElement' to wait for element to be attached
@@ -3636,9 +3680,9 @@ class Playwright extends Helper {
3636
3680
  *
3637
3681
  */
3638
3682
  async grabElementBoundingRect(locator, prop) {
3639
- const els = await this._locate(locator);
3640
- assertElementExists(els, locator);
3641
- const rect = await els[0].boundingBox();
3683
+ const el = await this._locateElement(locator);
3684
+ assertElementExists(el, locator);
3685
+ const rect = await el.boundingBox();
3642
3686
  if (prop) return rect[prop];
3643
3687
  return rect;
3644
3688
  }
@@ -3675,16 +3719,16 @@ class Playwright extends Helper {
3675
3719
  }
3676
3720
 
3677
3721
  /**
3678
- * Starts recording of network traffic.
3722
+ * Starts recording the network traffics.
3679
3723
  * This also resets recorded network requests.
3680
3724
  *
3681
3725
  * ```js
3682
3726
  * I.startRecordingTraffic();
3683
3727
  * ```
3684
3728
  *
3685
- * @return {Promise<void>}
3729
+ * @return {void}
3686
3730
  */
3687
- async startRecordingTraffic() {
3731
+ startRecordingTraffic() {
3688
3732
  this.flushNetworkTraffics();
3689
3733
  this.recording = true;
3690
3734
  this.recordedAtLeastOnce = true;
@@ -3695,31 +3739,62 @@ class Playwright extends Helper {
3695
3739
  method: request.method(),
3696
3740
  requestHeaders: request.headers(),
3697
3741
  requestPostData: request.postData(),
3742
+ response: request.response(),
3698
3743
  };
3699
3744
 
3700
3745
  this.debugSection('REQUEST: ', JSON.stringify(information));
3701
3746
 
3702
- information.requestPostData = JSON.parse(information.requestPostData);
3747
+ if (typeof information.requestPostData === 'object') {
3748
+ information.requestPostData = JSON.parse(information.requestPostData);
3749
+ }
3750
+
3703
3751
  this.requests.push(information);
3704
- return this._waitForAction();
3705
3752
  });
3706
3753
  }
3707
3754
 
3708
3755
  /**
3709
3756
  * Grab the recording network traffics
3710
3757
  *
3711
- * @return { Array<any> }
3758
+ * ```js
3759
+ * const traffics = await I.grabRecordedNetworkTraffics();
3760
+ * expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
3761
+ * expect(traffics[0].response.status).to.equal(200);
3762
+ * expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
3763
+ * ```
3764
+ *
3765
+ * @return { Promise<Array<any>> }
3712
3766
  *
3713
3767
  */
3714
- grabRecordedNetworkTraffics() {
3768
+ async grabRecordedNetworkTraffics() {
3715
3769
  if (!this.recording || !this.recordedAtLeastOnce) {
3716
3770
  throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
3717
3771
  }
3772
+
3773
+ const requests = await this.requests;
3774
+ const promises = requests.map(async (request) => request.response.then(
3775
+ async (response) => {
3776
+ let body;
3777
+ try {
3778
+ // There's no 'body' for some requests (redirect etc...)
3779
+ body = JSON.parse((await response.body()).toString());
3780
+ } catch (e) {
3781
+ // only interested in JSON, not HTML responses.
3782
+ }
3783
+
3784
+ request.response = {
3785
+ status: response.status(),
3786
+ statusText: response.statusText(),
3787
+ body,
3788
+ };
3789
+ },
3790
+ ));
3791
+ await Promise.all(promises);
3792
+
3718
3793
  return this.requests;
3719
3794
  }
3720
3795
 
3721
3796
  /**
3722
- * Blocks traffic for URL.
3797
+ * Blocks traffic of a given URL or a list of URLs.
3723
3798
  *
3724
3799
  * Examples:
3725
3800
  *
@@ -3730,16 +3805,30 @@ class Playwright extends Helper {
3730
3805
  * I.blockTraffic(/\.css$/);
3731
3806
  * ```
3732
3807
  *
3733
- * @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.
3734
- */
3735
- async blockTraffic(url) {
3736
- this.page.route(url, (route) => {
3737
- route
3738
- .abort()
3739
- // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3740
- .catch((e) => {});
3741
- });
3742
- return this._waitForAction();
3808
+ * ```js
3809
+ * I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
3810
+ * ```
3811
+ *
3812
+ * @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.
3813
+ */
3814
+ blockTraffic(urls) {
3815
+ if (Array.isArray(urls)) {
3816
+ urls.forEach(url => {
3817
+ this.page.route(url, (route) => {
3818
+ route
3819
+ .abort()
3820
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3821
+ .catch((e) => {});
3822
+ });
3823
+ });
3824
+ } else {
3825
+ this.page.route(urls, (route) => {
3826
+ route
3827
+ .abort()
3828
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3829
+ .catch((e) => {});
3830
+ });
3831
+ }
3743
3832
  }
3744
3833
 
3745
3834
  /**
@@ -3758,7 +3847,7 @@ class Playwright extends Helper {
3758
3847
  * @param responseString string The string to return in fake response's body.
3759
3848
  * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
3760
3849
  */
3761
- async mockTraffic(urls, responseString, contentType = 'application/json') {
3850
+ mockTraffic(urls, responseString, contentType = 'application/json') {
3762
3851
  // Required to mock cross-domain requests
3763
3852
  const headers = { 'access-control-allow-origin': '*' };
3764
3853
 
@@ -3780,7 +3869,6 @@ class Playwright extends Helper {
3780
3869
  });
3781
3870
  });
3782
3871
  });
3783
- return this._waitForAction();
3784
3872
  }
3785
3873
 
3786
3874
  /**
@@ -3852,7 +3940,7 @@ class Playwright extends Helper {
3852
3940
  }
3853
3941
 
3854
3942
  if (!this.recording || !this.recordedAtLeastOnce) {
3855
- throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
3943
+ throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
3856
3944
  }
3857
3945
 
3858
3946
  for (let i = 0; i <= timeout * 2; i++) {
@@ -3860,7 +3948,9 @@ class Playwright extends Helper {
3860
3948
  if (found) {
3861
3949
  return true;
3862
3950
  }
3863
- await new Promise((done) => setTimeout(done, 1000));
3951
+ await new Promise((done) => {
3952
+ setTimeout(done, 1000);
3953
+ });
3864
3954
  }
3865
3955
 
3866
3956
  // check request post data
@@ -3997,6 +4087,163 @@ class Playwright extends Helper {
3997
4087
  });
3998
4088
  return dumpedTraffic;
3999
4089
  }
4090
+
4091
+ /**
4092
+ * Starts recording of websocket messages.
4093
+ * This also resets recorded websocket messages.
4094
+ *
4095
+ * ```js
4096
+ * await I.startRecordingWebSocketMessages();
4097
+ * ```
4098
+ *
4099
+ */
4100
+ async startRecordingWebSocketMessages() {
4101
+ this.flushWebSocketMessages();
4102
+ this.recordingWebSocketMessages = true;
4103
+ this.recordedWebSocketMessagesAtLeastOnce = true;
4104
+
4105
+ this.cdpSession = await this.getNewCDPSession();
4106
+ await this.cdpSession.send('Network.enable');
4107
+ await this.cdpSession.send('Page.enable');
4108
+
4109
+ this.cdpSession.on(
4110
+ 'Network.webSocketFrameReceived',
4111
+ (payload) => {
4112
+ this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
4113
+ },
4114
+ );
4115
+
4116
+ this.cdpSession.on(
4117
+ 'Network.webSocketFrameSent',
4118
+ (payload) => {
4119
+ this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
4120
+ },
4121
+ );
4122
+
4123
+ this.cdpSession.on(
4124
+ 'Network.webSocketFrameError',
4125
+ (payload) => {
4126
+ this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
4127
+ },
4128
+ );
4129
+ }
4130
+
4131
+ /**
4132
+ * Stops recording WS messages. Recorded WS messages is not flashed.
4133
+ *
4134
+ * ```js
4135
+ * await I.stopRecordingWebSocketMessages();
4136
+ * ```
4137
+ */
4138
+ async stopRecordingWebSocketMessages() {
4139
+ await this.cdpSession.send('Network.disable');
4140
+ await this.cdpSession.send('Page.disable');
4141
+ this.page.removeAllListeners('Network');
4142
+ this.recordingWebSocketMessages = false;
4143
+ }
4144
+
4145
+ /**
4146
+ * Grab the recording WS messages
4147
+ *
4148
+ * @return { Array<any> }
4149
+ *
4150
+ */
4151
+ grabWebSocketMessages() {
4152
+ if (!this.recordingWebSocketMessages) {
4153
+ if (!this.recordedWebSocketMessagesAtLeastOnce) {
4154
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
4155
+ }
4156
+ }
4157
+ return this.webSocketMessages;
4158
+ }
4159
+
4160
+ /**
4161
+ * Resets all recorded WS messages.
4162
+ */
4163
+ flushWebSocketMessages() {
4164
+ this.webSocketMessages = [];
4165
+ }
4166
+
4167
+ /**
4168
+ * Return a performance metric from the chrome cdp session.
4169
+ * Note: Chrome-only
4170
+ *
4171
+ * Examples:
4172
+ *
4173
+ * ```js
4174
+ * const metrics = await I.grabMetrics();
4175
+ *
4176
+ * // returned metrics
4177
+ *
4178
+ * [
4179
+ * { name: 'Timestamp', value: 1584904.203473 },
4180
+ * { name: 'AudioHandlers', value: 0 },
4181
+ * { name: 'AudioWorkletProcessors', value: 0 },
4182
+ * { name: 'Documents', value: 22 },
4183
+ * { name: 'Frames', value: 10 },
4184
+ * { name: 'JSEventListeners', value: 366 },
4185
+ * { name: 'LayoutObjects', value: 1240 },
4186
+ * { name: 'MediaKeySessions', value: 0 },
4187
+ * { name: 'MediaKeys', value: 0 },
4188
+ * { name: 'Nodes', value: 4505 },
4189
+ * { name: 'Resources', value: 141 },
4190
+ * { name: 'ContextLifecycleStateObservers', value: 34 },
4191
+ * { name: 'V8PerContextDatas', value: 4 },
4192
+ * { name: 'WorkerGlobalScopes', value: 0 },
4193
+ * { name: 'UACSSResources', value: 0 },
4194
+ * { name: 'RTCPeerConnections', value: 0 },
4195
+ * { name: 'ResourceFetchers', value: 22 },
4196
+ * { name: 'AdSubframes', value: 0 },
4197
+ * { name: 'DetachedScriptStates', value: 2 },
4198
+ * { name: 'ArrayBufferContents', value: 1 },
4199
+ * { name: 'LayoutCount', value: 0 },
4200
+ * { name: 'RecalcStyleCount', value: 0 },
4201
+ * { name: 'LayoutDuration', value: 0 },
4202
+ * { name: 'RecalcStyleDuration', value: 0 },
4203
+ * { name: 'DevToolsCommandDuration', value: 0.000013 },
4204
+ * { name: 'ScriptDuration', value: 0 },
4205
+ * { name: 'V8CompileDuration', value: 0 },
4206
+ * { name: 'TaskDuration', value: 0.000014 },
4207
+ * { name: 'TaskOtherDuration', value: 0.000001 },
4208
+ * { name: 'ThreadTime', value: 0.000046 },
4209
+ * { name: 'ProcessTime', value: 0.616852 },
4210
+ * { name: 'JSHeapUsedSize', value: 19004908 },
4211
+ * { name: 'JSHeapTotalSize', value: 26820608 },
4212
+ * { name: 'FirstMeaningfulPaint', value: 0 },
4213
+ * { name: 'DomContentLoaded', value: 1584903.690491 },
4214
+ * { name: 'NavigationStart', value: 1584902.841845 }
4215
+ * ]
4216
+ *
4217
+ * ```
4218
+ *
4219
+ * @return {Promise<Array<Object>>}
4220
+ */
4221
+ async grabMetrics() {
4222
+ const client = await this.page.context().newCDPSession(this.page);
4223
+ await client.send('Performance.enable');
4224
+ const perfMetricObject = await client.send('Performance.getMetrics');
4225
+ return perfMetricObject?.metrics;
4226
+ }
4227
+
4228
+ _getWebSocketMessage(payload) {
4229
+ if (payload.errorMessage) {
4230
+ return payload.errorMessage;
4231
+ }
4232
+
4233
+ return payload.response.payloadData;
4234
+ }
4235
+
4236
+ _getWebSocketLog(prefix, payload) {
4237
+ return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
4238
+ }
4239
+
4240
+ async getNewCDPSession() {
4241
+ return this.page.context().newCDPSession(this.page);
4242
+ }
4243
+
4244
+ _logWebsocketMessages(message) {
4245
+ this.webSocketMessages += message;
4246
+ }
4000
4247
  }
4001
4248
 
4002
4249
  module.exports = Playwright;
@@ -4009,42 +4256,19 @@ function buildLocatorString(locator) {
4009
4256
  }
4010
4257
  return locator.simplify();
4011
4258
  }
4012
- // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
4013
- async function getXPathForElement(elementHandle) {
4014
- function calculateIndex(node) {
4015
- let index = 1;
4016
- let sibling = node.previousElementSibling;
4017
- while (sibling) {
4018
- if (sibling.tagName === node.tagName) {
4019
- index++;
4020
- }
4021
- sibling = sibling.previousElementSibling;
4022
- }
4023
- return index;
4024
- }
4025
4259
 
4026
- function generateXPath(node) {
4027
- const segments = [];
4028
- while (node && node.nodeType === Node.ELEMENT_NODE) {
4029
- if (node.hasAttribute('id')) {
4030
- segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
4031
- break;
4032
- } else {
4033
- const index = calculateIndex(node);
4034
- segments.unshift(`${node.localName}[${index}]`);
4035
- node = node.parentNode;
4036
- }
4037
- }
4038
- return `//${segments.join('/')}`;
4039
- }
4260
+ async function findElements(matcher, locator) {
4261
+ if (locator.react) return findReact(matcher, locator);
4262
+ locator = new Locator(locator, 'css');
4040
4263
 
4041
- return elementHandle.evaluate(generateXPath);
4264
+ return matcher.locator(buildLocatorString(locator)).all();
4042
4265
  }
4043
4266
 
4044
- async function findElements(matcher, locator) {
4267
+ async function findElement(matcher, locator) {
4045
4268
  if (locator.react) return findReact(matcher, locator);
4046
4269
  locator = new Locator(locator, 'css');
4047
- return matcher.$$(buildLocatorString(locator));
4270
+
4271
+ return matcher.locator(buildLocatorString(locator));
4048
4272
  }
4049
4273
 
4050
4274
  async function getVisibleElements(elements) {
@@ -4074,8 +4298,7 @@ async function proceedClick(locator, context = null, options = {}) {
4074
4298
  assertElementExists(els, locator, 'Clickable element');
4075
4299
  }
4076
4300
 
4077
- const element = els[0];
4078
- highlightActiveElement.call(this, els[0], this.page);
4301
+ highlightActiveElement.call(this, els[0], await this._getContext());
4079
4302
 
4080
4303
  /*
4081
4304
  using the force true options itself but instead dispatching a click
@@ -4088,7 +4311,7 @@ async function proceedClick(locator, context = null, options = {}) {
4088
4311
  }
4089
4312
  const promises = [];
4090
4313
  if (options.waitForNavigation) {
4091
- promises.push(this.waitForNavigation());
4314
+ promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
4092
4315
  }
4093
4316
  promises.push(this._waitForAction());
4094
4317
 
@@ -4123,28 +4346,28 @@ async function findClickable(matcher, locator) {
4123
4346
  async function proceedSee(assertType, text, context, strict = false) {
4124
4347
  let description;
4125
4348
  let allText;
4349
+
4126
4350
  if (!context) {
4127
4351
  let el = await this.context;
4128
-
4129
4352
  if (el && !el.getProperty) {
4130
4353
  // Fallback to body
4131
- el = await this.context.$('body');
4354
+ el = await this.page.$('body');
4132
4355
  }
4133
4356
 
4134
- allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
4357
+ allText = [await el.innerText()];
4135
4358
  description = 'web application';
4136
4359
  } else {
4137
4360
  const locator = new Locator(context, 'css');
4138
4361
  description = `element ${locator.toString()}`;
4139
4362
  const els = await this._locate(locator);
4140
4363
  assertElementExists(els, locator.toString());
4141
- allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
4364
+ allText = await Promise.all(els.map(el => el.innerText()));
4142
4365
  }
4143
4366
 
4144
4367
  if (strict) {
4145
4368
  return allText.map(elText => equals(description)[assertType](text, elText));
4146
4369
  }
4147
- return stringIncludes(description)[assertType](text, allText.join(' | '));
4370
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
4148
4371
  }
4149
4372
 
4150
4373
  async function findCheckable(locator, context) {
@@ -4206,15 +4429,15 @@ async function proceedSeeInField(assertType, field, value) {
4206
4429
  const els = await findFields.call(this, field);
4207
4430
  assertElementExists(els, field, 'Field');
4208
4431
  const el = els[0];
4209
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
4210
- const fieldType = await el.getProperty('type').then(el => el.jsonValue());
4432
+ const tag = await el.evaluate(e => e.tagName);
4433
+ const fieldType = await el.getAttribute('type');
4211
4434
 
4212
4435
  const proceedMultiple = async (elements) => {
4213
4436
  const fields = Array.isArray(elements) ? elements : [elements];
4214
4437
 
4215
4438
  const elementValues = [];
4216
4439
  for (const element of fields) {
4217
- elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
4440
+ elementValues.push(await element.inputValue());
4218
4441
  }
4219
4442
 
4220
4443
  if (typeof value === 'boolean') {
@@ -4228,8 +4451,8 @@ async function proceedSeeInField(assertType, field, value) {
4228
4451
  };
4229
4452
 
4230
4453
  if (tag === 'SELECT') {
4231
- if (await el.getProperty('multiple')) {
4232
- const selectedOptions = await el.$$('option:checked');
4454
+ if (await el.getAttribute('multiple')) {
4455
+ const selectedOptions = await el.all('option:checked');
4233
4456
  if (!selectedOptions.length) return null;
4234
4457
 
4235
4458
  const options = await filterFieldsByValue(selectedOptions, value, true);
@@ -4253,14 +4476,23 @@ async function proceedSeeInField(assertType, field, value) {
4253
4476
  return proceedMultiple(els[0]);
4254
4477
  }
4255
4478
 
4256
- const fieldVal = await el.inputValue();
4479
+ let fieldVal;
4480
+
4481
+ try {
4482
+ fieldVal = await el.inputValue();
4483
+ } catch (e) {
4484
+ if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
4485
+ fieldVal = await el.innerText();
4486
+ }
4487
+ }
4488
+
4257
4489
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
4258
4490
  }
4259
4491
 
4260
4492
  async function filterFieldsByValue(elements, value, onlySelected) {
4261
4493
  const matches = [];
4262
4494
  for (const element of elements) {
4263
- const val = await element.getProperty('value').then(el => el.jsonValue());
4495
+ const val = await element.getAttribute('value');
4264
4496
  let isSelected = true;
4265
4497
  if (onlySelected) {
4266
4498
  isSelected = await elementSelected(element);
@@ -4284,12 +4516,12 @@ async function filterFieldsBySelectionState(elements, state) {
4284
4516
  }
4285
4517
 
4286
4518
  async function elementSelected(element) {
4287
- const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
4519
+ const type = await element.getAttribute('type');
4288
4520
 
4289
4521
  if (type === 'checkbox' || type === 'radio') {
4290
4522
  return element.isChecked();
4291
4523
  }
4292
- return element.getProperty('selected').then(el => el.jsonValue());
4524
+ return element.getAttribute('selected');
4293
4525
  }
4294
4526
 
4295
4527
  function isFrameLocator(locator) {
@@ -4490,7 +4722,7 @@ async function saveTraceForContext(context, name) {
4490
4722
  }
4491
4723
 
4492
4724
  function highlightActiveElement(element, context) {
4493
- if (!this.options.enableHighlight && !store.debugMode) return;
4725
+ if (!this.options.highlightElement && !store.debugMode) return;
4494
4726
 
4495
4727
  highlightElement(element, context);
4496
4728
  }