codeceptjs 3.5.1 โ†’ 3.5.3

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.
@@ -59,7 +59,7 @@ For WebDriver installation Selenium Server is required ๐Ÿ‘‡
59
59
 
60
60
  ## WebDriver
61
61
 
62
- WebDriver based helpers like WebDriver, Protractor, Selenium WebDriver will require [Selenium Server](https://codecept.io/helpers/WebDriver/#selenium-installation) installed. They will also require ChromeDriver or GeckoDriver to run corresponding browsers.
62
+ WebDriver based helpers like WebDriver will require [Selenium Server](https://codecept.io/helpers/WebDriver/#selenium-installation) installed. They will also require ChromeDriver or GeckoDriver to run corresponding browsers.
63
63
 
64
64
  We recommend to install them manually or use NPM packages:
65
65
 
package/docs/mobile.md CHANGED
@@ -123,9 +123,7 @@ Select [Appium helper](https://codecept.io/helpers/Appium/) when asked.
123
123
  ```sh
124
124
  ? What helpers do you want to use?
125
125
  โ—ฏ WebDriver
126
- โ—ฏ Protractor
127
126
  โ—ฏ Puppeteer
128
- โ—ฏ Nightmare
129
127
  โฏโ—‰ Appium
130
128
  โ—ฏ REST
131
129
  ```
package/docs/plugins.md CHANGED
@@ -1152,8 +1152,6 @@ plugins: {
1152
1152
  }
1153
1153
  ```
1154
1154
 
1155
- Please note, this service can be used with Protractor helper as well!
1156
-
1157
1155
  #### Sauce Service
1158
1156
 
1159
1157
  Install `@wdio/sauce-service` package, as [described here][19].
@@ -31,7 +31,6 @@ TestCafe provides cross-browser support without Selenium. TestCafe tests are fas
31
31
  ---
32
32
 
33
33
  * [Mobile Testing with Appium ยป](/mobile)
34
- * [Testing with Protractor ยป](/angular)
35
34
 
36
35
  :::
37
36
 
package/docs/testcafe.md CHANGED
@@ -5,7 +5,7 @@ title: Testing with TestCafe
5
5
 
6
6
  # Testing with TestCafe
7
7
 
8
- [TestCafe](https://devexpress.github.io/testcafe/) is another alternative engine for driving browsers. It is driven by unique technology which provides fast and simple cross browser testing for desktop and mobile browsers. Unlike WebDriver or Puppeteer, TestCafe doesn't control a browser at all. It is not a browser itself, like [Nightmare](/nightmare) or Cypress. **TestCafe core is a proxy server** that runs behind the scene, and transforms all HTML and JS to include code that is needed for test automation.
8
+ [TestCafe](https://devexpress.github.io/testcafe/) is another alternative engine for driving browsers. It is driven by unique technology which provides fast and simple cross browser testing for desktop and mobile browsers. Unlike WebDriver or Puppeteer, TestCafe doesn't control a browser at all. It is not a browser itself, like Cypress. **TestCafe core is a proxy server** that runs behind the scene, and transforms all HTML and JS to include code that is needed for test automation.
9
9
 
10
10
  ![Testcafe](/img/testcafe.png)
11
11
 
package/docs/webdriver.md CHANGED
@@ -21,8 +21,6 @@ Let's clarify the terms:
21
21
 
22
22
  We use [webdriverio](https://webdriver.io) library to run tests over WebDriver.
23
23
 
24
- > Popular tool [Protractor](/angular) also uses WebDriver for running end 2 end tests.
25
-
26
24
  To proceed you need to have [CodeceptJS installed](/quickstart#using-selenium-webdriver) and `WebDriver` helper selected.
27
25
 
28
26
  Selenium WebDriver may be complicated from start, as it requires following tools to be installed and started.
@@ -20,7 +20,7 @@ const defaultConfig = {
20
20
  include: {},
21
21
  };
22
22
 
23
- const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe', 'Nightmare'];
23
+ const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe'];
24
24
  const translations = Object.keys(require('../../translations'));
25
25
 
26
26
  const noTranslation = 'English (no localization)';
@@ -25,7 +25,9 @@ class OpenAI extends Helper {
25
25
  chunkSize: 80000,
26
26
  };
27
27
  this.options = { ...this.options, ...config };
28
+ }
28
29
 
30
+ _beforeSuite() {
29
31
  const helpers = Container.helpers();
30
32
 
31
33
  for (const helperName of standardActingHelpers) {
@@ -37,16 +39,16 @@ class OpenAI extends Helper {
37
39
  }
38
40
 
39
41
  /**
40
- * Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML.
41
- *
42
- * ```js
43
- * I.askGptOnPage('what does this page do?');
44
- * ```
45
- *
46
- * @async
47
- * @param {string} prompt - The question or prompt to ask the GPT model.
48
- * @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
49
- */
42
+ * Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML.
43
+ *
44
+ * ```js
45
+ * I.askGptOnPage('what does this page do?');
46
+ * ```
47
+ *
48
+ * @async
49
+ * @param {string} prompt - The question or prompt to ask the GPT model.
50
+ * @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
51
+ */
50
52
  async askGptOnPage(prompt) {
51
53
  const html = await this.helper.grabSource();
52
54
 
@@ -120,3 +122,5 @@ class OpenAI extends Helper {
120
122
  return response;
121
123
  }
122
124
  }
125
+
126
+ module.exports = OpenAI;
@@ -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
  }
@@ -1560,7 +1566,7 @@ class Playwright extends Helper {
1560
1566
  */
1561
1567
  async clearField(locator, options = {}) {
1562
1568
  let result;
1563
- const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
1569
+ const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
1564
1570
 
1565
1571
  if (isNewClearMethodPresent) {
1566
1572
  const els = await findFields.call(this, locator);
@@ -2655,6 +2661,330 @@ class Playwright extends Helper {
2655
2661
  async stopMockingRoute(url, handler) {
2656
2662
  return this.browserContext.unroute(...arguments);
2657
2663
  }
2664
+
2665
+ /**
2666
+ * Starts recording of network traffic.
2667
+ * This also resets recorded network requests.
2668
+ *
2669
+ * ```js
2670
+ * I.startRecordingTraffic();
2671
+ * ```
2672
+ *
2673
+ * @return {Promise<void>}
2674
+ */
2675
+ async startRecordingTraffic() {
2676
+ this.flushNetworkTraffics();
2677
+ this.recording = true;
2678
+ this.recordedAtLeastOnce = true;
2679
+
2680
+ this.page.on('requestfinished', async (request) => {
2681
+ const information = {
2682
+ url: request.url(),
2683
+ method: request.method(),
2684
+ requestHeaders: request.headers(),
2685
+ requestPostData: request.postData(),
2686
+ };
2687
+
2688
+ this.debugSection('REQUEST: ', JSON.stringify(information));
2689
+
2690
+ information.requestPostData = JSON.parse(information.requestPostData);
2691
+ this.requests.push(information);
2692
+ return this._waitForAction();
2693
+ });
2694
+ }
2695
+
2696
+ /**
2697
+ * Grab the recording network traffics
2698
+ *
2699
+ * @return { Array<any> }
2700
+ *
2701
+ */
2702
+ grabRecordedNetworkTraffics() {
2703
+ if (!this.recording || !this.recordedAtLeastOnce) {
2704
+ throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
2705
+ }
2706
+ return this.requests;
2707
+ }
2708
+
2709
+ /**
2710
+ * Blocks traffic for URL.
2711
+ *
2712
+ * Examples:
2713
+ *
2714
+ * ```js
2715
+ * I.blockTraffic('http://example.com/css/style.css');
2716
+ * I.blockTraffic('http://example.com/css/*.css');
2717
+ * I.blockTraffic('http://example.com/**');
2718
+ * I.blockTraffic(/\.css$/);
2719
+ * ```
2720
+ *
2721
+ * @param url URL to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
2722
+ */
2723
+ async blockTraffic(url) {
2724
+ this.page.route(url, (route) => {
2725
+ route
2726
+ .abort()
2727
+ // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
2728
+ .catch((e) => {});
2729
+ });
2730
+ return this._waitForAction();
2731
+ }
2732
+
2733
+ /**
2734
+ * Mocks traffic for URL(s).
2735
+ * 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.
2736
+ *
2737
+ * Examples:
2738
+ *
2739
+ * ```js
2740
+ * I.mockTraffic('/api/users/1', '{ id: 1, name: 'John Doe' }');
2741
+ * I.mockTraffic('/api/users/*', JSON.stringify({ id: 1, name: 'John Doe' }));
2742
+ * I.mockTraffic([/^https://api.example.com/v1/, 'https://api.example.com/v2/**'], 'Internal Server Error', 'text/html');
2743
+ * ```
2744
+ *
2745
+ * @param urls string|Array These are the URL(s) to mock, e.g. "/fooapi/*" or "['/fooapi_1/*', '/barapi_2/*']". Regular expressions are also supported.
2746
+ * @param responseString string The string to return in fake response's body.
2747
+ * @param contentType Content type of fake response. If not specified default value 'application/json' is used.
2748
+ */
2749
+ async mockTraffic(urls, responseString, contentType = 'application/json') {
2750
+ // Required to mock cross-domain requests
2751
+ const headers = { 'access-control-allow-origin': '*' };
2752
+
2753
+ if (typeof urls === 'string') {
2754
+ urls = [urls];
2755
+ }
2756
+
2757
+ urls.forEach((url) => {
2758
+ this.page.route(url, (route) => {
2759
+ if (this.page.isClosed()) {
2760
+ // Sometimes it happens that browser has been closed in the meantime.
2761
+ // In this case we just don't fulfill to prevent error in test scenario.
2762
+ return;
2763
+ }
2764
+ route.fulfill({
2765
+ contentType,
2766
+ headers,
2767
+ body: responseString,
2768
+ });
2769
+ });
2770
+ });
2771
+ return this._waitForAction();
2772
+ }
2773
+
2774
+ /**
2775
+ * Resets all recorded network requests.
2776
+ */
2777
+ flushNetworkTraffics() {
2778
+ this.requests = [];
2779
+ }
2780
+
2781
+ /**
2782
+ * Stops recording of network traffic. Recorded traffic is not flashed.
2783
+ *
2784
+ * ```js
2785
+ * I.stopRecordingTraffic();
2786
+ * ```
2787
+ */
2788
+ stopRecordingTraffic() {
2789
+ this.page.removeAllListeners('request');
2790
+ this.recording = false;
2791
+ }
2792
+
2793
+ /**
2794
+ * Verifies that a certain request is part of network traffic.
2795
+ *
2796
+ * ```js
2797
+ * // checking the request url contains certain query strings
2798
+ * I.amOnPage('https://openai.com/blog/chatgpt');
2799
+ * I.startRecordingTraffic();
2800
+ * await I.seeTraffic({
2801
+ * name: 'sentry event',
2802
+ * url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
2803
+ * parameters: {
2804
+ * width: '1919',
2805
+ * height: '1138',
2806
+ * },
2807
+ * });
2808
+ * ```
2809
+ *
2810
+ * ```js
2811
+ * // checking the request url contains certain post data
2812
+ * I.amOnPage('https://openai.com/blog/chatgpt');
2813
+ * I.startRecordingTraffic();
2814
+ * await I.seeTraffic({
2815
+ * name: 'event',
2816
+ * url: 'https://cloudflareinsights.com/cdn-cgi/rum',
2817
+ * requestPostData: {
2818
+ * st: 2,
2819
+ * },
2820
+ * });
2821
+ * ```
2822
+ *
2823
+ * @param {Object} opts - options when checking the traffic network.
2824
+ * @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.
2825
+ * @param {string} opts.url Expected URL of request in network traffic
2826
+ * @param {Object} [opts.parameters] Expected parameters of that request in network traffic
2827
+ * @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
2828
+ * @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
2829
+ * @return { Promise<*> }
2830
+ */
2831
+ async seeTraffic({
2832
+ name, url, parameters, requestPostData, timeout = 10,
2833
+ }) {
2834
+ if (!name) {
2835
+ throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
2836
+ }
2837
+
2838
+ if (!url) {
2839
+ throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
2840
+ }
2841
+
2842
+ if (!this.recording || !this.recordedAtLeastOnce) {
2843
+ throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
2844
+ }
2845
+
2846
+ for (let i = 0; i <= timeout * 2; i++) {
2847
+ const found = this._isInTraffic(url, parameters);
2848
+ if (found) {
2849
+ return true;
2850
+ }
2851
+ await new Promise((done) => setTimeout(done, 1000));
2852
+ }
2853
+
2854
+ // check request post data
2855
+ if (requestPostData && this._isInTraffic(url)) {
2856
+ const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
2857
+
2858
+ assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
2859
+ } else if (parameters && this._isInTraffic(url)) {
2860
+ const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
2861
+
2862
+ assert.fail(
2863
+ `Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
2864
+ + `${advancedTestResults}`,
2865
+ );
2866
+ } else {
2867
+ assert.fail(
2868
+ `Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
2869
+ + `Expected url: ${url}.\n`
2870
+ + `Recorded traffic:\n${this._getTrafficDump()}`,
2871
+ );
2872
+ }
2873
+ }
2874
+
2875
+ /**
2876
+ * Returns full URL of request matching parameter "urlMatch".
2877
+ *
2878
+ * @param {string|RegExp} urlMatch Expected URL of request in network traffic. Can be a string or a regular expression.
2879
+ *
2880
+ * Examples:
2881
+ *
2882
+ * ```js
2883
+ * I.grabTrafficUrl('https://api.example.com/session');
2884
+ * I.grabTrafficUrl(/session.*start/);
2885
+ * ```
2886
+ *
2887
+ * @return {Promise<*>}
2888
+ */
2889
+ grabTrafficUrl(urlMatch) {
2890
+ if (!this.recordedAtLeastOnce) {
2891
+ throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.');
2892
+ }
2893
+
2894
+ for (const i in this.requests) {
2895
+ // eslint-disable-next-line no-prototype-builtins
2896
+ if (this.requests.hasOwnProperty(i)) {
2897
+ const request = this.requests[i];
2898
+
2899
+ if (request.url && request.url.match(new RegExp(urlMatch))) {
2900
+ return request.url;
2901
+ }
2902
+ }
2903
+ }
2904
+
2905
+ assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`);
2906
+ }
2907
+
2908
+ /**
2909
+ * Verifies that a certain request is not part of network traffic.
2910
+ *
2911
+ * Examples:
2912
+ *
2913
+ * ```js
2914
+ * I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
2915
+ * I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
2916
+ * ```
2917
+ *
2918
+ * @param {Object} opts - options when checking the traffic network.
2919
+ * @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.
2920
+ * @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
2921
+ *
2922
+ */
2923
+ dontSeeTraffic({ name, url }) {
2924
+ if (!this.recordedAtLeastOnce) {
2925
+ throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
2926
+ }
2927
+
2928
+ if (!name) {
2929
+ throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
2930
+ }
2931
+
2932
+ if (!url) {
2933
+ throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
2934
+ }
2935
+
2936
+ if (this._isInTraffic(url)) {
2937
+ assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
2938
+ }
2939
+ }
2940
+
2941
+ /**
2942
+ * Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
2943
+ *
2944
+ * @param url URL to look for.
2945
+ * @param [parameters] Parameters that this URL needs to contain
2946
+ * @return {boolean} Whether or not URL with parameters is part of network traffic.
2947
+ * @private
2948
+ */
2949
+ _isInTraffic(url, parameters) {
2950
+ let isInTraffic = false;
2951
+ this.requests.forEach((request) => {
2952
+ if (isInTraffic) {
2953
+ return; // We already found traffic. Continue with next request
2954
+ }
2955
+
2956
+ if (!request.url.match(new RegExp(url))) {
2957
+ return; // url not found in this request. continue with next request
2958
+ }
2959
+
2960
+ // URL has matched. Now we check the parameters
2961
+
2962
+ if (parameters) {
2963
+ const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
2964
+ if (advancedReport === true) {
2965
+ isInTraffic = true;
2966
+ }
2967
+ } else {
2968
+ isInTraffic = true;
2969
+ }
2970
+ });
2971
+
2972
+ return isInTraffic;
2973
+ }
2974
+
2975
+ /**
2976
+ * Returns all URLs of all network requests recorded so far during execution of test scenario.
2977
+ *
2978
+ * @return {string} List of URLs recorded as a string, seperaeted by new lines after each URL
2979
+ * @private
2980
+ */
2981
+ _getTrafficDump() {
2982
+ let dumpedTraffic = '';
2983
+ this.requests.forEach((request) => {
2984
+ dumpedTraffic += `${request.method} - ${request.url}\n`;
2985
+ });
2986
+ return dumpedTraffic;
2987
+ }
2658
2988
  }
2659
2989
 
2660
2990
  module.exports = Playwright;
@@ -3152,3 +3482,134 @@ function highlightActiveElement(element, context) {
3152
3482
 
3153
3483
  highlightElement(element, context);
3154
3484
  }
3485
+
3486
+ const createAdvancedTestResults = (url, dataToCheck, requests) => {
3487
+ // Creates advanced test results for a network traffic check.
3488
+ // Advanced test results only applies when expected parameters are set
3489
+ if (!dataToCheck) return '';
3490
+
3491
+ let urlFound = false;
3492
+ let advancedResults;
3493
+ requests.forEach((request) => {
3494
+ // url not found in this request. continue with next request
3495
+ if (urlFound || !request.url.match(new RegExp(url))) return;
3496
+ urlFound = true;
3497
+
3498
+ // Url found. Now we create advanced test report for that URL and show which parameters failed
3499
+ if (!request.requestPostData) {
3500
+ advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
3501
+ } else if (request.requestPostData) {
3502
+ advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
3503
+ }
3504
+ });
3505
+ return advancedResults;
3506
+ };
3507
+
3508
+ const extractQueryObjects = (queryString) => {
3509
+ // Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
3510
+ if (queryString.indexOf('?') === -1) {
3511
+ return [];
3512
+ }
3513
+ const queryObjects = [];
3514
+
3515
+ const queryPart = queryString.split('?')[1];
3516
+
3517
+ const queryParameters = queryPart.split('&');
3518
+
3519
+ queryParameters.forEach((queryParameter) => {
3520
+ const keyValue = queryParameter.split('=');
3521
+ const queryObject = {};
3522
+ // eslint-disable-next-line prefer-destructuring
3523
+ queryObject.name = keyValue[0];
3524
+ queryObject.value = decodeURIComponent(keyValue[1]);
3525
+ queryObjects.push(queryObject);
3526
+ });
3527
+
3528
+ return queryObjects;
3529
+ };
3530
+
3531
+ const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
3532
+ // More advanced check if all request parameters match with the expectations
3533
+ let littleReport = '\nQuery parameters:\n';
3534
+ let success = true;
3535
+
3536
+ for (const expectedKey in advancedExpectedParameterValuePairs) {
3537
+ if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
3538
+ continue;
3539
+ }
3540
+ let parameterFound = false;
3541
+ const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
3542
+
3543
+ for (const queryParameter of queryStringObject) {
3544
+ if (queryParameter.name === expectedKey) {
3545
+ parameterFound = true;
3546
+ if (expectedValue === undefined) {
3547
+ littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
3548
+ } else if (typeof expectedValue === 'object' && expectedValue.base64) {
3549
+ const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
3550
+ if (decodedActualValue === expectedValue.base64) {
3551
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
3552
+ } else {
3553
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
3554
+ success = false;
3555
+ }
3556
+ } else if (queryParameter.value === expectedValue) {
3557
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
3558
+ } else {
3559
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
3560
+ success = false;
3561
+ }
3562
+ }
3563
+ }
3564
+
3565
+ if (parameterFound === false) {
3566
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
3567
+ success = false;
3568
+ }
3569
+ }
3570
+
3571
+ return success ? true : littleReport;
3572
+ };
3573
+
3574
+ const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
3575
+ // More advanced check if all request post data match with the expectations
3576
+ let littleReport = '\nRequest Post Data:\n';
3577
+ let success = true;
3578
+
3579
+ for (const expectedKey in advancedExpectedRequestPostValuePairs) {
3580
+ if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
3581
+ continue;
3582
+ }
3583
+ let keyFound = false;
3584
+ const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
3585
+
3586
+ for (const [key, value] of Object.entries(RequestPostDataObject)) {
3587
+ if (key === expectedKey) {
3588
+ keyFound = true;
3589
+ if (expectedValue === undefined) {
3590
+ littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
3591
+ } else if (typeof expectedValue === 'object' && expectedValue.base64) {
3592
+ const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
3593
+ if (decodedActualValue === expectedValue.base64) {
3594
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
3595
+ } else {
3596
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
3597
+ success = false;
3598
+ }
3599
+ } else if (value === expectedValue) {
3600
+ littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
3601
+ } else {
3602
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
3603
+ success = false;
3604
+ }
3605
+ }
3606
+ }
3607
+
3608
+ if (keyFound === false) {
3609
+ littleReport += ` โœ– ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
3610
+ success = false;
3611
+ }
3612
+ }
3613
+
3614
+ return success ? true : littleReport;
3615
+ };
@@ -4,8 +4,6 @@ const standardActingHelpers = [
4
4
  'Puppeteer',
5
5
  'Appium',
6
6
  'TestCafe',
7
- 'Protractor',
8
- 'Nightmare',
9
7
  ];
10
8
 
11
9
  module.exports = standardActingHelpers;
@@ -51,7 +51,6 @@ let restartsSession;
51
51
  * }
52
52
  * ```
53
53
  *
54
- * Please note, this service can be used with Protractor helper as well!
55
54
  *
56
55
  * #### Sauce Service
57
56
  *
package/lib/recorder.js CHANGED
@@ -94,7 +94,8 @@ module.exports = {
94
94
  oldPromises = [];
95
95
  tasks = [];
96
96
  this.session.running = false;
97
- this.retries = [];
97
+ // reset this retries makes the retryFailedStep plugin won't work if there is Before/BeforeSuit block due to retries is undefined on Scenario
98
+ // this.retries = [];
98
99
  },
99
100
 
100
101
  /**