codeceptjs 3.5.9-beta.2 → 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.
package/docs/parallel.md CHANGED
@@ -26,12 +26,124 @@ This command is similar to `run`, however, steps output can't be shown in worker
26
26
 
27
27
  Each worker spins an instance of CodeceptJS, executes a group of tests, and sends back report to the main process.
28
28
 
29
- By default the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to assigne the suites one by one to the workers.
29
+ By default, the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to assign the suites one by one to the workers.
30
30
 
31
31
  ```sh
32
32
  npx codeceptjs run-workers --suites 2
33
33
  ```
34
34
 
35
+ ## Test stats with Parallel Execution by Workers
36
+
37
+ ```js
38
+ const { event } = require('codeceptjs');
39
+
40
+ module.exports = function() {
41
+
42
+ event.dispatcher.on(event.workers.result, function (result) {
43
+
44
+ console.log(result);
45
+
46
+ });
47
+ }
48
+
49
+ // in console log
50
+ FAIL | 7 passed, 1 failed, 1 skipped // 2s
51
+ {
52
+ "tests": {
53
+ "passed": [
54
+ {
55
+ "type": "test",
56
+ "title": "Assert @C3",
57
+ "body": "() => { }",
58
+ "async": 0,
59
+ "sync": true,
60
+ "_timeout": 2000,
61
+ "_slow": 75,
62
+ "_retries": -1,
63
+ "timedOut": false,
64
+ "_currentRetry": 0,
65
+ "pending": false,
66
+ "opts": {},
67
+ "tags": [
68
+ "@C3"
69
+ ],
70
+ "uid": "xe4q1HdqpRrZG5dPe0JG+A",
71
+ "workerIndex": 3,
72
+ "retries": -1,
73
+ "duration": 493,
74
+ "err": null,
75
+ "parent": {
76
+ "title": "My",
77
+ "ctx": {},
78
+ "suites": [],
79
+ "tests": [],
80
+ "root": false,
81
+ "pending": false,
82
+ "_retries": -1,
83
+ "_beforeEach": [],
84
+ "_beforeAll": [],
85
+ "_afterEach": [],
86
+ "_afterAll": [],
87
+ "_timeout": 2000,
88
+ "_slow": 75,
89
+ "_bail": false,
90
+ "_onlyTests": [],
91
+ "_onlySuites": [],
92
+ "delayed": false
93
+ },
94
+ "steps": [
95
+ {
96
+ "actor": "I",
97
+ "name": "amOnPage",
98
+ "status": "success",
99
+ "agrs": [
100
+ "https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"
101
+ ],
102
+ "startedAt": 1698760652610,
103
+ "startTime": 1698760652611,
104
+ "endTime": 1698760653098,
105
+ "finishedAt": 1698760653098,
106
+ "duration": 488
107
+ },
108
+ {
109
+ "actor": "I",
110
+ "name": "grabCurrentUrl",
111
+ "status": "success",
112
+ "agrs": [],
113
+ "startedAt": 1698760653098,
114
+ "startTime": 1698760653098,
115
+ "endTime": 1698760653099,
116
+ "finishedAt": 1698760653099,
117
+ "duration": 1
118
+ }
119
+ ]
120
+ }
121
+ ],
122
+ "failed": [],
123
+ "skipped": []
124
+ }
125
+ }
126
+ ```
127
+
128
+ CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with `run-workers` command so that you could handle the events better in your plugins/helpers
129
+
130
+ ```js
131
+ const { event } = require('codeceptjs');
132
+
133
+ module.exports = function() {
134
+ // this event would trigger the `_publishResultsToTestrail` when running `run-workers` command
135
+ event.dispatcher.on(event.workers.result, async () => {
136
+ await _publishResultsToTestrail();
137
+ });
138
+
139
+ // this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command
140
+ event.dispatcher.on(event.all.result, async () => {
141
+ // when running `run` command, this env var is undefined
142
+ if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail();
143
+ });
144
+ }
145
+ ```
146
+
35
147
  ## Parallel Execution by Workers on Multiple Browsers
36
148
 
37
149
  To run tests in parallel across multiple browsers, modify your `codecept.conf.js` file to configure multiple browsers on which you want to run your tests and your tests will run across multiple browsers.
@@ -236,7 +348,7 @@ customWorkers.on(event.all.result, () => {
236
348
 
237
349
  ### Emitting messages to the parent worker
238
350
 
239
- Child workers can send non test events to the main process. This is useful if you want to pass along information not related to the tests event cycles itself such as `event.test.success`.
351
+ Child workers can send non-test events to the main process. This is useful if you want to pass along information not related to the tests event cycles itself such as `event.test.success`.
240
352
 
241
353
  ```js
242
354
  // inside main process
@@ -0,0 +1,9 @@
1
+ Grab WebElement for given locator
2
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
3
+
4
+ ```js
5
+ const webElement = await I.grabWebElement('#button');
6
+ ```
7
+
8
+ @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
9
+ @returns {Promise<*>} WebElement of being used Web helper
@@ -0,0 +1,9 @@
1
+ Grab WebElements for given locator
2
+ Resumes test execution, so **should be used inside an async function with `await`** operator.
3
+
4
+ ```js
5
+ const webElements = await I.grabWebElements('#button');
6
+ ```
7
+
8
+ @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
9
+ @returns {Promise<*>} WebElement of being used Web helper
package/lib/ai.js CHANGED
@@ -16,6 +16,8 @@ const htmlConfig = {
16
16
  html: {},
17
17
  };
18
18
 
19
+ const aiInstance = null;
20
+
19
21
  class AiAssistant {
20
22
  constructor() {
21
23
  this.config = config.get('ai', defaultConfig);
@@ -26,7 +28,10 @@ class AiAssistant {
26
28
 
27
29
  this.isEnabled = !!process.env.OPENAI_API_KEY;
28
30
 
29
- if (!this.isEnabled) return;
31
+ if (!this.isEnabled) {
32
+ debug('No OpenAI API key provided. AI assistant is disabled.');
33
+ return;
34
+ }
30
35
 
31
36
  const configuration = new Configuration({
32
37
  apiKey: process.env.OPENAI_API_KEY,
@@ -35,13 +40,17 @@ class AiAssistant {
35
40
  this.openai = new OpenAIApi(configuration);
36
41
  }
37
42
 
38
- setHtmlContext(html) {
43
+ static getInstance() {
44
+ return aiInstance || new AiAssistant();
45
+ }
46
+
47
+ async setHtmlContext(html) {
39
48
  let processedHTML = html;
40
49
 
41
50
  if (this.htmlConfig.simplify) {
42
51
  processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
43
52
  }
44
- if (this.htmlConfig.minify) processedHTML = minifyHtml(processedHTML);
53
+ if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
45
54
  if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
46
55
 
47
56
  debug(processedHTML);
package/lib/colorUtils.js CHANGED
@@ -226,15 +226,25 @@ function isColorProperty(prop) {
226
226
  'color',
227
227
  'background',
228
228
  'backgroundColor',
229
+ 'background-color',
229
230
  'borderColor',
231
+ 'border-color',
230
232
  'borderBottomColor',
233
+ 'border-bottom-color',
231
234
  'borderLeftColor',
235
+ 'border-left-color',
232
236
  'borderRightColor',
233
237
  'borderTopColor',
234
238
  'caretColor',
235
239
  'columnRuleColor',
236
240
  'outlineColor',
237
241
  'textDecorationColor',
242
+ 'border-right-color',
243
+ 'border-top-color',
244
+ 'caret-color',
245
+ 'column-rule-color',
246
+ 'outline-color',
247
+ 'text-decoration-color',
238
248
  ].indexOf(prop) > -1;
239
249
  }
240
250
 
@@ -276,7 +276,7 @@ class Appium extends Webdriver {
276
276
  const _convertedCaps = {};
277
277
  for (const [key, value] of Object.entries(capabilities)) {
278
278
  if (!key.startsWith(vendorPrefix.appium)) {
279
- if (key !== 'platformName') {
279
+ if (key !== 'platformName' && key !== 'bstack:options') {
280
280
  _convertedCaps[`${vendorPrefix.appium}:${key}`] = value;
281
281
  } else {
282
282
  _convertedCaps[`${key}`] = value;
@@ -1424,10 +1424,10 @@ class Appium extends Webdriver {
1424
1424
  *
1425
1425
  * @return {Promise<void>}
1426
1426
  *
1427
- * Appium: support only iOS
1427
+ * Appium: support both Android and iOS
1428
1428
  */
1429
1429
  async closeApp() {
1430
- onlyForApps.call(this, 'iOS');
1430
+ onlyForApps.call(this);
1431
1431
  return this.browser.closeApp();
1432
1432
  }
1433
1433
 
@@ -1304,6 +1304,22 @@ class Playwright extends Helper {
1304
1304
  return findFields.call(this, locator);
1305
1305
  }
1306
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
+
1307
1323
  /**
1308
1324
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
1309
1325
  *
@@ -2103,19 +2119,17 @@ class Playwright extends Helper {
2103
2119
 
2104
2120
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
2105
2121
  const elemAmount = res.length;
2106
- const commands = [];
2107
2122
  let props = [];
2108
2123
 
2109
2124
  for (const element of res) {
2110
- const cssProperties = await element.evaluate((el) => getComputedStyle(el));
2111
-
2112
- Object.keys(cssPropertiesCamelCase).forEach(prop => {
2125
+ for (const prop of Object.keys(cssProperties)) {
2126
+ const cssProp = await this.grabCssPropertyFrom(locator, prop);
2113
2127
  if (isColorProperty(prop)) {
2114
- props.push(convertColorToRGBA(cssProperties[prop]));
2128
+ props.push(convertColorToRGBA(cssProp));
2115
2129
  } else {
2116
- props.push(cssProperties[prop]);
2130
+ props.push(cssProp);
2117
2131
  }
2118
- });
2132
+ }
2119
2133
  }
2120
2134
 
2121
2135
  const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
@@ -2123,9 +2137,8 @@ class Playwright extends Helper {
2123
2137
  let chunked = chunkArray(props, values.length);
2124
2138
  chunked = chunked.filter((val) => {
2125
2139
  for (let i = 0; i < val.length; ++i) {
2126
- const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
2127
- const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
2128
- if (_acutal !== _expected) return false;
2140
+ // eslint-disable-next-line eqeqeq
2141
+ if (val[i] != values[i]) return false;
2129
2142
  }
2130
2143
  return true;
2131
2144
  });
@@ -2154,7 +2167,7 @@ class Playwright extends Helper {
2154
2167
  let chunked = chunkArray(attrs, values.length);
2155
2168
  chunked = chunked.filter((val) => {
2156
2169
  for (let i = 0; i < val.length; ++i) {
2157
- if (val[i] !== values[i]) return false;
2170
+ if (!val[i].includes(values[i])) return false;
2158
2171
  }
2159
2172
  return true;
2160
2173
  });
@@ -2652,7 +2665,7 @@ class Playwright extends Helper {
2652
2665
  const _contextObject = this.frame ? this.frame : contextObject;
2653
2666
  let count = 0;
2654
2667
  do {
2655
- waiter = await _contextObject.locator(`:has-text('${text}')`).first().isVisible();
2668
+ waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
2656
2669
  if (waiter) break;
2657
2670
  await this.wait(1);
2658
2671
  count += 1000;
@@ -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,31 +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
- const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1788
- const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1789
- if (_acutal !== _expected) return false;
1791
+ // eslint-disable-next-line eqeqeq
1792
+ if (val[i] != values[i]) return false;
1790
1793
  }
1791
1794
  return true;
1792
1795
  });
@@ -1817,9 +1820,9 @@ class Puppeteer extends Helper {
1817
1820
  let chunked = chunkArray(attrs, values.length);
1818
1821
  chunked = chunked.filter((val) => {
1819
1822
  for (let i = 0; i < val.length; ++i) {
1820
- const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1823
+ const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
1821
1824
  const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1822
- if (_acutal !== _expected) return false;
1825
+ if (!_actual.includes(_expected)) return false;
1823
1826
  }
1824
1827
  return true;
1825
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
  *
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 = {
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;
@@ -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
  };
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