codeceptjs 3.5.2 → 3.5.4-beta.1

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.
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
 
4
4
  const Helper = require('@codeceptjs/helper');
5
5
  const { v4: uuidv4 } = require('uuid');
6
+ const assert = require('assert');
6
7
  const Locator = require('../locator');
7
8
  const store = require('../store');
8
9
  const recorder = require('../recorder');
@@ -311,6 +312,11 @@ class Playwright extends Helper {
311
312
  this.electronSessions = [];
312
313
  this.storageState = null;
313
314
 
315
+ // for network stuff
316
+ this.requests = [];
317
+ this.recording = false;
318
+ this.recordedAtLeastOnce = false;
319
+
314
320
  // override defaults with config
315
321
  this._setConfig(config);
316
322
  }
@@ -818,9 +824,9 @@ class Playwright extends Helper {
818
824
  return;
819
825
  }
820
826
 
821
- const els = await this._locate(locator);
822
- assertElementExists(els, locator);
823
- this.context = els[0];
827
+ const el = await this._locateElement(locator);
828
+ assertElementExists(el, locator);
829
+ this.context = el;
824
830
  this.contextLocator = locator;
825
831
 
826
832
  this.withinLocator = new Locator(locator);
@@ -923,11 +929,11 @@ class Playwright extends Helper {
923
929
  *
924
930
  */
925
931
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
926
- const els = await this._locate(locator);
927
- assertElementExists(els, locator);
932
+ const el = await this._locateElement(locator);
933
+ assertElementExists(el, locator);
928
934
 
929
935
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
930
- const { x, y } = await clickablePoint(els[0]);
936
+ const { x, y } = await clickablePoint(el);
931
937
  await this.page.mouse.move(x + offsetX, y + offsetY);
932
938
  return this._waitForAction();
933
939
  }
@@ -937,9 +943,8 @@ class Playwright extends Helper {
937
943
  *
938
944
  */
939
945
  async focus(locator, options = {}) {
940
- const els = await this._locate(locator);
941
- assertElementExists(els, locator, 'Element to focus');
942
- const el = els[0];
946
+ const el = await this._locateElement(locator);
947
+ assertElementExists(el, locator, 'Element to focus');
943
948
 
944
949
  await el.focus(options);
945
950
  return this._waitForAction();
@@ -950,12 +955,10 @@ class Playwright extends Helper {
950
955
  *
951
956
  */
952
957
  async blur(locator, options = {}) {
953
- const els = await this._locate(locator);
954
- assertElementExists(els, locator, 'Element to blur');
955
- // TODO: locator change required after #3677 implementation
956
- const elXpath = await getXPathForElement(els[0]);
958
+ const el = await this._locateElement(locator);
959
+ assertElementExists(el, locator, 'Element to blur');
957
960
 
958
- await this.page.locator(elXpath).blur(options);
961
+ await el.blur(options);
959
962
  return this._waitForAction();
960
963
  }
961
964
 
@@ -1052,10 +1055,10 @@ class Playwright extends Helper {
1052
1055
  }
1053
1056
 
1054
1057
  if (locator) {
1055
- const els = await this._locate(locator);
1056
- assertElementExists(els, locator, 'Element');
1057
- await els[0].scrollIntoViewIfNeeded();
1058
- const elementCoordinates = await clickablePoint(els[0]);
1058
+ const el = await this._locateElement(locator);
1059
+ assertElementExists(el, locator, 'Element');
1060
+ await el.scrollIntoViewIfNeeded();
1061
+ const elementCoordinates = await clickablePoint(el);
1059
1062
  await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
1060
1063
  } else {
1061
1064
  await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
@@ -1123,7 +1126,20 @@ class Playwright extends Helper {
1123
1126
  }
1124
1127
 
1125
1128
  /**
1126
- * Find a checkbox by providing human readable text:
1129
+ * Get the first element by different locator types, including strict locator
1130
+ * Should be used in custom helpers:
1131
+ *
1132
+ * ```js
1133
+ * const element = await this.helpers['Playwright']._locateElement({name: 'password'});
1134
+ * ```
1135
+ */
1136
+ async _locateElement(locator) {
1137
+ const context = await this.context || await this._getContext();
1138
+ return findElement(context, locator);
1139
+ }
1140
+
1141
+ /**
1142
+ * Find a checkbox by providing human-readable text:
1127
1143
  * NOTE: Assumes the checkable element exists
1128
1144
  *
1129
1145
  * ```js
@@ -1138,7 +1154,7 @@ class Playwright extends Helper {
1138
1154
  }
1139
1155
 
1140
1156
  /**
1141
- * Find a clickable element by providing human readable text:
1157
+ * Find a clickable element by providing human-readable text:
1142
1158
  *
1143
1159
  * ```js
1144
1160
  * this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -1150,7 +1166,7 @@ class Playwright extends Helper {
1150
1166
  }
1151
1167
 
1152
1168
  /**
1153
- * Find field elements by providing human readable text:
1169
+ * Find field elements by providing human-readable text:
1154
1170
  *
1155
1171
  * ```js
1156
1172
  * this.helpers['Playwright']._locateFields('Your email').then // ...
@@ -1526,13 +1542,8 @@ class Playwright extends Helper {
1526
1542
  const els = await findFields.call(this, field);
1527
1543
  assertElementExists(els, field, 'Field');
1528
1544
  const el = els[0];
1529
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
1530
- const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
1531
- if (tag === 'INPUT' || tag === 'TEXTAREA') {
1532
- await this._evaluateHandeInContext(el => el.value = '', el);
1533
- } else if (editable) {
1534
- await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1535
- }
1545
+
1546
+ await el.clear();
1536
1547
 
1537
1548
  highlightActiveElement.call(this, el, this.page);
1538
1549
 
@@ -1559,21 +1570,16 @@ class Playwright extends Helper {
1559
1570
  * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
1560
1571
  */
1561
1572
  async clearField(locator, options = {}) {
1562
- let result;
1563
- const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
1573
+ const els = await findFields.call(this, locator);
1574
+ assertElementExists(els, locator, 'Field to clear');
1575
+
1576
+ const el = els[0];
1564
1577
 
1565
- if (isNewClearMethodPresent) {
1566
- const els = await findFields.call(this, locator);
1567
- assertElementExists(els, locator, 'Field to clear');
1568
- // TODO: locator change required after #3677 implementation
1569
- const elXpath = await getXPathForElement(els[0]);
1578
+ highlightActiveElement.call(this, el, this.page);
1570
1579
 
1571
- await this.page.locator(elXpath).clear(options);
1572
- result = await this._waitForAction();
1573
- } else {
1574
- result = await this.fillField(locator, '');
1575
- }
1576
- return result;
1580
+ await el.clear();
1581
+
1582
+ return this._waitForAction();
1577
1583
  }
1578
1584
 
1579
1585
  /**
@@ -1627,29 +1633,11 @@ class Playwright extends Helper {
1627
1633
  const els = await findFields.call(this, select);
1628
1634
  assertElementExists(els, select, 'Selectable field');
1629
1635
  const el = els[0];
1630
- if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
1631
- throw new Error('Element is not <select>');
1632
- }
1636
+
1633
1637
  highlightActiveElement.call(this, el, this.page);
1634
1638
  if (!Array.isArray(option)) option = [option];
1635
1639
 
1636
- for (const key in option) {
1637
- const opt = xpathLocator.literal(option[key]);
1638
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
1639
- if (optEl.length) {
1640
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1641
- continue;
1642
- }
1643
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
1644
- if (optEl.length) {
1645
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1646
- }
1647
- }
1648
- await this._evaluateHandeInContext((element) => {
1649
- element.dispatchEvent(new Event('input', { bubbles: true }));
1650
- element.dispatchEvent(new Event('change', { bubbles: true }));
1651
- }, el);
1652
-
1640
+ await el.selectOption(option);
1653
1641
  return this._waitForAction();
1654
1642
  }
1655
1643
 
@@ -1900,7 +1888,7 @@ class Playwright extends Helper {
1900
1888
  const els = await this._locate(locator);
1901
1889
  const texts = [];
1902
1890
  for (const el of els) {
1903
- texts.push(await (await el.getProperty('innerText')).jsonValue());
1891
+ texts.push(await (await el.innerText()));
1904
1892
  }
1905
1893
  this.debug(`Matched ${els.length} elements`);
1906
1894
  return texts;
@@ -1922,7 +1910,7 @@ class Playwright extends Helper {
1922
1910
  async grabValueFromAll(locator) {
1923
1911
  const els = await findFields.call(this, locator);
1924
1912
  this.debug(`Matched ${els.length} elements`);
1925
- return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
1913
+ return Promise.all(els.map(el => el.inputValue()));
1926
1914
  }
1927
1915
 
1928
1916
  /**
@@ -1941,7 +1929,7 @@ class Playwright extends Helper {
1941
1929
  async grabHTMLFromAll(locator) {
1942
1930
  const els = await this._locate(locator);
1943
1931
  this.debug(`Matched ${els.length} elements`);
1944
- return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
1932
+ return Promise.all(els.map(el => el.innerHTML()));
1945
1933
  }
1946
1934
 
1947
1935
  /**
@@ -1962,7 +1950,7 @@ class Playwright extends Helper {
1962
1950
  async grabCssPropertyFromAll(locator, cssProperty) {
1963
1951
  const els = await this._locate(locator);
1964
1952
  this.debug(`Matched ${els.length} elements`);
1965
- const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
1953
+ const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
1966
1954
 
1967
1955
  return cssValues;
1968
1956
  }
@@ -1978,21 +1966,20 @@ class Playwright extends Helper {
1978
1966
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
1979
1967
  const elemAmount = res.length;
1980
1968
  const commands = [];
1981
- res.forEach((el) => {
1982
- Object.keys(cssPropertiesCamelCase).forEach((prop) => {
1983
- commands.push(el.$eval('xpath=.', (el) => {
1984
- const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
1985
- return JSON.parse(JSON.stringify(style));
1986
- }, el)
1987
- .then((props) => {
1988
- if (isColorProperty(prop)) {
1989
- return convertColorToRGBA(props[prop]);
1990
- }
1991
- return props[prop];
1992
- }));
1969
+ let props = [];
1970
+
1971
+ for (const element of res) {
1972
+ const cssProperties = await element.evaluate((el) => getComputedStyle(el));
1973
+
1974
+ Object.keys(cssPropertiesCamelCase).forEach(prop => {
1975
+ if (isColorProperty(prop)) {
1976
+ props.push(convertColorToRGBA(cssProperties[prop]));
1977
+ } else {
1978
+ props.push(cssProperties[prop]);
1979
+ }
1993
1980
  });
1994
- });
1995
- let props = await Promise.all(commands);
1981
+ }
1982
+
1996
1983
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
1997
1984
  if (!Array.isArray(props)) props = [props];
1998
1985
  let chunked = chunkArray(props, values.length);
@@ -2018,7 +2005,7 @@ class Playwright extends Helper {
2018
2005
  res.forEach((el) => {
2019
2006
  Object.keys(attributes).forEach((prop) => {
2020
2007
  commands.push(el
2021
- .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
2008
+ .evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
2022
2009
  });
2023
2010
  });
2024
2011
  let attrs = await Promise.all(commands);
@@ -2039,11 +2026,11 @@ class Playwright extends Helper {
2039
2026
  *
2040
2027
  */
2041
2028
  async dragSlider(locator, offsetX = 0) {
2042
- const src = await this._locate(locator);
2029
+ const src = await this._locateElement(locator);
2043
2030
  assertElementExists(src, locator, 'Slider Element');
2044
2031
 
2045
2032
  // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
2046
- const sliderSource = await clickablePoint(src[0]);
2033
+ const sliderSource = await clickablePoint(src);
2047
2034
 
2048
2035
  // Drag start point
2049
2036
  await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2077,8 +2064,7 @@ class Playwright extends Helper {
2077
2064
  const array = [];
2078
2065
 
2079
2066
  for (let index = 0; index < els.length; index++) {
2080
- const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
2081
- array.push(await a.jsonValue());
2067
+ array.push(await els[index].getAttribute(attr));
2082
2068
  }
2083
2069
 
2084
2070
  return array;
@@ -2091,10 +2077,9 @@ class Playwright extends Helper {
2091
2077
  async saveElementScreenshot(locator, fileName) {
2092
2078
  const outputFile = screenshotOutputFolder(fileName);
2093
2079
 
2094
- const res = await this._locate(locator);
2080
+ const res = await this._locateElement(locator);
2095
2081
  assertElementExists(res, locator);
2096
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
2097
- const elem = res[0];
2082
+ const elem = res;
2098
2083
  this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
2099
2084
  return elem.screenshot({ path: outputFile, type: 'png' });
2100
2085
  }
@@ -2520,16 +2505,26 @@ class Playwright extends Helper {
2520
2505
  }
2521
2506
  return;
2522
2507
  }
2508
+ let contentFrame;
2509
+
2523
2510
  if (!locator) {
2524
- this.context = this.page;
2511
+ this.context = await this.page.frames()[0];
2525
2512
  this.contextLocator = null;
2526
2513
  return;
2527
2514
  }
2528
2515
 
2529
2516
  // iframe by selector
2530
2517
  const els = await this._locate(locator);
2531
- assertElementExists(els, locator);
2532
- const contentFrame = await els[0].contentFrame();
2518
+ // assertElementExists(els, locator);
2519
+
2520
+ // get content of the first iframe
2521
+ if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
2522
+ contentFrame = await this.page.frames()[1];
2523
+ // get content of the iframe using its name
2524
+ } else if (locator.toLowerCase().includes('name=')) {
2525
+ const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
2526
+ contentFrame = await this.page.frame(frameName);
2527
+ }
2533
2528
 
2534
2529
  if (contentFrame) {
2535
2530
  this.context = contentFrame;
@@ -2618,9 +2613,9 @@ class Playwright extends Helper {
2618
2613
  * {{> grabElementBoundingRect }}
2619
2614
  */
2620
2615
  async grabElementBoundingRect(locator, prop) {
2621
- const els = await this._locate(locator);
2622
- assertElementExists(els, locator);
2623
- const rect = await els[0].boundingBox();
2616
+ const el = await this._locateElement(locator);
2617
+ assertElementExists(el, locator);
2618
+ const rect = await el.boundingBox();
2624
2619
  if (prop) return rect[prop];
2625
2620
  return rect;
2626
2621
  }
@@ -2655,6 +2650,330 @@ class Playwright extends Helper {
2655
2650
  async stopMockingRoute(url, handler) {
2656
2651
  return this.browserContext.unroute(...arguments);
2657
2652
  }
2653
+
2654
+ /**
2655
+ * Starts recording of network traffic.
2656
+ * This also resets recorded network requests.
2657
+ *
2658
+ * ```js
2659
+ * I.startRecordingTraffic();
2660
+ * ```
2661
+ *
2662
+ * @return {Promise<void>}
2663
+ */
2664
+ async startRecordingTraffic() {
2665
+ this.flushNetworkTraffics();
2666
+ this.recording = true;
2667
+ this.recordedAtLeastOnce = true;
2668
+
2669
+ this.page.on('requestfinished', async (request) => {
2670
+ const information = {
2671
+ url: request.url(),
2672
+ method: request.method(),
2673
+ requestHeaders: request.headers(),
2674
+ requestPostData: request.postData(),
2675
+ };
2676
+
2677
+ this.debugSection('REQUEST: ', JSON.stringify(information));
2678
+
2679
+ information.requestPostData = JSON.parse(information.requestPostData);
2680
+ this.requests.push(information);
2681
+ return this._waitForAction();
2682
+ });
2683
+ }
2684
+
2685
+ /**
2686
+ * Grab the recording network traffics
2687
+ *
2688
+ * @return { Array<any> }
2689
+ *
2690
+ */
2691
+ grabRecordedNetworkTraffics() {
2692
+ if (!this.recording || !this.recordedAtLeastOnce) {
2693
+ throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
2694
+ }
2695
+ return this.requests;
2696
+ }
2697
+
2698
+ /**
2699
+ * Blocks traffic for URL.
2700
+ *
2701
+ * Examples:
2702
+ *
2703
+ * ```js
2704
+ * I.blockTraffic('http://example.com/css/style.css');
2705
+ * I.blockTraffic('http://example.com/css/*.css');
2706
+ * I.blockTraffic('http://example.com/**');
2707
+ * I.blockTraffic(/\.css$/);
2708
+ * ```
2709
+ *
2710
+ * @param url URL to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
2711
+ */
2712
+ async blockTraffic(url) {
2713
+ this.page.route(url, (route) => {
2714
+ route
2715
+ .abort()
2716
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2717
+ .catch((e) => {});
2718
+ });
2719
+ return this._waitForAction();
2720
+ }
2721
+
2722
+ /**
2723
+ * Mocks traffic for URL(s).
2724
+ * This is a powerful feature to manipulate network traffic. Can be used e.g. to stabilize your tests, speed up your tests or as a last resort to make some test scenarios even possible.
2725
+ *
2726
+ * Examples:
2727
+ *
2728
+ * ```js
2729
+ * I.mockTraffic('/api/users/1', '{ id: 1, name: 'John Doe' }');
2730
+ * I.mockTraffic('/api/users/*', JSON.stringify({ id: 1, name: 'John Doe' }));
2731
+ * I.mockTraffic([/^https://api.example.com/v1/, 'https://api.example.com/v2/**'], 'Internal Server Error', 'text/html');
2732
+ * ```
2733
+ *
2734
+ * @param urls string|Array These are the URL(s) to mock, e.g. "/fooapi/*" or "['/fooapi_1/*', '/barapi_2/*']". Regular expressions are also supported.
2735
+ * @param responseString string The string to return in fake response's body.
2736
+ * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
2737
+ */
2738
+ async mockTraffic(urls, responseString, contentType = 'application/json') {
2739
+ // Required to mock cross-domain requests
2740
+ const headers = { 'access-control-allow-origin': '*' };
2741
+
2742
+ if (typeof urls === 'string') {
2743
+ urls = [urls];
2744
+ }
2745
+
2746
+ urls.forEach((url) => {
2747
+ this.page.route(url, (route) => {
2748
+ if (this.page.isClosed()) {
2749
+ // Sometimes it happens that browser has been closed in the meantime.
2750
+ // In this case we just don't fulfill to prevent error in test scenario.
2751
+ return;
2752
+ }
2753
+ route.fulfill({
2754
+ contentType,
2755
+ headers,
2756
+ body: responseString,
2757
+ });
2758
+ });
2759
+ });
2760
+ return this._waitForAction();
2761
+ }
2762
+
2763
+ /**
2764
+ * Resets all recorded network requests.
2765
+ */
2766
+ flushNetworkTraffics() {
2767
+ this.requests = [];
2768
+ }
2769
+
2770
+ /**
2771
+ * Stops recording of network traffic. Recorded traffic is not flashed.
2772
+ *
2773
+ * ```js
2774
+ * I.stopRecordingTraffic();
2775
+ * ```
2776
+ */
2777
+ stopRecordingTraffic() {
2778
+ this.page.removeAllListeners('request');
2779
+ this.recording = false;
2780
+ }
2781
+
2782
+ /**
2783
+ * Verifies that a certain request is part of network traffic.
2784
+ *
2785
+ * ```js
2786
+ * // checking the request url contains certain query strings
2787
+ * I.amOnPage('https://openai.com/blog/chatgpt');
2788
+ * I.startRecordingTraffic();
2789
+ * await I.seeTraffic({
2790
+ * name: 'sentry event',
2791
+ * url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
2792
+ * parameters: {
2793
+ * width: '1919',
2794
+ * height: '1138',
2795
+ * },
2796
+ * });
2797
+ * ```
2798
+ *
2799
+ * ```js
2800
+ * // checking the request url contains certain post data
2801
+ * I.amOnPage('https://openai.com/blog/chatgpt');
2802
+ * I.startRecordingTraffic();
2803
+ * await I.seeTraffic({
2804
+ * name: 'event',
2805
+ * url: 'https://cloudflareinsights.com/cdn-cgi/rum',
2806
+ * requestPostData: {
2807
+ * st: 2,
2808
+ * },
2809
+ * });
2810
+ * ```
2811
+ *
2812
+ * @param {Object} opts - options when checking the traffic network.
2813
+ * @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
2814
+ * @param {string} opts.url Expected URL of request in network traffic
2815
+ * @param {Object} [opts.parameters] Expected parameters of that request in network traffic
2816
+ * @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
2817
+ * @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
2818
+ * @return { Promise<*> }
2819
+ */
2820
+ async seeTraffic({
2821
+ name, url, parameters, requestPostData, timeout = 10,
2822
+ }) {
2823
+ if (!name) {
2824
+ throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
2825
+ }
2826
+
2827
+ if (!url) {
2828
+ throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
2829
+ }
2830
+
2831
+ if (!this.recording || !this.recordedAtLeastOnce) {
2832
+ throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
2833
+ }
2834
+
2835
+ for (let i = 0; i <= timeout * 2; i++) {
2836
+ const found = this._isInTraffic(url, parameters);
2837
+ if (found) {
2838
+ return true;
2839
+ }
2840
+ await new Promise((done) => setTimeout(done, 1000));
2841
+ }
2842
+
2843
+ // check request post data
2844
+ if (requestPostData && this._isInTraffic(url)) {
2845
+ const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
2846
+
2847
+ assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
2848
+ } else if (parameters && this._isInTraffic(url)) {
2849
+ const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
2850
+
2851
+ assert.fail(
2852
+ `Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
2853
+ + `${advancedTestResults}`,
2854
+ );
2855
+ } else {
2856
+ assert.fail(
2857
+ `Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
2858
+ + `Expected url: ${url}.\n`
2859
+ + `Recorded traffic:\n${this._getTrafficDump()}`,
2860
+ );
2861
+ }
2862
+ }
2863
+
2864
+ /**
2865
+ * Returns full URL of request matching parameter "urlMatch".
2866
+ *
2867
+ * @param {string|RegExp} urlMatch Expected URL of request in network traffic. Can be a string or a regular expression.
2868
+ *
2869
+ * Examples:
2870
+ *
2871
+ * ```js
2872
+ * I.grabTrafficUrl('https://api.example.com/session');
2873
+ * I.grabTrafficUrl(/session.*start/);
2874
+ * ```
2875
+ *
2876
+ * @return {Promise<*>}
2877
+ */
2878
+ grabTrafficUrl(urlMatch) {
2879
+ if (!this.recordedAtLeastOnce) {
2880
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.');
2881
+ }
2882
+
2883
+ for (const i in this.requests) {
2884
+ // eslint-disable-next-line no-prototype-builtins
2885
+ if (this.requests.hasOwnProperty(i)) {
2886
+ const request = this.requests[i];
2887
+
2888
+ if (request.url && request.url.match(new RegExp(urlMatch))) {
2889
+ return request.url;
2890
+ }
2891
+ }
2892
+ }
2893
+
2894
+ assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`);
2895
+ }
2896
+
2897
+ /**
2898
+ * Verifies that a certain request is not part of network traffic.
2899
+ *
2900
+ * Examples:
2901
+ *
2902
+ * ```js
2903
+ * I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
2904
+ * I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
2905
+ * ```
2906
+ *
2907
+ * @param {Object} opts - options when checking the traffic network.
2908
+ * @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
2909
+ * @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
2910
+ *
2911
+ */
2912
+ dontSeeTraffic({ name, url }) {
2913
+ if (!this.recordedAtLeastOnce) {
2914
+ throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
2915
+ }
2916
+
2917
+ if (!name) {
2918
+ throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
2919
+ }
2920
+
2921
+ if (!url) {
2922
+ throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
2923
+ }
2924
+
2925
+ if (this._isInTraffic(url)) {
2926
+ assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
2927
+ }
2928
+ }
2929
+
2930
+ /**
2931
+ * Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
2932
+ *
2933
+ * @param url URL to look for.
2934
+ * @param [parameters] Parameters that this URL needs to contain
2935
+ * @return {boolean} Whether or not URL with parameters is part of network traffic.
2936
+ * @private
2937
+ */
2938
+ _isInTraffic(url, parameters) {
2939
+ let isInTraffic = false;
2940
+ this.requests.forEach((request) => {
2941
+ if (isInTraffic) {
2942
+ return; // We already found traffic. Continue with next request
2943
+ }
2944
+
2945
+ if (!request.url.match(new RegExp(url))) {
2946
+ return; // url not found in this request. continue with next request
2947
+ }
2948
+
2949
+ // URL has matched. Now we check the parameters
2950
+
2951
+ if (parameters) {
2952
+ const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
2953
+ if (advancedReport === true) {
2954
+ isInTraffic = true;
2955
+ }
2956
+ } else {
2957
+ isInTraffic = true;
2958
+ }
2959
+ });
2960
+
2961
+ return isInTraffic;
2962
+ }
2963
+
2964
+ /**
2965
+ * Returns all URLs of all network requests recorded so far during execution of test scenario.
2966
+ *
2967
+ * @return {string} List of URLs recorded as a string, seperaeted by new lines after each URL
2968
+ * @private
2969
+ */
2970
+ _getTrafficDump() {
2971
+ let dumpedTraffic = '';
2972
+ this.requests.forEach((request) => {
2973
+ dumpedTraffic += `${request.method} - ${request.url}\n`;
2974
+ });
2975
+ return dumpedTraffic;
2976
+ }
2658
2977
  }
2659
2978
 
2660
2979
  module.exports = Playwright;
@@ -2667,42 +2986,19 @@ function buildLocatorString(locator) {
2667
2986
  }
2668
2987
  return locator.simplify();
2669
2988
  }
2670
- // TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
2671
- async function getXPathForElement(elementHandle) {
2672
- function calculateIndex(node) {
2673
- let index = 1;
2674
- let sibling = node.previousElementSibling;
2675
- while (sibling) {
2676
- if (sibling.tagName === node.tagName) {
2677
- index++;
2678
- }
2679
- sibling = sibling.previousElementSibling;
2680
- }
2681
- return index;
2682
- }
2683
2989
 
2684
- function generateXPath(node) {
2685
- const segments = [];
2686
- while (node && node.nodeType === Node.ELEMENT_NODE) {
2687
- if (node.hasAttribute('id')) {
2688
- segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
2689
- break;
2690
- } else {
2691
- const index = calculateIndex(node);
2692
- segments.unshift(`${node.localName}[${index}]`);
2693
- node = node.parentNode;
2694
- }
2695
- }
2696
- return `//${segments.join('/')}`;
2697
- }
2990
+ async function findElements(matcher, locator) {
2991
+ if (locator.react) return findReact(matcher, locator);
2992
+ locator = new Locator(locator, 'css');
2698
2993
 
2699
- return elementHandle.evaluate(generateXPath);
2994
+ return matcher.locator(buildLocatorString(locator)).all();
2700
2995
  }
2701
2996
 
2702
- async function findElements(matcher, locator) {
2997
+ async function findElement(matcher, locator) {
2703
2998
  if (locator.react) return findReact(matcher, locator);
2704
2999
  locator = new Locator(locator, 'css');
2705
- return matcher.$$(buildLocatorString(locator));
3000
+
3001
+ return matcher.locator(buildLocatorString(locator));
2706
3002
  }
2707
3003
 
2708
3004
  async function getVisibleElements(elements) {
@@ -2732,7 +3028,6 @@ async function proceedClick(locator, context = null, options = {}) {
2732
3028
  assertElementExists(els, locator, 'Clickable element');
2733
3029
  }
2734
3030
 
2735
- const element = els[0];
2736
3031
  highlightActiveElement.call(this, els[0], this.page);
2737
3032
 
2738
3033
  /*
@@ -2781,22 +3076,22 @@ async function findClickable(matcher, locator) {
2781
3076
  async function proceedSee(assertType, text, context, strict = false) {
2782
3077
  let description;
2783
3078
  let allText;
3079
+
2784
3080
  if (!context) {
2785
3081
  let el = await this.context;
2786
-
2787
3082
  if (el && !el.getProperty) {
2788
3083
  // Fallback to body
2789
- el = await this.context.$('body');
3084
+ el = await this.page.$('body');
2790
3085
  }
2791
3086
 
2792
- allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
3087
+ allText = [await el.innerText()];
2793
3088
  description = 'web application';
2794
3089
  } else {
2795
3090
  const locator = new Locator(context, 'css');
2796
3091
  description = `element ${locator.toString()}`;
2797
3092
  const els = await this._locate(locator);
2798
3093
  assertElementExists(els, locator.toString());
2799
- allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
3094
+ allText = await Promise.all(els.map(el => el.innerText()));
2800
3095
  }
2801
3096
 
2802
3097
  if (strict) {
@@ -2864,15 +3159,15 @@ async function proceedSeeInField(assertType, field, value) {
2864
3159
  const els = await findFields.call(this, field);
2865
3160
  assertElementExists(els, field, 'Field');
2866
3161
  const el = els[0];
2867
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
2868
- const fieldType = await el.getProperty('type').then(el => el.jsonValue());
3162
+ const tag = await el.evaluate(e => e.tagName);
3163
+ const fieldType = await el.getAttribute('type');
2869
3164
 
2870
3165
  const proceedMultiple = async (elements) => {
2871
3166
  const fields = Array.isArray(elements) ? elements : [elements];
2872
3167
 
2873
3168
  const elementValues = [];
2874
3169
  for (const element of fields) {
2875
- elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
3170
+ elementValues.push(await element.inputValue());
2876
3171
  }
2877
3172
 
2878
3173
  if (typeof value === 'boolean') {
@@ -2886,8 +3181,8 @@ async function proceedSeeInField(assertType, field, value) {
2886
3181
  };
2887
3182
 
2888
3183
  if (tag === 'SELECT') {
2889
- if (await el.getProperty('multiple')) {
2890
- const selectedOptions = await el.$$('option:checked');
3184
+ if (await el.getAttribute('multiple')) {
3185
+ const selectedOptions = await el.all('option:checked');
2891
3186
  if (!selectedOptions.length) return null;
2892
3187
 
2893
3188
  const options = await filterFieldsByValue(selectedOptions, value, true);
@@ -2918,7 +3213,7 @@ async function proceedSeeInField(assertType, field, value) {
2918
3213
  async function filterFieldsByValue(elements, value, onlySelected) {
2919
3214
  const matches = [];
2920
3215
  for (const element of elements) {
2921
- const val = await element.getProperty('value').then(el => el.jsonValue());
3216
+ const val = await element.getAttribute('value');
2922
3217
  let isSelected = true;
2923
3218
  if (onlySelected) {
2924
3219
  isSelected = await elementSelected(element);
@@ -2942,12 +3237,12 @@ async function filterFieldsBySelectionState(elements, state) {
2942
3237
  }
2943
3238
 
2944
3239
  async function elementSelected(element) {
2945
- const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
3240
+ const type = await element.getAttribute('type');
2946
3241
 
2947
3242
  if (type === 'checkbox' || type === 'radio') {
2948
3243
  return element.isChecked();
2949
3244
  }
2950
- return element.getProperty('selected').then(el => el.jsonValue());
3245
+ return element.getAttribute('selected');
2951
3246
  }
2952
3247
 
2953
3248
  function isFrameLocator(locator) {
@@ -3152,3 +3447,134 @@ function highlightActiveElement(element, context) {
3152
3447
 
3153
3448
  highlightElement(element, context);
3154
3449
  }
3450
+
3451
+ const createAdvancedTestResults = (url, dataToCheck, requests) => {
3452
+ // Creates advanced test results for a network traffic check.
3453
+ // Advanced test results only applies when expected parameters are set
3454
+ if (!dataToCheck) return '';
3455
+
3456
+ let urlFound = false;
3457
+ let advancedResults;
3458
+ requests.forEach((request) => {
3459
+ // url not found in this request. continue with next request
3460
+ if (urlFound || !request.url.match(new RegExp(url))) return;
3461
+ urlFound = true;
3462
+
3463
+ // Url found. Now we create advanced test report for that URL and show which parameters failed
3464
+ if (!request.requestPostData) {
3465
+ advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
3466
+ } else if (request.requestPostData) {
3467
+ advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
3468
+ }
3469
+ });
3470
+ return advancedResults;
3471
+ };
3472
+
3473
+ const extractQueryObjects = (queryString) => {
3474
+ // Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
3475
+ if (queryString.indexOf('?') === -1) {
3476
+ return [];
3477
+ }
3478
+ const queryObjects = [];
3479
+
3480
+ const queryPart = queryString.split('?')[1];
3481
+
3482
+ const queryParameters = queryPart.split('&');
3483
+
3484
+ queryParameters.forEach((queryParameter) => {
3485
+ const keyValue = queryParameter.split('=');
3486
+ const queryObject = {};
3487
+ // eslint-disable-next-line prefer-destructuring
3488
+ queryObject.name = keyValue[0];
3489
+ queryObject.value = decodeURIComponent(keyValue[1]);
3490
+ queryObjects.push(queryObject);
3491
+ });
3492
+
3493
+ return queryObjects;
3494
+ };
3495
+
3496
+ const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
3497
+ // More advanced check if all request parameters match with the expectations
3498
+ let littleReport = '\nQuery parameters:\n';
3499
+ let success = true;
3500
+
3501
+ for (const expectedKey in advancedExpectedParameterValuePairs) {
3502
+ if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
3503
+ continue;
3504
+ }
3505
+ let parameterFound = false;
3506
+ const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
3507
+
3508
+ for (const queryParameter of queryStringObject) {
3509
+ if (queryParameter.name === expectedKey) {
3510
+ parameterFound = true;
3511
+ if (expectedValue === undefined) {
3512
+ littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
3513
+ } else if (typeof expectedValue === 'object' && expectedValue.base64) {
3514
+ const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
3515
+ if (decodedActualValue === expectedValue.base64) {
3516
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
3517
+ } else {
3518
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
3519
+ success = false;
3520
+ }
3521
+ } else if (queryParameter.value === expectedValue) {
3522
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
3523
+ } else {
3524
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
3525
+ success = false;
3526
+ }
3527
+ }
3528
+ }
3529
+
3530
+ if (parameterFound === false) {
3531
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
3532
+ success = false;
3533
+ }
3534
+ }
3535
+
3536
+ return success ? true : littleReport;
3537
+ };
3538
+
3539
+ const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
3540
+ // More advanced check if all request post data match with the expectations
3541
+ let littleReport = '\nRequest Post Data:\n';
3542
+ let success = true;
3543
+
3544
+ for (const expectedKey in advancedExpectedRequestPostValuePairs) {
3545
+ if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
3546
+ continue;
3547
+ }
3548
+ let keyFound = false;
3549
+ const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
3550
+
3551
+ for (const [key, value] of Object.entries(RequestPostDataObject)) {
3552
+ if (key === expectedKey) {
3553
+ keyFound = true;
3554
+ if (expectedValue === undefined) {
3555
+ littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
3556
+ } else if (typeof expectedValue === 'object' && expectedValue.base64) {
3557
+ const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
3558
+ if (decodedActualValue === expectedValue.base64) {
3559
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
3560
+ } else {
3561
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
3562
+ success = false;
3563
+ }
3564
+ } else if (value === expectedValue) {
3565
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
3566
+ } else {
3567
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
3568
+ success = false;
3569
+ }
3570
+ }
3571
+ }
3572
+
3573
+ if (keyFound === false) {
3574
+ littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
3575
+ success = false;
3576
+ }
3577
+ }
3578
+
3579
+ return success ? true : littleReport;
3580
+ };