codeceptjs 3.5.9 → 3.5.10

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.
@@ -94,6 +94,7 @@ const pathSeparator = path.sep;
94
94
  * @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
95
95
  * @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
96
96
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
97
+ * @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
97
98
  */
98
99
  const config = {};
99
100
 
@@ -141,6 +142,21 @@ const config = {};
141
142
  * * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
142
143
  * * `keepTraceForPassedTests`: - save trace for passed tests
143
144
  *
145
+ * #### HAR Recording Customization
146
+ *
147
+ * A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded.
148
+ * It contains information about the request and response headers, cookies, content, timings, and more. You can use HAR files to mock network requests in your tests.
149
+ * HAR will be saved to `output/har`. More info could be found here https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har.
150
+ *
151
+ * ```
152
+ * ...
153
+ * recordHar: {
154
+ * mode: 'minimal', // possible values: 'minimal'|'full'.
155
+ * content: 'embed' // possible values: "omit"|"embed"|"attach".
156
+ * }
157
+ * ...
158
+ *```
159
+ *
144
160
  * #### Example #1: Wait for 0 network connections.
145
161
  *
146
162
  * ```js
@@ -346,7 +362,7 @@ class Playwright extends Helper {
346
362
  ignoreLog: ['warning', 'log'],
347
363
  uniqueScreenshotNames: false,
348
364
  manualStart: false,
349
- getPageTimeout: 0,
365
+ getPageTimeout: 30000,
350
366
  waitForNavigation: 'load',
351
367
  restart: false,
352
368
  keepCookies: false,
@@ -455,9 +471,10 @@ class Playwright extends Helper {
455
471
  }
456
472
  }
457
473
 
458
- async _before() {
474
+ async _before(test) {
475
+ this.currentRunningTest = test;
459
476
  recorder.retry({
460
- retries: 5,
477
+ retries: process.env.FAILED_STEP_RETRIES || 3,
461
478
  when: err => {
462
479
  if (!err || typeof (err.message) !== 'string') {
463
480
  return false;
@@ -487,6 +504,15 @@ class Playwright extends Helper {
487
504
  }
488
505
  if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
489
506
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
507
+ if (this.options.recordHar) {
508
+ const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har';
509
+ const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`;
510
+ const dir = path.dirname(fileName);
511
+ if (!fileExists(dir)) fs.mkdirSync(dir);
512
+ this.options.recordHar.path = fileName;
513
+ this.currentRunningTest.artifacts.har = fileName;
514
+ contextOptions.recordHar = this.options.recordHar;
515
+ }
490
516
  if (this.storageState) contextOptions.storageState = this.storageState;
491
517
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
492
518
  if (this.options.locale) contextOptions.locale = this.options.locale;
@@ -834,6 +860,7 @@ class Playwright extends Helper {
834
860
  this.context = null;
835
861
  this.frame = null;
836
862
  popupStore.clear();
863
+ if (this.options.recordHar) await this.browserContext.close();
837
864
  await this.browser.close();
838
865
  }
839
866
 
@@ -1089,6 +1116,33 @@ class Playwright extends Helper {
1089
1116
  return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
1090
1117
  }
1091
1118
 
1119
+ /**
1120
+ * Replaying from HAR
1121
+ *
1122
+ * ```js
1123
+ * // Replay API requests from HAR.
1124
+ * // Either use a matching response from the HAR,
1125
+ * // or abort the request if nothing matches.
1126
+ * I.replayFromHar('./output/har/something.har', { url: "*\/**\/api/v1/fruits" });
1127
+ * I.amOnPage('https://demo.playwright.dev/api-mocking');
1128
+ * I.see('CodeceptJS');
1129
+ * ```
1130
+ *
1131
+ * @param {string} harFilePath Path to recorded HAR file
1132
+ * @param {object} [opts] [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har)
1133
+ *
1134
+ * @returns Promise<void>
1135
+ */
1136
+ async replayFromHar(harFilePath, opts) {
1137
+ const file = path.join(global.codecept_dir, harFilePath);
1138
+
1139
+ if (!fileExists(file)) {
1140
+ throw new Error(`File at ${file} cannot be found on local system`);
1141
+ }
1142
+
1143
+ await this.page.routeFromHAR(harFilePath, opts);
1144
+ }
1145
+
1092
1146
  /**
1093
1147
  * {{> scrollPageToTop }}
1094
1148
  */
@@ -1250,6 +1304,22 @@ class Playwright extends Helper {
1250
1304
  return findFields.call(this, locator);
1251
1305
  }
1252
1306
 
1307
+ /**
1308
+ * {{> grabWebElements }}
1309
+ *
1310
+ */
1311
+ async grabWebElements(locator) {
1312
+ return this._locate(locator);
1313
+ }
1314
+
1315
+ /**
1316
+ * {{> grabWebElement }}
1317
+ *
1318
+ */
1319
+ async grabWebElement(locator) {
1320
+ return this._locateElement(locator);
1321
+ }
1322
+
1253
1323
  /**
1254
1324
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
1255
1325
  *
@@ -1711,8 +1781,15 @@ class Playwright extends Helper {
1711
1781
  const el = els[0];
1712
1782
 
1713
1783
  await highlightActiveElement.call(this, el);
1784
+ let optionToSelect = '';
1785
+
1786
+ try {
1787
+ optionToSelect = await el.locator('option', { hasText: option }).textContent();
1788
+ } catch (e) {
1789
+ optionToSelect = option;
1790
+ }
1714
1791
 
1715
- if (!Array.isArray(option)) option = [option];
1792
+ if (!Array.isArray(option)) option = [optionToSelect];
1716
1793
 
1717
1794
  await el.selectOption(option);
1718
1795
  return this._waitForAction();
@@ -2042,19 +2119,17 @@ class Playwright extends Helper {
2042
2119
 
2043
2120
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
2044
2121
  const elemAmount = res.length;
2045
- const commands = [];
2046
2122
  let props = [];
2047
2123
 
2048
2124
  for (const element of res) {
2049
- const cssProperties = await element.evaluate((el) => getComputedStyle(el));
2050
-
2051
- Object.keys(cssPropertiesCamelCase).forEach(prop => {
2125
+ for (const prop of Object.keys(cssProperties)) {
2126
+ const cssProp = await this.grabCssPropertyFrom(locator, prop);
2052
2127
  if (isColorProperty(prop)) {
2053
- props.push(convertColorToRGBA(cssProperties[prop]));
2128
+ props.push(convertColorToRGBA(cssProp));
2054
2129
  } else {
2055
- props.push(cssProperties[prop]);
2130
+ props.push(cssProp);
2056
2131
  }
2057
- });
2132
+ }
2058
2133
  }
2059
2134
 
2060
2135
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
@@ -2062,7 +2137,8 @@ class Playwright extends Helper {
2062
2137
  let chunked = chunkArray(props, values.length);
2063
2138
  chunked = chunked.filter((val) => {
2064
2139
  for (let i = 0; i < val.length; ++i) {
2065
- if (val[i] !== values[i]) return false;
2140
+ // eslint-disable-next-line eqeqeq
2141
+ if (val[i] != values[i]) return false;
2066
2142
  }
2067
2143
  return true;
2068
2144
  });
@@ -2091,7 +2167,7 @@ class Playwright extends Helper {
2091
2167
  let chunked = chunkArray(attrs, values.length);
2092
2168
  chunked = chunked.filter((val) => {
2093
2169
  for (let i = 0; i < val.length; ++i) {
2094
- if (val[i] !== values[i]) return false;
2170
+ if (!val[i].includes(values[i])) return false;
2095
2171
  }
2096
2172
  return true;
2097
2173
  });
@@ -2262,6 +2338,10 @@ class Playwright extends Helper {
2262
2338
  test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
2263
2339
  }
2264
2340
  }
2341
+
2342
+ if (this.options.recordHar) {
2343
+ test.artifacts.har = this.currentRunningTest.artifacts.har;
2344
+ }
2265
2345
  }
2266
2346
 
2267
2347
  async _passed(test) {
@@ -2289,6 +2369,10 @@ class Playwright extends Helper {
2289
2369
  await this.browserContext.tracing.stop();
2290
2370
  }
2291
2371
  }
2372
+
2373
+ if (this.options.recordHar) {
2374
+ test.artifacts.har = this.currentRunningTest.artifacts.har;
2375
+ }
2292
2376
  }
2293
2377
 
2294
2378
  /**
@@ -2581,7 +2665,7 @@ class Playwright extends Helper {
2581
2665
  const _contextObject = this.frame ? this.frame : contextObject;
2582
2666
  let count = 0;
2583
2667
  do {
2584
- waiter = await _contextObject.locator(`:has-text('${text}')`).first().isVisible();
2668
+ waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
2585
2669
  if (waiter) break;
2586
2670
  await this.wait(1);
2587
2671
  count += 1000;
@@ -297,7 +297,7 @@ class Puppeteer extends Helper {
297
297
  this.sessionPages = {};
298
298
  this.currentRunningTest = test;
299
299
  recorder.retry({
300
- retries: 3,
300
+ retries: process.env.FAILED_STEP_RETRIES || 3,
301
301
  when: err => {
302
302
  if (!err || typeof (err.message) !== 'string') {
303
303
  return false;
@@ -917,6 +917,14 @@ class Puppeteer extends Helper {
917
917
  return findFields.call(this, locator);
918
918
  }
919
919
 
920
+ /**
921
+ * {{> grabWebElements }}
922
+ *
923
+ */
924
+ async grabWebElements(locator) {
925
+ return this._locate(locator);
926
+ }
927
+
920
928
  /**
921
929
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
922
930
  *
@@ -1762,29 +1770,26 @@ class Puppeteer extends Helper {
1762
1770
 
1763
1771
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
1764
1772
  const elemAmount = res.length;
1765
- const commands = [];
1766
- res.forEach((el) => {
1767
- Object.keys(cssPropertiesCamelCase).forEach((prop) => {
1768
- commands.push(el.executionContext()
1769
- .evaluate((el) => {
1770
- const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
1771
- return JSON.parse(JSON.stringify(style));
1772
- }, el)
1773
- .then((props) => {
1774
- if (isColorProperty(prop)) {
1775
- return convertColorToRGBA(props[prop]);
1776
- }
1777
- return props[prop];
1778
- }));
1779
- });
1780
- });
1781
- let props = await Promise.all(commands);
1773
+ let props = [];
1774
+
1775
+ for (const element of res) {
1776
+ for (const prop of Object.keys(cssProperties)) {
1777
+ const cssProp = await this.grabCssPropertyFrom(locator, prop);
1778
+ if (isColorProperty(prop)) {
1779
+ props.push(convertColorToRGBA(cssProp));
1780
+ } else {
1781
+ props.push(cssProp);
1782
+ }
1783
+ }
1784
+ }
1785
+
1782
1786
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
1783
1787
  if (!Array.isArray(props)) props = [props];
1784
1788
  let chunked = chunkArray(props, values.length);
1785
1789
  chunked = chunked.filter((val) => {
1786
1790
  for (let i = 0; i < val.length; ++i) {
1787
- if (val[i] !== values[i]) return false;
1791
+ // eslint-disable-next-line eqeqeq
1792
+ if (val[i] != values[i]) return false;
1788
1793
  }
1789
1794
  return true;
1790
1795
  });
@@ -1815,7 +1820,9 @@ class Puppeteer extends Helper {
1815
1820
  let chunked = chunkArray(attrs, values.length);
1816
1821
  chunked = chunked.filter((val) => {
1817
1822
  for (let i = 0; i < val.length; ++i) {
1818
- if (val[i] !== values[i]) return false;
1823
+ const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
1824
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1825
+ if (!_actual.includes(_expected)) return false;
1819
1826
  }
1820
1827
  return true;
1821
1828
  });
@@ -869,6 +869,14 @@ class WebDriver extends Helper {
869
869
  return findFields.call(this, locator).then(res => res);
870
870
  }
871
871
 
872
+ /**
873
+ * {{> grabWebElements }}
874
+ *
875
+ */
876
+ async grabWebElements(locator) {
877
+ return this._locate(locator);
878
+ }
879
+
872
880
  /**
873
881
  * Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
874
882
  *
@@ -1508,7 +1516,9 @@ class WebDriver extends Helper {
1508
1516
  let chunked = chunkArray(props, values.length);
1509
1517
  chunked = chunked.filter((val) => {
1510
1518
  for (let i = 0; i < val.length; ++i) {
1511
- if (val[i] !== values[i]) return false;
1519
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1520
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1521
+ if (_acutal !== _expected) return false;
1512
1522
  }
1513
1523
  return true;
1514
1524
  });
@@ -1535,7 +1545,9 @@ class WebDriver extends Helper {
1535
1545
  let chunked = chunkArray(attrs, values.length);
1536
1546
  chunked = chunked.filter((val) => {
1537
1547
  for (let i = 0; i < val.length; ++i) {
1538
- if (val[i] !== values[i]) return false;
1548
+ const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1549
+ const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1550
+ if (_acutal !== _expected) return false;
1539
1551
  }
1540
1552
  return true;
1541
1553
  });
package/lib/html.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { parse, serialize } = require('parse5');
2
- const { minify } = require('html-minifier');
2
+ const { minify } = require('html-minifier-terser');
3
3
 
4
- function minifyHtml(html) {
4
+ async function minifyHtml(html) {
5
5
  return minify(html, {
6
6
  collapseWhitespace: true,
7
7
  removeComments: true,
@@ -11,7 +11,7 @@ function minifyHtml(html) {
11
11
  removeStyleLinkTypeAttributes: true,
12
12
  collapseBooleanAttributes: true,
13
13
  useShortDoctype: true,
14
- }).toString();
14
+ });
15
15
  }
16
16
 
17
17
  const defaultHtmlOpts = {
@@ -119,7 +119,14 @@ module.exports = (text, file) => {
119
119
  });
120
120
  }
121
121
  const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
122
- const title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
122
+ let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
123
+
124
+ for (const [key, value] of Object.entries(current)) {
125
+ if (title.includes(`<${key}>`)) {
126
+ title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value);
127
+ }
128
+ }
129
+
123
130
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
124
131
  test.tags = suite.tags.concat(tags);
125
132
  test.file = file;
@@ -35,6 +35,7 @@ class ScenarioConfig {
35
35
  * @returns {this}
36
36
  */
37
37
  retry(retries) {
38
+ if (process.env.SCENARIO_ONLY) retries = -retries;
38
39
  this.test.retries(retries);
39
40
  return this;
40
41
  }
package/lib/locator.js CHANGED
@@ -1,4 +1,4 @@
1
- const cssToXPath = require('css-to-xpath');
1
+ const cssToXPath = require('convert-cssxpath');
2
2
  const { sprintf } = require('sprintf-js');
3
3
 
4
4
  const { xpathLocator } = require('./utils');
@@ -162,7 +162,7 @@ class Locator {
162
162
  */
163
163
  toXPath() {
164
164
  if (this.isXPath()) return this.value;
165
- if (this.isCSS()) return cssToXPath(this.value);
165
+ if (this.isCSS()) return cssToXPath.convert(this.value);
166
166
 
167
167
  throw new Error('Can\'t be converted to XPath');
168
168
  }
package/lib/pause.js CHANGED
@@ -18,8 +18,7 @@ let nextStep;
18
18
  let finish;
19
19
  let next;
20
20
  let registeredVariables = {};
21
- const aiAssistant = new AiAssistant();
22
-
21
+ let aiAssistant;
23
22
  /**
24
23
  * Pauses test execution and starts interactive shell
25
24
  * @param {Object<string, *>} [passedObject]
@@ -45,6 +44,8 @@ function pauseSession(passedObject = {}) {
45
44
  let vars = Object.keys(registeredVariables).join(', ');
46
45
  if (vars) vars = `(vars: ${vars})`;
47
46
 
47
+ aiAssistant = AiAssistant.getInstance();
48
+
48
49
  output.print(colors.yellow(' Interactive shell started'));
49
50
  output.print(colors.yellow(' Use JavaScript syntax to try steps in action'));
50
51
  output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`));
@@ -102,7 +103,9 @@ async function parseInput(cmd) {
102
103
  let isAiCommand = false;
103
104
  let $res;
104
105
  try {
106
+ // eslint-disable-next-line
105
107
  const locate = global.locate; // enable locate in this context
108
+ // eslint-disable-next-line
106
109
  const I = container.support('I');
107
110
  if (cmd.trim().startsWith('=>')) {
108
111
  isCustomCommand = true;
@@ -115,7 +118,7 @@ async function parseInput(cmd) {
115
118
  executeCommand = executeCommand.then(async () => {
116
119
  try {
117
120
  const html = await res;
118
- aiAssistant.setHtmlContext(html);
121
+ await aiAssistant.setHtmlContext(html);
119
122
  } catch (err) {
120
123
  output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
121
124
  return;
@@ -38,12 +38,14 @@ const defaultConfig = {
38
38
  * ```js
39
39
  * // inside a test file
40
40
  * // use login to inject auto-login function
41
+ * Feature('Login');
42
+ *
41
43
  * Before(({ login }) => {
42
44
  * login('user'); // login using user session
43
45
  * });
44
46
  *
45
- * // Alternatively log in for one scenario
46
- * Scenario('log me in', ( {I, login} ) => {
47
+ * // Alternatively log in for one scenario.
48
+ * Scenario('log me in', ( { I, login } ) => {
47
49
  * login('admin');
48
50
  * I.see('I am logged in');
49
51
  * });
@@ -8,6 +8,7 @@ const output = require('../output');
8
8
  const supportedHelpers = require('./standardActingHelpers');
9
9
 
10
10
  const defaultConfig = {
11
+ healTries: 1,
11
12
  healLimit: 2,
12
13
  healSteps: [
13
14
  'click',
@@ -54,11 +55,14 @@ const defaultConfig = {
54
55
  *
55
56
  */
56
57
  module.exports = function (config = {}) {
57
- const aiAssistant = new AiAssistant();
58
+ const aiAssistant = AiAssistant.getInstance();
58
59
 
59
60
  let currentTest = null;
60
61
  let currentStep = null;
61
62
  let healedSteps = 0;
63
+ let caughtError;
64
+ let healTries = 0;
65
+ let isHealing = false;
62
66
 
63
67
  const healSuggestions = [];
64
68
 
@@ -67,20 +71,35 @@ module.exports = function (config = {}) {
67
71
  event.dispatcher.on(event.test.before, (test) => {
68
72
  currentTest = test;
69
73
  healedSteps = 0;
74
+ caughtError = null;
70
75
  });
71
76
 
72
77
  event.dispatcher.on(event.step.started, step => currentStep = step);
73
78
 
74
- event.dispatcher.on(event.step.before, () => {
79
+ event.dispatcher.on(event.step.after, (step) => {
80
+ if (isHealing) return;
75
81
  const store = require('../store');
76
82
  if (store.debugMode) return;
77
-
78
83
  recorder.catchWithoutStop(async (err) => {
79
- if (!aiAssistant.isEnabled) throw err;
84
+ isHealing = true;
85
+ if (caughtError === err) throw err; // avoid double handling
86
+ caughtError = err;
87
+ if (!aiAssistant.isEnabled) {
88
+ output.print(colors.yellow('Heal plugin can\'t operate, AI assistant is disabled. Please set OPENAI_API_KEY env variable to enable it.'));
89
+ throw err;
90
+ }
80
91
  if (!currentStep) throw err;
81
92
  if (!config.healSteps.includes(currentStep.name)) throw err;
82
93
  const test = currentTest;
83
94
 
95
+ if (healTries >= config.healTries) {
96
+ output.print(colors.bold.red(`Healing failed for ${config.healTries} time(s)`));
97
+ output.print('AI couldn\'t identify the correct solution');
98
+ output.print('Probably the entire flow has changed and the test should be updated');
99
+
100
+ throw err;
101
+ }
102
+
84
103
  if (healedSteps >= config.healLimit) {
85
104
  output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
86
105
  output.print('Entire flow can be broken, please check it manually');
@@ -111,9 +130,17 @@ module.exports = function (config = {}) {
111
130
 
112
131
  if (!html) throw err;
113
132
 
114
- aiAssistant.setHtmlContext(html);
133
+ healTries++;
134
+ await aiAssistant.setHtmlContext(html);
115
135
  await tryToHeal(step, err);
116
- recorder.session.restore();
136
+
137
+ recorder.add('close healing session', () => {
138
+ recorder.session.restore('heal');
139
+ recorder.ignoreErr(err);
140
+ });
141
+ await recorder.promise();
142
+
143
+ isHealing = false;
117
144
  });
118
145
  });
119
146
 
@@ -155,6 +182,9 @@ module.exports = function (config = {}) {
155
182
  for (const codeSnippet of codeSnippets) {
156
183
  try {
157
184
  debug('Executing', codeSnippet);
185
+ recorder.catch((e) => {
186
+ console.log(e);
187
+ });
158
188
  await eval(codeSnippet); // eslint-disable-line
159
189
 
160
190
  healSuggestions.push({
@@ -163,14 +193,17 @@ module.exports = function (config = {}) {
163
193
  snippet: codeSnippet,
164
194
  });
165
195
 
166
- output.print(colors.bold.green(' Code healed successfully'));
196
+ recorder.add('healed', () => output.print(colors.bold.green(' Code healed successfully')));
167
197
  healedSteps++;
168
198
  return;
169
199
  } catch (err) {
170
200
  debug('Failed to execute code', err);
201
+ recorder.ignoreErr(err); // healing ded not help
202
+ // recorder.catch(() => output.print(colors.bold.red(' Failed healing code')));
171
203
  }
172
204
  }
173
205
 
174
206
  output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
175
207
  }
208
+ return recorder.promise();
176
209
  };
@@ -1,5 +1,6 @@
1
1
  const event = require('../event');
2
2
  const recorder = require('../recorder');
3
+ const container = require('../container');
3
4
 
4
5
  const defaultConfig = {
5
6
  retries: 3,
@@ -98,6 +99,8 @@ module.exports = (config) => {
98
99
  config.when = when;
99
100
 
100
101
  event.dispatcher.on(event.step.started, (step) => {
102
+ if (container.plugins('tryTo')) return;
103
+
101
104
  // if a step is ignored - return
102
105
  for (const ignored of config.ignoredSteps) {
103
106
  if (step.name === ignored) return;
@@ -114,6 +117,8 @@ module.exports = (config) => {
114
117
 
115
118
  event.dispatcher.on(event.test.before, (test) => {
116
119
  if (test && test.disableRetryFailedStep) return; // disable retry when a test is not active
120
+ // this env var is used to set the retries inside _before() block of helpers
121
+ process.env.FAILED_STEP_RETRIES = config.retries;
117
122
  recorder.retry(config);
118
123
  });
119
124
  };
@@ -89,8 +89,8 @@ module.exports = function (config) {
89
89
  const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output;
90
90
 
91
91
  event.dispatcher.on(event.test.before, (test) => {
92
- const md5hash = crypto.createHash('md5').update(test.file + test.title).digest('hex');
93
- dir = path.join(reportDir, `record_${md5hash}`);
92
+ const sha256hash = crypto.createHash('sha256').update(test.file + test.title).digest('hex');
93
+ dir = path.join(reportDir, `record_${sha256hash}`);
94
94
  mkdirp.sync(dir);
95
95
  stepNum = 0;
96
96
  error = null;
package/lib/recorder.js CHANGED
@@ -11,6 +11,7 @@ let errFn;
11
11
  let queueId = 0;
12
12
  let sessionId = null;
13
13
  let asyncErr = null;
14
+ let ignoredErrs = [];
14
15
 
15
16
  let tasks = [];
16
17
  let oldPromises = [];
@@ -93,6 +94,7 @@ module.exports = {
93
94
  promise = Promise.resolve();
94
95
  oldPromises = [];
95
96
  tasks = [];
97
+ ignoredErrs = [];
96
98
  this.session.running = false;
97
99
  // reset this retries makes the retryFailedStep plugin won't work if there is Before/BeforeSuit block due to retries is undefined on Scenario
98
100
  // this.retries = [];
@@ -226,9 +228,10 @@ module.exports = {
226
228
  * @inner
227
229
  */
228
230
  catch(customErrFn) {
229
- debug(`${currentQueue()}Queued | catch with error handler`);
231
+ const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
232
+ debug(`${currentQueue()}Queued | catch with error handler ${fnDescription || ''}`);
230
233
  return promise = promise.catch((err) => {
231
- log(`${currentQueue()}Error | ${err}`);
234
+ log(`${currentQueue()}Error | ${err} ${fnDescription}...`);
232
235
  if (!(err instanceof Error)) { // strange things may happen
233
236
  err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`); // we should be prepared for them
234
237
  }
@@ -247,15 +250,15 @@ module.exports = {
247
250
  * @inner
248
251
  */
249
252
  catchWithoutStop(customErrFn) {
253
+ const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
250
254
  return promise = promise.catch((err) => {
251
- log(`${currentQueue()}Error | ${err}`);
255
+ if (ignoredErrs.includes(err)) return; // already caught
256
+ log(`${currentQueue()}Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`);
252
257
  if (!(err instanceof Error)) { // strange things may happen
253
258
  err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
254
259
  }
255
260
  if (customErrFn) {
256
261
  return customErrFn(err);
257
- } if (errFn) {
258
- return errFn(err);
259
262
  }
260
263
  });
261
264
  },
@@ -274,6 +277,10 @@ module.exports = {
274
277
  });
275
278
  },
276
279
 
280
+ ignoreErr(err) {
281
+ ignoredErrs.push(err);
282
+ },
283
+
277
284
  /**
278
285
  * @param {*} err
279
286
  * @inner
package/lib/ui.js CHANGED
@@ -168,6 +168,7 @@ module.exports = function (suite) {
168
168
  context.Scenario.only = function (title, opts, fn) {
169
169
  const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`;
170
170
  mocha.grep(new RegExp(reString));
171
+ process.env.SCENARIO_ONLY = true;
171
172
  return addScenario(title, opts, fn);
172
173
  };
173
174