codeceptjs 3.5.4-beta.1 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/README.md +0 -2
  3. package/docs/build/Appium.js +48 -7
  4. package/docs/build/GraphQL.js +25 -0
  5. package/docs/build/Nightmare.js +15 -6
  6. package/docs/build/Playwright.js +436 -197
  7. package/docs/build/Protractor.js +17 -8
  8. package/docs/build/Puppeteer.js +37 -20
  9. package/docs/build/TestCafe.js +19 -10
  10. package/docs/build/WebDriver.js +45 -37
  11. package/docs/changelog.md +375 -0
  12. package/docs/community-helpers.md +8 -4
  13. package/docs/examples.md +8 -2
  14. package/docs/helpers/Appium.md +39 -2
  15. package/docs/helpers/GraphQL.md +21 -0
  16. package/docs/helpers/Nightmare.md +1260 -0
  17. package/docs/helpers/Playwright.md +223 -119
  18. package/docs/helpers/Protractor.md +1711 -0
  19. package/docs/helpers/Puppeteer.md +31 -29
  20. package/docs/helpers/TestCafe.md +18 -17
  21. package/docs/helpers/WebDriver.md +34 -32
  22. package/docs/playwright.md +24 -1
  23. package/docs/webapi/dontSeeInField.mustache +1 -1
  24. package/docs/webapi/executeAsyncScript.mustache +2 -0
  25. package/docs/webapi/executeScript.mustache +2 -0
  26. package/docs/webapi/seeInField.mustache +1 -1
  27. package/docs/wiki/Books-&-Posts.md +0 -0
  28. package/docs/wiki/Community-Helpers-&-Plugins.md +8 -4
  29. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +46 -14
  30. package/docs/wiki/Examples.md +8 -2
  31. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -0
  32. package/docs/wiki/Home.md +0 -0
  33. package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
  34. package/docs/wiki/Release-Process.md +0 -0
  35. package/docs/wiki/Roadmap.md +0 -0
  36. package/docs/wiki/Tests.md +0 -0
  37. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -0
  38. package/docs/wiki/Videos.md +0 -0
  39. package/lib/codecept.js +1 -0
  40. package/lib/command/definitions.js +2 -7
  41. package/lib/command/init.js +40 -4
  42. package/lib/command/run-multiple/collection.js +17 -5
  43. package/lib/command/run-workers.js +4 -0
  44. package/lib/command/run.js +6 -0
  45. package/lib/helper/Appium.js +46 -5
  46. package/lib/helper/GraphQL.js +25 -0
  47. package/lib/helper/Nightmare.js +1415 -0
  48. package/lib/helper/Playwright.js +336 -62
  49. package/lib/helper/Protractor.js +1837 -0
  50. package/lib/helper/Puppeteer.js +31 -18
  51. package/lib/helper/TestCafe.js +15 -8
  52. package/lib/helper/WebDriver.js +39 -35
  53. package/lib/helper/clientscripts/nightmare.js +213 -0
  54. package/lib/helper/errors/ElementNotFound.js +2 -1
  55. package/lib/helper/scripts/highlightElement.js +1 -1
  56. package/lib/interfaces/bdd.js +1 -1
  57. package/lib/mochaFactory.js +2 -1
  58. package/lib/pause.js +6 -4
  59. package/lib/plugin/heal.js +2 -3
  60. package/lib/plugin/selenoid.js +6 -1
  61. package/lib/step.js +27 -10
  62. package/lib/utils.js +4 -0
  63. package/lib/workers.js +3 -1
  64. package/package.json +87 -87
  65. package/typings/promiseBasedTypes.d.ts +163 -126
  66. package/typings/types.d.ts +183 -144
  67. package/docs/build/Polly.js +0 -42
  68. package/docs/build/SeleniumWebdriver.js +0 -76
@@ -23,6 +23,7 @@ const {
23
23
  isModifierKey,
24
24
  clearString,
25
25
  requireWithFallback,
26
+ normalizeSpacesInString,
26
27
  } = require('../utils');
27
28
  const {
28
29
  isColorProperty,
@@ -76,7 +77,7 @@ const pathSeparator = path.sep;
76
77
  * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
77
78
  * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
78
79
  * @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
79
- * @prop {'load' | 'domcontentloaded' | 'networkidle'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-navigation).
80
+ * @prop {'load' | 'domcontentloaded' | 'commit'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `commit`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-url).
80
81
  * @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
81
82
  * @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
82
83
  * @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
@@ -93,7 +94,7 @@ const pathSeparator = path.sep;
93
94
  * @prop {string[]} [ignoreLog] - An array with console message types that are not logged to debug log. Default value is `['warning', 'log']`. E.g. you can set `[]` to log all messages. See all possible [values](https://playwright.dev/docs/api/class-consolemessage#console-message-type).
94
95
  * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
95
96
  * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
96
- * @prop {boolean} [highlightElement] - highlight the interacting elements
97
+ * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false
97
98
  */
98
99
  const config = {};
99
100
 
@@ -207,6 +208,7 @@ const config = {};
207
208
  * url: "http://localhost",
208
209
  * show: true // headless mode not supported for extensions
209
210
  * chromium: {
211
+ * // Note: due to this would launch persistent context, so to avoid the error when running tests with run-workers a timestamp would be appended to the defined folder name. For instance: playwright-tmp_1692715649511
210
212
  * userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
211
213
  * args: [
212
214
  * `--disable-extensions-except=${pathToExtension}`,
@@ -317,6 +319,12 @@ class Playwright extends Helper {
317
319
  this.recording = false;
318
320
  this.recordedAtLeastOnce = false;
319
321
 
322
+ // for websocket messages
323
+ this.webSocketMessages = [];
324
+ this.recordingWebSocketMessages = false;
325
+ this.recordedWebSocketMessagesAtLeastOnce = false;
326
+ this.cdpSession = null;
327
+
320
328
  // override defaults with config
321
329
  this._setConfig(config);
322
330
  }
@@ -343,7 +351,8 @@ class Playwright extends Helper {
343
351
  show: false,
344
352
  defaultPopupAction: 'accept',
345
353
  use: { actionTimeout: 0 },
346
- ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors
354
+ ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
355
+ highlightElement: false,
347
356
  };
348
357
 
349
358
  config = Object.assign(defaults, config);
@@ -388,7 +397,7 @@ class Playwright extends Helper {
388
397
  }
389
398
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
390
399
  this.isElectron = this.options.browser === 'electron';
391
- this.userDataDir = this.playwrightOptions.userDataDir;
400
+ this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined;
392
401
  this.isCDPConnection = this.playwrightOptions.cdpConnection;
393
402
  popupStore.defaultAction = this.options.defaultPopupAction;
394
403
  }
@@ -461,7 +470,7 @@ class Playwright extends Helper {
461
470
  this.isAuthenticated = false;
462
471
  if (this.isElectron) {
463
472
  this.browserContext = this.browser.context();
464
- } else if (this.userDataDir) {
473
+ } else if (this.playwrightOptions.userDataDir) {
465
474
  this.browserContext = this.browser;
466
475
  } else {
467
476
  const contextOptions = {
@@ -473,6 +482,7 @@ class Playwright extends Helper {
473
482
  contextOptions.httpCredentials = this.options.basicAuth;
474
483
  this.isAuthenticated = true;
475
484
  }
485
+ if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
476
486
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
477
487
  if (this.storageState) contextOptions.storageState = this.storageState;
478
488
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
@@ -487,8 +497,17 @@ class Playwright extends Helper {
487
497
  if (this.isElectron) {
488
498
  mainPage = await this.browser.firstWindow();
489
499
  } else {
490
- const existingPages = await this.browserContext.pages();
491
- mainPage = existingPages[0] || await this.browserContext.newPage();
500
+ try {
501
+ const existingPages = await this.browserContext.pages();
502
+ mainPage = existingPages[0] || await this.browserContext.newPage();
503
+ } catch (e) {
504
+ if (this.playwrightOptions.userDataDir) {
505
+ this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
506
+ this.browserContext = this.browser;
507
+ const existingPages = await this.browserContext.pages();
508
+ mainPage = existingPages[0];
509
+ }
510
+ }
492
511
  }
493
512
  await targetCreatedHandler.call(this, mainPage);
494
513
 
@@ -519,13 +538,15 @@ class Playwright extends Helper {
519
538
 
520
539
  // close other sessions
521
540
  try {
522
- const contexts = await this.browser.contexts();
523
- const currentContext = contexts[0];
524
- if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
525
- this.storageState = await currentContext.storageState();
526
- }
541
+ if ((await this.browser)._type === 'Browser') {
542
+ const contexts = await this.browser.contexts();
543
+ const currentContext = contexts[0];
544
+ if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
545
+ this.storageState = await currentContext.storageState();
546
+ }
527
547
 
528
- await Promise.all(contexts.map(c => c.close()));
548
+ await Promise.all(contexts.map(c => c.close()));
549
+ }
529
550
  } catch (e) {
530
551
  console.log(e);
531
552
  }
@@ -555,8 +576,16 @@ class Playwright extends Helper {
555
576
  browserContext = browser.context();
556
577
  page = await browser.firstWindow();
557
578
  } else {
558
- browserContext = await this.browser.newContext(Object.assign(this.options, config));
559
- page = await browserContext.newPage();
579
+ try {
580
+ browserContext = await this.browser.newContext(Object.assign(this.options, config));
581
+ page = await browserContext.newPage();
582
+ } catch (e) {
583
+ if (this.playwrightOptions.userDataDir) {
584
+ browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
585
+ this.browser = browserContext;
586
+ page = await browserContext.pages()[0];
587
+ }
588
+ }
560
589
  }
561
590
 
562
591
  if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
@@ -569,10 +598,12 @@ class Playwright extends Helper {
569
598
  // is closed by _after
570
599
  },
571
600
  loadVars: async (context) => {
572
- this.browserContext = context;
573
- const existingPages = await context.pages();
574
- this.sessionPages[this.activeSessionName] = existingPages[0];
575
- return this._setPage(this.sessionPages[this.activeSessionName]);
601
+ if (context) {
602
+ this.browserContext = context;
603
+ const existingPages = await context.pages();
604
+ this.sessionPages[this.activeSessionName] = existingPages[0];
605
+ return this._setPage(this.sessionPages[this.activeSessionName]);
606
+ }
576
607
  },
577
608
  restoreVars: async (session) => {
578
609
  this.withinLocator = null;
@@ -771,7 +802,7 @@ class Playwright extends Helper {
771
802
  }
772
803
  throw err;
773
804
  }
774
- } else if (this.userDataDir) {
805
+ } else if (this.playwrightOptions.userDataDir) {
775
806
  this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
776
807
  } else {
777
808
  this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
@@ -827,14 +858,14 @@ class Playwright extends Helper {
827
858
  await this.switchTo(null);
828
859
  return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
829
860
  }
830
- await this.switchTo(locator);
831
- this.withinLocator = new Locator(locator);
861
+ await this.switchTo(frame);
862
+ this.withinLocator = new Locator(frame);
832
863
  return;
833
864
  }
834
865
 
835
- const els = await this._locate(locator);
836
- assertElementExists(els, locator);
837
- this.context = els[0];
866
+ const el = await this._locateElement(locator);
867
+ assertElementExists(el, locator);
868
+ this.context = el;
838
869
  this.contextLocator = locator;
839
870
 
840
871
  this.withinLocator = new Locator(locator);
@@ -965,11 +996,11 @@ class Playwright extends Helper {
965
996
  *
966
997
  */
967
998
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
968
- const els = await this._locate(locator);
969
- assertElementExists(els, locator);
999
+ const el = await this._locateElement(locator);
1000
+ assertElementExists(el, locator);
970
1001
 
971
1002
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
972
- const { x, y } = await clickablePoint(els[0]);
1003
+ const { x, y } = await clickablePoint(el);
973
1004
  await this.page.mouse.move(x + offsetX, y + offsetY);
974
1005
  return this._waitForAction();
975
1006
  }
@@ -991,9 +1022,8 @@ class Playwright extends Helper {
991
1022
  *
992
1023
  */
993
1024
  async focus(locator, options = {}) {
994
- const els = await this._locate(locator);
995
- assertElementExists(els, locator, 'Element to focus');
996
- const el = els[0];
1025
+ const el = await this._locateElement(locator);
1026
+ assertElementExists(el, locator, 'Element to focus');
997
1027
 
998
1028
  await el.focus(options);
999
1029
  return this._waitForAction();
@@ -1021,12 +1051,10 @@ class Playwright extends Helper {
1021
1051
  *
1022
1052
  */
1023
1053
  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]);
1054
+ const el = await this._locateElement(locator);
1055
+ assertElementExists(el, locator, 'Element to blur');
1028
1056
 
1029
- await this.page.locator(elXpath).blur(options);
1057
+ await el.blur(options);
1030
1058
  return this._waitForAction();
1031
1059
  }
1032
1060
 
@@ -1133,8 +1161,11 @@ class Playwright extends Helper {
1133
1161
  const body = document.body;
1134
1162
  const html = document.documentElement;
1135
1163
  window.scrollTo(0, Math.max(
1136
- body.scrollHeight, body.offsetHeight,
1137
- html.clientHeight, html.scrollHeight, html.offsetHeight,
1164
+ body.scrollHeight,
1165
+ body.offsetHeight,
1166
+ html.clientHeight,
1167
+ html.scrollHeight,
1168
+ html.offsetHeight,
1138
1169
  ));
1139
1170
  });
1140
1171
  }
@@ -1162,10 +1193,10 @@ class Playwright extends Helper {
1162
1193
  }
1163
1194
 
1164
1195
  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]);
1196
+ const el = await this._locateElement(locator);
1197
+ assertElementExists(el, locator, 'Element');
1198
+ await el.scrollIntoViewIfNeeded();
1199
+ const elementCoordinates = await clickablePoint(el);
1169
1200
  await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
1170
1201
  } else {
1171
1202
  await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
@@ -1272,7 +1303,20 @@ class Playwright extends Helper {
1272
1303
  }
1273
1304
 
1274
1305
  /**
1275
- * Find a checkbox by providing human readable text:
1306
+ * Get the first element by different locator types, including strict locator
1307
+ * Should be used in custom helpers:
1308
+ *
1309
+ * ```js
1310
+ * const element = await this.helpers['Playwright']._locateElement({name: 'password'});
1311
+ * ```
1312
+ */
1313
+ async _locateElement(locator) {
1314
+ const context = await this.context || await this._getContext();
1315
+ return findElement(context, locator);
1316
+ }
1317
+
1318
+ /**
1319
+ * Find a checkbox by providing human-readable text:
1276
1320
  * NOTE: Assumes the checkable element exists
1277
1321
  *
1278
1322
  * ```js
@@ -1287,7 +1331,7 @@ class Playwright extends Helper {
1287
1331
  }
1288
1332
 
1289
1333
  /**
1290
- * Find a clickable element by providing human readable text:
1334
+ * Find a clickable element by providing human-readable text:
1291
1335
  *
1292
1336
  * ```js
1293
1337
  * this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -1299,7 +1343,7 @@ class Playwright extends Helper {
1299
1343
  }
1300
1344
 
1301
1345
  /**
1302
- * Find field elements by providing human readable text:
1346
+ * Find field elements by providing human-readable text:
1303
1347
  *
1304
1348
  * ```js
1305
1349
  * this.helpers['Playwright']._locateFields('Your email').then // ...
@@ -1962,15 +2006,10 @@ class Playwright extends Helper {
1962
2006
  const els = await findFields.call(this, field);
1963
2007
  assertElementExists(els, field, 'Field');
1964
2008
  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
2009
 
1973
- highlightActiveElement.call(this, el, this.page);
2010
+ await el.clear();
2011
+
2012
+ highlightActiveElement.call(this, el, await this._getContext());
1974
2013
 
1975
2014
  await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1976
2015
 
@@ -1995,21 +2034,16 @@ class Playwright extends Helper {
1995
2034
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1996
2035
  */
1997
2036
  async clearField(locator, options = {}) {
1998
- let result;
1999
- const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
2037
+ const els = await findFields.call(this, locator);
2038
+ assertElementExists(els, locator, 'Field to clear');
2000
2039
 
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]);
2040
+ const el = els[0];
2006
2041
 
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;
2042
+ highlightActiveElement.call(this, el, this.page);
2043
+
2044
+ await el.clear();
2045
+
2046
+ return this._waitForAction();
2013
2047
  }
2014
2048
 
2015
2049
  /**
@@ -2031,7 +2065,7 @@ class Playwright extends Helper {
2031
2065
  async appendField(field, value) {
2032
2066
  const els = await findFields.call(this, field);
2033
2067
  assertElementExists(els, field, 'Field');
2034
- highlightActiveElement.call(this, els[0], this.page);
2068
+ highlightActiveElement.call(this, els[0], await this._getContext());
2035
2069
  await els[0].press('End');
2036
2070
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
2037
2071
  return this._waitForAction();
@@ -2048,12 +2082,13 @@ class Playwright extends Helper {
2048
2082
  * I.seeInField('#searchform input','Search');
2049
2083
  * ```
2050
2084
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2051
- * @param {string} value value to check.
2085
+ * @param {CodeceptJS.StringOrSecret} value value to check.
2052
2086
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
2053
2087
  *
2054
2088
  */
2055
2089
  async seeInField(field, value) {
2056
- return proceedSeeInField.call(this, 'assert', field, value);
2090
+ const _value = (typeof value === 'boolean') ? value : value.toString();
2091
+ return proceedSeeInField.call(this, 'assert', field, _value);
2057
2092
  }
2058
2093
 
2059
2094
  /**
@@ -2066,12 +2101,13 @@ class Playwright extends Helper {
2066
2101
  * ```
2067
2102
  *
2068
2103
  * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2069
- * @param {string} value value to check.
2104
+ * @param {CodeceptJS.StringOrSecret} value value to check.
2070
2105
  * ⚠️ returns a _promise_ which is synchronized internally by recorder
2071
2106
  *
2072
2107
  */
2073
2108
  async dontSeeInField(field, value) {
2074
- return proceedSeeInField.call(this, 'negate', field, value);
2109
+ const _value = (typeof value === 'boolean') ? value : value.toString();
2110
+ return proceedSeeInField.call(this, 'negate', field, _value);
2075
2111
  }
2076
2112
 
2077
2113
  /**
@@ -2130,29 +2166,12 @@ class Playwright extends Helper {
2130
2166
  const els = await findFields.call(this, select);
2131
2167
  assertElementExists(els, select, 'Selectable field');
2132
2168
  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
2169
 
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);
2170
+ highlightActiveElement.call(this, el, await this._getContext());
2155
2171
 
2172
+ if (!Array.isArray(option)) option = [option];
2173
+
2174
+ await el.selectOption(option);
2156
2175
  return this._waitForAction();
2157
2176
  }
2158
2177
 
@@ -2598,7 +2617,7 @@ class Playwright extends Helper {
2598
2617
  const els = await this._locate(locator);
2599
2618
  const texts = [];
2600
2619
  for (const el of els) {
2601
- texts.push(await (await el.getProperty('innerText')).jsonValue());
2620
+ texts.push(await (await el.innerText()));
2602
2621
  }
2603
2622
  this.debug(`Matched ${els.length} elements`);
2604
2623
  return texts;
@@ -2637,7 +2656,7 @@ class Playwright extends Helper {
2637
2656
  async grabValueFromAll(locator) {
2638
2657
  const els = await findFields.call(this, locator);
2639
2658
  this.debug(`Matched ${els.length} elements`);
2640
- return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
2659
+ return Promise.all(els.map(el => el.inputValue()));
2641
2660
  }
2642
2661
 
2643
2662
  /**
@@ -2675,7 +2694,7 @@ class Playwright extends Helper {
2675
2694
  async grabHTMLFromAll(locator) {
2676
2695
  const els = await this._locate(locator);
2677
2696
  this.debug(`Matched ${els.length} elements`);
2678
- return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
2697
+ return Promise.all(els.map(el => el.innerHTML()));
2679
2698
  }
2680
2699
 
2681
2700
  /**
@@ -2717,7 +2736,7 @@ class Playwright extends Helper {
2717
2736
  async grabCssPropertyFromAll(locator, cssProperty) {
2718
2737
  const els = await this._locate(locator);
2719
2738
  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)));
2739
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
2721
2740
 
2722
2741
  return cssValues;
2723
2742
  }
@@ -2742,21 +2761,20 @@ class Playwright extends Helper {
2742
2761
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
2743
2762
  const elemAmount = res.length;
2744
2763
  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
- }));
2764
+ let props = [];
2765
+
2766
+ for (const element of res) {
2767
+ const cssProperties = await element.evaluate((el) => getComputedStyle(el));
2768
+
2769
+ Object.keys(cssPropertiesCamelCase).forEach(prop => {
2770
+ if (isColorProperty(prop)) {
2771
+ props.push(convertColorToRGBA(cssProperties[prop]));
2772
+ } else {
2773
+ props.push(cssProperties[prop]);
2774
+ }
2757
2775
  });
2758
- });
2759
- let props = await Promise.all(commands);
2776
+ }
2777
+
2760
2778
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
2761
2779
  if (!Array.isArray(props)) props = [props];
2762
2780
  let chunked = chunkArray(props, values.length);
@@ -2791,7 +2809,7 @@ class Playwright extends Helper {
2791
2809
  res.forEach((el) => {
2792
2810
  Object.keys(attributes).forEach((prop) => {
2793
2811
  commands.push(el
2794
- .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
2812
+ .evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
2795
2813
  });
2796
2814
  });
2797
2815
  let attrs = await Promise.all(commands);
@@ -2823,11 +2841,11 @@ class Playwright extends Helper {
2823
2841
  *
2824
2842
  */
2825
2843
  async dragSlider(locator, offsetX = 0) {
2826
- const src = await this._locate(locator);
2844
+ const src = await this._locateElement(locator);
2827
2845
  assertElementExists(src, locator, 'Slider Element');
2828
2846
 
2829
2847
  // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
2830
- const sliderSource = await clickablePoint(src[0]);
2848
+ const sliderSource = await clickablePoint(src);
2831
2849
 
2832
2850
  // Drag start point
2833
2851
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2880,8 +2898,7 @@ class Playwright extends Helper {
2880
2898
  const array = [];
2881
2899
 
2882
2900
  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());
2901
+ array.push(await els[index].getAttribute(attr));
2885
2902
  }
2886
2903
 
2887
2904
  return array;
@@ -2904,10 +2921,9 @@ class Playwright extends Helper {
2904
2921
  async saveElementScreenshot(locator, fileName) {
2905
2922
  const outputFile = screenshotOutputFolder(fileName);
2906
2923
 
2907
- const res = await this._locate(locator);
2924
+ const res = await this._locateElement(locator);
2908
2925
  assertElementExists(res, locator);
2909
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
2910
- const elem = res[0];
2926
+ const elem = res;
2911
2927
  this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
2912
2928
  return elem.screenshot({ path: outputFile, type: 'png' });
2913
2929
  }
@@ -3472,22 +3488,35 @@ class Playwright extends Helper {
3472
3488
  }
3473
3489
  return;
3474
3490
  }
3491
+ let contentFrame;
3492
+
3475
3493
  if (!locator) {
3476
- this.context = this.page;
3494
+ this.context = await this.page.frames()[0];
3477
3495
  this.contextLocator = null;
3478
3496
  return;
3479
3497
  }
3480
3498
 
3481
3499
  // iframe by selector
3482
3500
  const els = await this._locate(locator);
3483
- assertElementExists(els, locator);
3484
- const contentFrame = await els[0].contentFrame();
3501
+ if (!els[0]) {
3502
+ throw new Error(`Element ${JSON.stringify(locator)} was not found by text|CSS|XPath`);
3503
+ }
3504
+
3505
+ // get content of the first iframe
3506
+ locator = new Locator(locator, 'css');
3507
+ if ((locator.frame && locator.frame === 'iframe') || locator.value.toLowerCase() === 'iframe') {
3508
+ contentFrame = await this.page.frames()[1];
3509
+ // get content of the iframe using its name
3510
+ } else if (locator.value.toLowerCase().includes('name=')) {
3511
+ const frameName = locator.value.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
3512
+ contentFrame = await this.page.frame(frameName);
3513
+ }
3485
3514
 
3486
3515
  if (contentFrame) {
3487
3516
  this.context = contentFrame;
3488
3517
  this.contextLocator = null;
3489
3518
  } else {
3490
- this.context = els[0];
3519
+ this.context = this.page.frame(this.page.frames()[1].name());
3491
3520
  this.contextLocator = locator;
3492
3521
  }
3493
3522
  }
@@ -3527,13 +3556,15 @@ class Playwright extends Helper {
3527
3556
  }
3528
3557
 
3529
3558
  /**
3530
- * Waits for navigation to finish. By default takes configured `waitForNavigation` option.
3559
+ * Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
3531
3560
  *
3532
3561
  * See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
3533
3562
  *
3534
3563
  * @param {*} options
3535
3564
  */
3536
3565
  async waitForNavigation(options = {}) {
3566
+ console.log(`waitForNavigation deprecated:
3567
+ * This method is inherently racy, please use 'waitForURL' instead.`);
3537
3568
  options = {
3538
3569
  timeout: this.options.getPageTimeout,
3539
3570
  waitUntil: this.options.waitForNavigation,
@@ -3542,6 +3573,23 @@ class Playwright extends Helper {
3542
3573
  return this.page.waitForNavigation(options);
3543
3574
  }
3544
3575
 
3576
+ /**
3577
+ * Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
3578
+ *
3579
+ * See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
3580
+ *
3581
+ * @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.
3582
+ * @param {*} options
3583
+ */
3584
+ async waitForURL(url, options = {}) {
3585
+ options = {
3586
+ timeout: this.options.getPageTimeout,
3587
+ waitUntil: this.options.waitForNavigation,
3588
+ ...options,
3589
+ };
3590
+ return this.page.waitForURL(url, options);
3591
+ }
3592
+
3545
3593
  async waitUntilExists(locator, sec) {
3546
3594
  console.log(`waitUntilExists deprecated:
3547
3595
  * use 'waitForElement' to wait for element to be attached
@@ -3636,9 +3684,9 @@ class Playwright extends Helper {
3636
3684
  *
3637
3685
  */
3638
3686
  async grabElementBoundingRect(locator, prop) {
3639
- const els = await this._locate(locator);
3640
- assertElementExists(els, locator);
3641
- const rect = await els[0].boundingBox();
3687
+ const el = await this._locateElement(locator);
3688
+ assertElementExists(el, locator);
3689
+ const rect = await el.boundingBox();
3642
3690
  if (prop) return rect[prop];
3643
3691
  return rect;
3644
3692
  }
@@ -3675,16 +3723,16 @@ class Playwright extends Helper {
3675
3723
  }
3676
3724
 
3677
3725
  /**
3678
- * Starts recording of network traffic.
3726
+ * Starts recording the network traffics.
3679
3727
  * This also resets recorded network requests.
3680
3728
  *
3681
3729
  * ```js
3682
3730
  * I.startRecordingTraffic();
3683
3731
  * ```
3684
3732
  *
3685
- * @return {Promise<void>}
3733
+ * @return {void}
3686
3734
  */
3687
- async startRecordingTraffic() {
3735
+ startRecordingTraffic() {
3688
3736
  this.flushNetworkTraffics();
3689
3737
  this.recording = true;
3690
3738
  this.recordedAtLeastOnce = true;
@@ -3695,31 +3743,62 @@ class Playwright extends Helper {
3695
3743
  method: request.method(),
3696
3744
  requestHeaders: request.headers(),
3697
3745
  requestPostData: request.postData(),
3746
+ response: request.response(),
3698
3747
  };
3699
3748
 
3700
3749
  this.debugSection('REQUEST: ', JSON.stringify(information));
3701
3750
 
3702
- information.requestPostData = JSON.parse(information.requestPostData);
3751
+ if (typeof information.requestPostData === 'object') {
3752
+ information.requestPostData = JSON.parse(information.requestPostData);
3753
+ }
3754
+
3703
3755
  this.requests.push(information);
3704
- return this._waitForAction();
3705
3756
  });
3706
3757
  }
3707
3758
 
3708
3759
  /**
3709
3760
  * Grab the recording network traffics
3710
3761
  *
3711
- * @return { Array<any> }
3762
+ * ```js
3763
+ * const traffics = await I.grabRecordedNetworkTraffics();
3764
+ * expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
3765
+ * expect(traffics[0].response.status).to.equal(200);
3766
+ * expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
3767
+ * ```
3768
+ *
3769
+ * @return { Promise<Array<any>> }
3712
3770
  *
3713
3771
  */
3714
- grabRecordedNetworkTraffics() {
3772
+ async grabRecordedNetworkTraffics() {
3715
3773
  if (!this.recording || !this.recordedAtLeastOnce) {
3716
3774
  throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
3717
3775
  }
3776
+
3777
+ const requests = await this.requests;
3778
+ const promises = requests.map(async (request) => request.response.then(
3779
+ async (response) => {
3780
+ let body;
3781
+ try {
3782
+ // There's no 'body' for some requests (redirect etc...)
3783
+ body = JSON.parse((await response.body()).toString());
3784
+ } catch (e) {
3785
+ // only interested in JSON, not HTML responses.
3786
+ }
3787
+
3788
+ request.response = {
3789
+ status: response.status(),
3790
+ statusText: response.statusText(),
3791
+ body,
3792
+ };
3793
+ },
3794
+ ));
3795
+ await Promise.all(promises);
3796
+
3718
3797
  return this.requests;
3719
3798
  }
3720
3799
 
3721
3800
  /**
3722
- * Blocks traffic for URL.
3801
+ * Blocks traffic of a given URL or a list of URLs.
3723
3802
  *
3724
3803
  * Examples:
3725
3804
  *
@@ -3730,16 +3809,30 @@ class Playwright extends Helper {
3730
3809
  * I.blockTraffic(/\.css$/);
3731
3810
  * ```
3732
3811
  *
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();
3812
+ * ```js
3813
+ * I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
3814
+ * ```
3815
+ *
3816
+ * @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.
3817
+ */
3818
+ blockTraffic(urls) {
3819
+ if (Array.isArray(urls)) {
3820
+ urls.forEach(url => {
3821
+ this.page.route(url, (route) => {
3822
+ route
3823
+ .abort()
3824
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3825
+ .catch((e) => {});
3826
+ });
3827
+ });
3828
+ } else {
3829
+ this.page.route(urls, (route) => {
3830
+ route
3831
+ .abort()
3832
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
3833
+ .catch((e) => {});
3834
+ });
3835
+ }
3743
3836
  }
3744
3837
 
3745
3838
  /**
@@ -3758,7 +3851,7 @@ class Playwright extends Helper {
3758
3851
  * @param responseString string The string to return in fake response's body.
3759
3852
  * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
3760
3853
  */
3761
- async mockTraffic(urls, responseString, contentType = 'application/json') {
3854
+ mockTraffic(urls, responseString, contentType = 'application/json') {
3762
3855
  // Required to mock cross-domain requests
3763
3856
  const headers = { 'access-control-allow-origin': '*' };
3764
3857
 
@@ -3780,7 +3873,6 @@ class Playwright extends Helper {
3780
3873
  });
3781
3874
  });
3782
3875
  });
3783
- return this._waitForAction();
3784
3876
  }
3785
3877
 
3786
3878
  /**
@@ -3852,7 +3944,7 @@ class Playwright extends Helper {
3852
3944
  }
3853
3945
 
3854
3946
  if (!this.recording || !this.recordedAtLeastOnce) {
3855
- throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
3947
+ throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
3856
3948
  }
3857
3949
 
3858
3950
  for (let i = 0; i <= timeout * 2; i++) {
@@ -3860,7 +3952,9 @@ class Playwright extends Helper {
3860
3952
  if (found) {
3861
3953
  return true;
3862
3954
  }
3863
- await new Promise((done) => setTimeout(done, 1000));
3955
+ await new Promise((done) => {
3956
+ setTimeout(done, 1000);
3957
+ });
3864
3958
  }
3865
3959
 
3866
3960
  // check request post data
@@ -3997,6 +4091,163 @@ class Playwright extends Helper {
3997
4091
  });
3998
4092
  return dumpedTraffic;
3999
4093
  }
4094
+
4095
+ /**
4096
+ * Starts recording of websocket messages.
4097
+ * This also resets recorded websocket messages.
4098
+ *
4099
+ * ```js
4100
+ * await I.startRecordingWebSocketMessages();
4101
+ * ```
4102
+ *
4103
+ */
4104
+ async startRecordingWebSocketMessages() {
4105
+ this.flushWebSocketMessages();
4106
+ this.recordingWebSocketMessages = true;
4107
+ this.recordedWebSocketMessagesAtLeastOnce = true;
4108
+
4109
+ this.cdpSession = await this.getNewCDPSession();
4110
+ await this.cdpSession.send('Network.enable');
4111
+ await this.cdpSession.send('Page.enable');
4112
+
4113
+ this.cdpSession.on(
4114
+ 'Network.webSocketFrameReceived',
4115
+ (payload) => {
4116
+ this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
4117
+ },
4118
+ );
4119
+
4120
+ this.cdpSession.on(
4121
+ 'Network.webSocketFrameSent',
4122
+ (payload) => {
4123
+ this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
4124
+ },
4125
+ );
4126
+
4127
+ this.cdpSession.on(
4128
+ 'Network.webSocketFrameError',
4129
+ (payload) => {
4130
+ this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
4131
+ },
4132
+ );
4133
+ }
4134
+
4135
+ /**
4136
+ * Stops recording WS messages. Recorded WS messages is not flashed.
4137
+ *
4138
+ * ```js
4139
+ * await I.stopRecordingWebSocketMessages();
4140
+ * ```
4141
+ */
4142
+ async stopRecordingWebSocketMessages() {
4143
+ await this.cdpSession.send('Network.disable');
4144
+ await this.cdpSession.send('Page.disable');
4145
+ this.page.removeAllListeners('Network');
4146
+ this.recordingWebSocketMessages = false;
4147
+ }
4148
+
4149
+ /**
4150
+ * Grab the recording WS messages
4151
+ *
4152
+ * @return { Array<any> }
4153
+ *
4154
+ */
4155
+ grabWebSocketMessages() {
4156
+ if (!this.recordingWebSocketMessages) {
4157
+ if (!this.recordedWebSocketMessagesAtLeastOnce) {
4158
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
4159
+ }
4160
+ }
4161
+ return this.webSocketMessages;
4162
+ }
4163
+
4164
+ /**
4165
+ * Resets all recorded WS messages.
4166
+ */
4167
+ flushWebSocketMessages() {
4168
+ this.webSocketMessages = [];
4169
+ }
4170
+
4171
+ /**
4172
+ * Return a performance metric from the chrome cdp session.
4173
+ * Note: Chrome-only
4174
+ *
4175
+ * Examples:
4176
+ *
4177
+ * ```js
4178
+ * const metrics = await I.grabMetrics();
4179
+ *
4180
+ * // returned metrics
4181
+ *
4182
+ * [
4183
+ * { name: 'Timestamp', value: 1584904.203473 },
4184
+ * { name: 'AudioHandlers', value: 0 },
4185
+ * { name: 'AudioWorkletProcessors', value: 0 },
4186
+ * { name: 'Documents', value: 22 },
4187
+ * { name: 'Frames', value: 10 },
4188
+ * { name: 'JSEventListeners', value: 366 },
4189
+ * { name: 'LayoutObjects', value: 1240 },
4190
+ * { name: 'MediaKeySessions', value: 0 },
4191
+ * { name: 'MediaKeys', value: 0 },
4192
+ * { name: 'Nodes', value: 4505 },
4193
+ * { name: 'Resources', value: 141 },
4194
+ * { name: 'ContextLifecycleStateObservers', value: 34 },
4195
+ * { name: 'V8PerContextDatas', value: 4 },
4196
+ * { name: 'WorkerGlobalScopes', value: 0 },
4197
+ * { name: 'UACSSResources', value: 0 },
4198
+ * { name: 'RTCPeerConnections', value: 0 },
4199
+ * { name: 'ResourceFetchers', value: 22 },
4200
+ * { name: 'AdSubframes', value: 0 },
4201
+ * { name: 'DetachedScriptStates', value: 2 },
4202
+ * { name: 'ArrayBufferContents', value: 1 },
4203
+ * { name: 'LayoutCount', value: 0 },
4204
+ * { name: 'RecalcStyleCount', value: 0 },
4205
+ * { name: 'LayoutDuration', value: 0 },
4206
+ * { name: 'RecalcStyleDuration', value: 0 },
4207
+ * { name: 'DevToolsCommandDuration', value: 0.000013 },
4208
+ * { name: 'ScriptDuration', value: 0 },
4209
+ * { name: 'V8CompileDuration', value: 0 },
4210
+ * { name: 'TaskDuration', value: 0.000014 },
4211
+ * { name: 'TaskOtherDuration', value: 0.000001 },
4212
+ * { name: 'ThreadTime', value: 0.000046 },
4213
+ * { name: 'ProcessTime', value: 0.616852 },
4214
+ * { name: 'JSHeapUsedSize', value: 19004908 },
4215
+ * { name: 'JSHeapTotalSize', value: 26820608 },
4216
+ * { name: 'FirstMeaningfulPaint', value: 0 },
4217
+ * { name: 'DomContentLoaded', value: 1584903.690491 },
4218
+ * { name: 'NavigationStart', value: 1584902.841845 }
4219
+ * ]
4220
+ *
4221
+ * ```
4222
+ *
4223
+ * @return {Promise<Array<Object>>}
4224
+ */
4225
+ async grabMetrics() {
4226
+ const client = await this.page.context().newCDPSession(this.page);
4227
+ await client.send('Performance.enable');
4228
+ const perfMetricObject = await client.send('Performance.getMetrics');
4229
+ return perfMetricObject?.metrics;
4230
+ }
4231
+
4232
+ _getWebSocketMessage(payload) {
4233
+ if (payload.errorMessage) {
4234
+ return payload.errorMessage;
4235
+ }
4236
+
4237
+ return payload.response.payloadData;
4238
+ }
4239
+
4240
+ _getWebSocketLog(prefix, payload) {
4241
+ return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
4242
+ }
4243
+
4244
+ async getNewCDPSession() {
4245
+ return this.page.context().newCDPSession(this.page);
4246
+ }
4247
+
4248
+ _logWebsocketMessages(message) {
4249
+ this.webSocketMessages += message;
4250
+ }
4000
4251
  }
4001
4252
 
4002
4253
  module.exports = Playwright;
@@ -4009,42 +4260,19 @@ function buildLocatorString(locator) {
4009
4260
  }
4010
4261
  return locator.simplify();
4011
4262
  }
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
4263
 
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
- }
4264
+ async function findElements(matcher, locator) {
4265
+ if (locator.react) return findReact(matcher, locator);
4266
+ locator = new Locator(locator, 'css');
4040
4267
 
4041
- return elementHandle.evaluate(generateXPath);
4268
+ return matcher.locator(buildLocatorString(locator)).all();
4042
4269
  }
4043
4270
 
4044
- async function findElements(matcher, locator) {
4271
+ async function findElement(matcher, locator) {
4045
4272
  if (locator.react) return findReact(matcher, locator);
4046
4273
  locator = new Locator(locator, 'css');
4047
- return matcher.$$(buildLocatorString(locator));
4274
+
4275
+ return matcher.locator(buildLocatorString(locator));
4048
4276
  }
4049
4277
 
4050
4278
  async function getVisibleElements(elements) {
@@ -4074,8 +4302,7 @@ async function proceedClick(locator, context = null, options = {}) {
4074
4302
  assertElementExists(els, locator, 'Clickable element');
4075
4303
  }
4076
4304
 
4077
- const element = els[0];
4078
- highlightActiveElement.call(this, els[0], this.page);
4305
+ highlightActiveElement.call(this, els[0], await this._getContext());
4079
4306
 
4080
4307
  /*
4081
4308
  using the force true options itself but instead dispatching a click
@@ -4088,7 +4315,7 @@ async function proceedClick(locator, context = null, options = {}) {
4088
4315
  }
4089
4316
  const promises = [];
4090
4317
  if (options.waitForNavigation) {
4091
- promises.push(this.waitForNavigation());
4318
+ promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
4092
4319
  }
4093
4320
  promises.push(this._waitForAction());
4094
4321
 
@@ -4123,28 +4350,28 @@ async function findClickable(matcher, locator) {
4123
4350
  async function proceedSee(assertType, text, context, strict = false) {
4124
4351
  let description;
4125
4352
  let allText;
4353
+
4126
4354
  if (!context) {
4127
4355
  let el = await this.context;
4128
-
4129
4356
  if (el && !el.getProperty) {
4130
4357
  // Fallback to body
4131
- el = await this.context.$('body');
4358
+ el = await this.page.$('body');
4132
4359
  }
4133
4360
 
4134
- allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
4361
+ allText = [await el.innerText()];
4135
4362
  description = 'web application';
4136
4363
  } else {
4137
4364
  const locator = new Locator(context, 'css');
4138
4365
  description = `element ${locator.toString()}`;
4139
4366
  const els = await this._locate(locator);
4140
4367
  assertElementExists(els, locator.toString());
4141
- allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
4368
+ allText = await Promise.all(els.map(el => el.innerText()));
4142
4369
  }
4143
4370
 
4144
4371
  if (strict) {
4145
4372
  return allText.map(elText => equals(description)[assertType](text, elText));
4146
4373
  }
4147
- return stringIncludes(description)[assertType](text, allText.join(' | '));
4374
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
4148
4375
  }
4149
4376
 
4150
4377
  async function findCheckable(locator, context) {
@@ -4206,15 +4433,15 @@ async function proceedSeeInField(assertType, field, value) {
4206
4433
  const els = await findFields.call(this, field);
4207
4434
  assertElementExists(els, field, 'Field');
4208
4435
  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());
4436
+ const tag = await el.evaluate(e => e.tagName);
4437
+ const fieldType = await el.getAttribute('type');
4211
4438
 
4212
4439
  const proceedMultiple = async (elements) => {
4213
4440
  const fields = Array.isArray(elements) ? elements : [elements];
4214
4441
 
4215
4442
  const elementValues = [];
4216
4443
  for (const element of fields) {
4217
- elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
4444
+ elementValues.push(await element.inputValue());
4218
4445
  }
4219
4446
 
4220
4447
  if (typeof value === 'boolean') {
@@ -4228,8 +4455,8 @@ async function proceedSeeInField(assertType, field, value) {
4228
4455
  };
4229
4456
 
4230
4457
  if (tag === 'SELECT') {
4231
- if (await el.getProperty('multiple')) {
4232
- const selectedOptions = await el.$$('option:checked');
4458
+ if (await el.getAttribute('multiple')) {
4459
+ const selectedOptions = await el.all('option:checked');
4233
4460
  if (!selectedOptions.length) return null;
4234
4461
 
4235
4462
  const options = await filterFieldsByValue(selectedOptions, value, true);
@@ -4253,14 +4480,23 @@ async function proceedSeeInField(assertType, field, value) {
4253
4480
  return proceedMultiple(els[0]);
4254
4481
  }
4255
4482
 
4256
- const fieldVal = await el.inputValue();
4483
+ let fieldVal;
4484
+
4485
+ try {
4486
+ fieldVal = await el.inputValue();
4487
+ } catch (e) {
4488
+ if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
4489
+ fieldVal = await el.innerText();
4490
+ }
4491
+ }
4492
+
4257
4493
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
4258
4494
  }
4259
4495
 
4260
4496
  async function filterFieldsByValue(elements, value, onlySelected) {
4261
4497
  const matches = [];
4262
4498
  for (const element of elements) {
4263
- const val = await element.getProperty('value').then(el => el.jsonValue());
4499
+ const val = await element.getAttribute('value');
4264
4500
  let isSelected = true;
4265
4501
  if (onlySelected) {
4266
4502
  isSelected = await elementSelected(element);
@@ -4284,17 +4520,20 @@ async function filterFieldsBySelectionState(elements, state) {
4284
4520
  }
4285
4521
 
4286
4522
  async function elementSelected(element) {
4287
- const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
4523
+ const type = await element.getAttribute('type');
4288
4524
 
4289
4525
  if (type === 'checkbox' || type === 'radio') {
4290
4526
  return element.isChecked();
4291
4527
  }
4292
- return element.getProperty('selected').then(el => el.jsonValue());
4528
+ return element.getAttribute('selected');
4293
4529
  }
4294
4530
 
4295
4531
  function isFrameLocator(locator) {
4296
4532
  locator = new Locator(locator);
4297
- if (locator.isFrame()) return locator.value;
4533
+ if (locator.isFrame()) {
4534
+ const _locator = new Locator(locator.value);
4535
+ return _locator.value;
4536
+ }
4298
4537
  return false;
4299
4538
  }
4300
4539
 
@@ -4490,7 +4729,7 @@ async function saveTraceForContext(context, name) {
4490
4729
  }
4491
4730
 
4492
4731
  function highlightActiveElement(element, context) {
4493
- if (!this.options.enableHighlight && !store.debugMode) return;
4732
+ if (!this.options.highlightElement && !store.debugMode) return;
4494
4733
 
4495
4734
  highlightElement(element, context);
4496
4735
  }