codeceptjs 3.4.1 → 3.5.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +11 -9
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +248 -0
  5. package/docs/build/Appium.js +47 -7
  6. package/docs/build/JSONResponse.js +4 -4
  7. package/docs/build/Nightmare.js +3 -1
  8. package/docs/build/OpenAI.js +122 -0
  9. package/docs/build/Playwright.js +234 -54
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +101 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +61 -2
  14. package/docs/build/WebDriver.js +85 -5
  15. package/docs/changelog.md +85 -0
  16. package/docs/helpers/Appium.md +152 -147
  17. package/docs/helpers/JSONResponse.md +4 -4
  18. package/docs/helpers/Nightmare.md +2 -0
  19. package/docs/helpers/OpenAI.md +70 -0
  20. package/docs/helpers/Playwright.md +228 -151
  21. package/docs/helpers/Puppeteer.md +153 -101
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +97 -49
  24. package/docs/helpers/WebDriver.md +159 -107
  25. package/docs/mobile.md +49 -2
  26. package/docs/parallel.md +56 -0
  27. package/docs/plugins.md +87 -33
  28. package/docs/secrets.md +6 -0
  29. package/docs/tutorial.md +2 -2
  30. package/docs/webapi/appendField.mustache +2 -0
  31. package/docs/webapi/blur.mustache +17 -0
  32. package/docs/webapi/focus.mustache +12 -0
  33. package/docs/webapi/type.mustache +3 -0
  34. package/lib/ai.js +171 -0
  35. package/lib/cli.js +10 -2
  36. package/lib/codecept.js +4 -0
  37. package/lib/command/dryRun.js +9 -1
  38. package/lib/command/generate.js +46 -3
  39. package/lib/command/init.js +23 -1
  40. package/lib/command/interactive.js +15 -1
  41. package/lib/command/run-workers.js +2 -1
  42. package/lib/container.js +13 -3
  43. package/lib/event.js +2 -0
  44. package/lib/helper/Appium.js +45 -7
  45. package/lib/helper/JSONResponse.js +4 -4
  46. package/lib/helper/Nightmare.js +1 -1
  47. package/lib/helper/OpenAI.js +122 -0
  48. package/lib/helper/Playwright.js +200 -45
  49. package/lib/helper/Protractor.js +1 -1
  50. package/lib/helper/Puppeteer.js +67 -12
  51. package/lib/helper/REST.js +15 -5
  52. package/lib/helper/TestCafe.js +30 -2
  53. package/lib/helper/WebDriver.js +51 -5
  54. package/lib/helper/scripts/blurElement.js +17 -0
  55. package/lib/helper/scripts/focusElement.js +17 -0
  56. package/lib/helper/scripts/highlightElement.js +20 -0
  57. package/lib/html.js +258 -0
  58. package/lib/interfaces/gherkin.js +8 -0
  59. package/lib/listener/retry.js +2 -1
  60. package/lib/pause.js +73 -17
  61. package/lib/plugin/debugErrors.js +67 -0
  62. package/lib/plugin/fakerTransform.js +4 -6
  63. package/lib/plugin/heal.js +177 -0
  64. package/lib/plugin/screenshotOnFail.js +11 -2
  65. package/lib/recorder.js +11 -8
  66. package/lib/secret.js +5 -4
  67. package/lib/step.js +6 -1
  68. package/lib/ui.js +4 -3
  69. package/lib/utils.js +17 -0
  70. package/lib/workers.js +57 -9
  71. package/package.json +25 -16
  72. package/translations/ja-JP.js +9 -9
  73. package/typings/index.d.ts +43 -9
  74. package/typings/promiseBasedTypes.d.ts +242 -25
  75. package/typings/types.d.ts +260 -35
@@ -346,6 +346,34 @@ class TestCafe extends Helper {
346
346
  return this.t.resizeWindow(width, height).catch(mapError);
347
347
  }
348
348
 
349
+ /**
350
+ * {{> focus }}
351
+ *
352
+ */
353
+ async focus(locator) {
354
+ const els = await this._locate(locator);
355
+ await assertElementExists(els, locator, 'Element to focus');
356
+ const element = await els.nth(0);
357
+
358
+ const focusElement = ClientFunction(() => element().focus(), { boundTestRun: this.t, dependencies: { element } });
359
+
360
+ return focusElement();
361
+ }
362
+
363
+ /**
364
+ * {{> blur }}
365
+ *
366
+ */
367
+ async blur(locator) {
368
+ const els = await this._locate(locator);
369
+ await assertElementExists(els, locator, 'Element to blur');
370
+ const element = await els.nth(0);
371
+
372
+ const blurElement = ClientFunction(() => element().blur(), { boundTestRun: this.t, dependencies: { element } });
373
+
374
+ return blurElement();
375
+ }
376
+
349
377
  /**
350
378
  * {{> click }}
351
379
  *
@@ -410,7 +438,7 @@ class TestCafe extends Helper {
410
438
  const el = await els.nth(0);
411
439
 
412
440
  return this.t
413
- .typeText(el, value, { replace: false })
441
+ .typeText(el, value.toString(), { replace: false })
414
442
  .catch(mapError);
415
443
  }
416
444
 
@@ -792,7 +820,7 @@ class TestCafe extends Helper {
792
820
  /**
793
821
  * {{> executeScript }}
794
822
  *
795
- * If a function returns a Promise It will wait for it resolution.
823
+ * If a function returns a Promise It will wait for its resolution.
796
824
  */
797
825
  async executeScript(fn, ...args) {
798
826
  const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
 
7
7
  const Helper = require('@codeceptjs/helper');
8
+ const crypto = require('crypto');
8
9
  const stringIncludes = require('../assert/include').includes;
9
10
  const { urlEquals, equals } = require('../assert/equal');
10
11
  const { debug } = require('../output');
@@ -27,6 +28,10 @@ const {
27
28
  const ElementNotFound = require('./errors/ElementNotFound');
28
29
  const ConnectionRefused = require('./errors/ConnectionRefused');
29
30
  const Locator = require('../locator');
31
+ const { highlightElement } = require('./scripts/highlightElement');
32
+ const store = require('../store');
33
+ const { focusElement } = require('./scripts/focusElement');
34
+ const { blurElement } = require('./scripts/blurElement');
30
35
 
31
36
  const SHADOW = 'shadow';
32
37
  const webRoot = 'body';
@@ -39,7 +44,7 @@ const webRoot = 'body';
39
44
  * @typedef WebDriverConfig
40
45
  * @type {object}
41
46
  * @prop {string} url - base url of website to be tested.
42
- * @prop {string} browser browser in which to perform testing.
47
+ * @prop {string} browser - Browser in which to perform testing.
43
48
  * @prop {string} [basicAuth] - (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
44
49
  * @prop {string} [host=localhost] - WebDriver host to connect.
45
50
  * @prop {number} [port=4444] - WebDriver port to connect.
@@ -57,6 +62,7 @@ const webRoot = 'body';
57
62
  * @prop {object} [desiredCapabilities] Selenium's [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities).
58
63
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
59
64
  * @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
65
+ * @prop {boolean} [highlightElement] - highlight the interacting elements
60
66
  */
61
67
  const config = {};
62
68
 
@@ -822,7 +828,7 @@ class WebDriver extends Helper {
822
828
  }
823
829
 
824
830
  /**
825
- * Find a checkbox by providing human readable text:
831
+ * Find a checkbox by providing human-readable text:
826
832
  *
827
833
  * ```js
828
834
  * this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ...
@@ -835,7 +841,7 @@ class WebDriver extends Helper {
835
841
  }
836
842
 
837
843
  /**
838
- * Find a clickable element by providing human readable text:
844
+ * Find a clickable element by providing human-readable text:
839
845
  *
840
846
  * ```js
841
847
  * const els = await this.helpers.WebDriver._locateClickable('Next page');
@@ -850,7 +856,7 @@ class WebDriver extends Helper {
850
856
  }
851
857
 
852
858
  /**
853
- * Find field elements by providing human readable text:
859
+ * Find field elements by providing human-readable text:
854
860
  *
855
861
  * ```js
856
862
  * this.helpers['WebDriver']._locateFields('Your email').then // ...
@@ -914,6 +920,7 @@ class WebDriver extends Helper {
914
920
  assertElementExists(res, locator, 'Clickable element');
915
921
  }
916
922
  const elem = usingFirstElement(res);
923
+ highlightActiveElement.call(this, elem);
917
924
  return this.browser[clickMethod](getElementId(elem));
918
925
  }
919
926
 
@@ -932,6 +939,7 @@ class WebDriver extends Helper {
932
939
  assertElementExists(res, locator, 'Clickable element');
933
940
  }
934
941
  const elem = usingFirstElement(res);
942
+ highlightActiveElement.call(this, elem);
935
943
 
936
944
  return this.executeScript((el) => {
937
945
  if (document.activeElement instanceof HTMLElement) {
@@ -959,6 +967,7 @@ class WebDriver extends Helper {
959
967
  }
960
968
 
961
969
  const elem = usingFirstElement(res);
970
+ highlightActiveElement.call(this, elem);
962
971
  return elem.doubleClick();
963
972
  }
964
973
 
@@ -1024,6 +1033,7 @@ class WebDriver extends Helper {
1024
1033
  const res = await findFields.call(this, field);
1025
1034
  assertElementExists(res, field, 'Field');
1026
1035
  const elem = usingFirstElement(res);
1036
+ highlightActiveElement.call(this, elem);
1027
1037
  return elem.setValue(value.toString());
1028
1038
  }
1029
1039
 
@@ -1035,7 +1045,8 @@ class WebDriver extends Helper {
1035
1045
  const res = await findFields.call(this, field);
1036
1046
  assertElementExists(res, field, 'Field');
1037
1047
  const elem = usingFirstElement(res);
1038
- return elem.addValue(value);
1048
+ highlightActiveElement.call(this, elem);
1049
+ return elem.addValue(value.toString());
1039
1050
  }
1040
1051
 
1041
1052
  /**
@@ -1046,6 +1057,7 @@ class WebDriver extends Helper {
1046
1057
  const res = await findFields.call(this, field);
1047
1058
  assertElementExists(res, field, 'Field');
1048
1059
  const elem = usingFirstElement(res);
1060
+ highlightActiveElement.call(this, elem);
1049
1061
  return elem.clearValue(getElementId(elem));
1050
1062
  }
1051
1063
 
@@ -1056,6 +1068,7 @@ class WebDriver extends Helper {
1056
1068
  const res = await findFields.call(this, select);
1057
1069
  assertElementExists(res, select, 'Selectable field');
1058
1070
  const elem = usingFirstElement(res);
1071
+ highlightActiveElement.call(this, elem);
1059
1072
 
1060
1073
  if (!Array.isArray(option)) {
1061
1074
  option = [option];
@@ -1122,6 +1135,7 @@ class WebDriver extends Helper {
1122
1135
  assertElementExists(res, field, 'Checkable');
1123
1136
  const elem = usingFirstElement(res);
1124
1137
  const elementId = getElementId(elem);
1138
+ highlightActiveElement.call(this, elem);
1125
1139
 
1126
1140
  const isSelected = await this.browser.isElementSelected(elementId);
1127
1141
  if (isSelected) return Promise.resolve(true);
@@ -1141,6 +1155,7 @@ class WebDriver extends Helper {
1141
1155
  assertElementExists(res, field, 'Checkable');
1142
1156
  const elem = usingFirstElement(res);
1143
1157
  const elementId = getElementId(elem);
1158
+ highlightActiveElement.call(this, elem);
1144
1159
 
1145
1160
  const isSelected = await this.browser.isElementSelected(elementId);
1146
1161
  if (!isSelected) return Promise.resolve(true);
@@ -1879,6 +1894,7 @@ class WebDriver extends Helper {
1879
1894
  */
1880
1895
  async type(keys, delay = null) {
1881
1896
  if (!Array.isArray(keys)) {
1897
+ keys = keys.toString();
1882
1898
  keys = keys.split('');
1883
1899
  }
1884
1900
  if (delay) {
@@ -1920,6 +1936,30 @@ class WebDriver extends Helper {
1920
1936
  }
1921
1937
  }
1922
1938
 
1939
+ /**
1940
+ * {{> focus }}
1941
+ *
1942
+ */
1943
+ async focus(locator) {
1944
+ const els = await this._locate(locator);
1945
+ assertElementExists(els, locator, 'Element to focus');
1946
+ const el = usingFirstElement(els);
1947
+
1948
+ await focusElement(el, this.browser);
1949
+ }
1950
+
1951
+ /**
1952
+ * {{> blur }}
1953
+ *
1954
+ */
1955
+ async blur(locator) {
1956
+ const els = await this._locate(locator);
1957
+ assertElementExists(els, locator, 'Element to blur');
1958
+ const el = usingFirstElement(els);
1959
+
1960
+ await blurElement(el, this.browser);
1961
+ }
1962
+
1923
1963
  /**
1924
1964
  * {{> dragAndDrop }}
1925
1965
  * Appium: not tested
@@ -2873,6 +2913,12 @@ function isModifierKey(key) {
2873
2913
  return unicodeModifierKeys.includes(key);
2874
2914
  }
2875
2915
 
2916
+ function highlightActiveElement(element) {
2917
+ if (!this.options.enableHighlight && !store.debugMode) return;
2918
+
2919
+ highlightElement(element, this.browser);
2920
+ }
2921
+
2876
2922
  function prepareLocateFn(context) {
2877
2923
  if (!context) return this._locate.bind(this);
2878
2924
  return (l) => {
@@ -0,0 +1,17 @@
1
+ module.exports.blurElement = (element, context) => {
2
+ const clientSideBlurFn = el => {
3
+ el.blur();
4
+ };
5
+
6
+ try {
7
+ // Puppeteer
8
+ context.evaluate(clientSideBlurFn, element);
9
+ } catch (e) {
10
+ // WebDriver
11
+ try {
12
+ context.execute(clientSideBlurFn, element);
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ }
17
+ };
@@ -0,0 +1,17 @@
1
+ module.exports.focusElement = (element, context) => {
2
+ const clientSideFn = el => {
3
+ el.focus();
4
+ };
5
+
6
+ try {
7
+ // Puppeteer
8
+ context.evaluate(clientSideFn, element);
9
+ } catch (e) {
10
+ // WebDriver
11
+ try {
12
+ context.execute(clientSideFn, element);
13
+ } catch (err) {
14
+ // ignore
15
+ }
16
+ }
17
+ };
@@ -0,0 +1,20 @@
1
+ module.exports.highlightElement = (element, context) => {
2
+ const clientSideHighlightFn = el => {
3
+ const style = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)';
4
+ const prevStyle = el.style.boxShadow;
5
+ el.style.boxShadow = style;
6
+ setTimeout(() => el.style.boxShadow = prevStyle, 2000);
7
+ };
8
+
9
+ try {
10
+ // Playwright, Puppeteer
11
+ context.evaluate(clientSideHighlightFn, element);
12
+ } catch (e) {
13
+ // WebDriver
14
+ try {
15
+ context.execute(clientSideHighlightFn, element);
16
+ } catch (err) {
17
+ // ignore
18
+ }
19
+ }
20
+ };
package/lib/html.js ADDED
@@ -0,0 +1,258 @@
1
+ const { parse, serialize } = require('parse5');
2
+ const { minify } = require('html-minifier');
3
+
4
+ function minifyHtml(html) {
5
+ return minify(html, {
6
+ collapseWhitespace: true,
7
+ removeComments: true,
8
+ removeEmptyAttributes: true,
9
+ removeRedundantAttributes: true,
10
+ removeScriptTypeAttributes: true,
11
+ removeStyleLinkTypeAttributes: true,
12
+ collapseBooleanAttributes: true,
13
+ useShortDoctype: true,
14
+ }).toString();
15
+ }
16
+
17
+ const defaultHtmlOpts = {
18
+ interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
19
+ textElements: ['label', 'h1', 'h2'],
20
+ allowedAttrs: ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'role'],
21
+ allowedRoles: ['button', 'checkbox', 'search', 'textbox', 'tab'],
22
+ };
23
+
24
+ function removeNonInteractiveElements(html, opts = {}) {
25
+ opts = { ...defaultHtmlOpts, ...opts };
26
+ const {
27
+ interactiveElements,
28
+ textElements,
29
+ allowedAttrs,
30
+ allowedRoles,
31
+ } = opts;
32
+
33
+ // Parse the HTML into a document tree
34
+ const document = parse(html);
35
+
36
+ const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/;
37
+ // Array to store interactive elements
38
+ const removeElements = ['path', 'script'];
39
+
40
+ function isFilteredOut(node) {
41
+ if (removeElements.includes(node.nodeName)) return true;
42
+ if (node.attrs) {
43
+ if (node.attrs.find(attr => attr.name === 'role' && attr.value === 'tooltip')) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ // Function to check if an element is interactive
49
+ function isInteractive(element) {
50
+ if (element.nodeName === 'input' && element.attrs.find(attr => attr.name === 'type' && attr.value === 'hidden')) return false;
51
+ if (interactiveElements.includes(element.nodeName)) return true;
52
+ if (element.attrs) {
53
+ if (element.attrs.find(attr => attr.name === 'contenteditable')) return true;
54
+ if (element.attrs.find(attr => attr.name === 'tabindex')) return true;
55
+ const role = element.attrs.find(attr => attr.name === 'role');
56
+ if (role && allowedRoles.includes(role.value)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function hasMeaningfulText(node) {
62
+ if (textElements.includes(node.nodeName)) return true;
63
+ return false;
64
+ }
65
+
66
+ function hasInteractiveDescendant(node) {
67
+ if (!node.childNodes) return false;
68
+ let result = false;
69
+
70
+ for (const childNode of node.childNodes) {
71
+ if (isInteractive(childNode) || hasMeaningfulText(childNode)) return true;
72
+ result = result || hasInteractiveDescendant(childNode);
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ // Function to remove non-interactive elements recursively
79
+ function removeNonInteractive(node) {
80
+ if (node.nodeName !== '#document') {
81
+ const parent = node.parentNode;
82
+ const index = parent.childNodes.indexOf(node);
83
+
84
+ if (isFilteredOut(node)) {
85
+ parent.childNodes.splice(index, 1);
86
+ return true;
87
+ }
88
+
89
+ // keep texts for interactive elements
90
+ if ((isInteractive(parent) || hasMeaningfulText(parent)) && node.nodeName === '#text') {
91
+ node.value = node.value.trim().slice(0, 200);
92
+ if (!node.value) return false;
93
+ return true;
94
+ }
95
+
96
+ if (
97
+ // if parent is interactive, we may need child element to match
98
+ !isInteractive(parent)
99
+ && !isInteractive(node)
100
+ && !hasInteractiveDescendant(node)
101
+ && !hasMeaningfulText(node)) {
102
+ parent.childNodes.splice(index, 1);
103
+ return true;
104
+ }
105
+ }
106
+
107
+ if (node.attrs) {
108
+ // Filter and keep allowed attributes, accessibility attributes
109
+ node.attrs = node.attrs.filter(attr => {
110
+ const { name, value } = attr;
111
+ if (name === 'class') {
112
+ // Remove classes containing digits
113
+ attr.value = value.split(' ')
114
+ // remove classes containing digits/
115
+ .filter(className => !/\d/.test(className))
116
+ // remove popular trash classes
117
+ .filter(className => !className.match(trashHtmlClasses))
118
+ // remove classes with : and __ in them
119
+ .filter(className => !className.match(/(:|__)/))
120
+ .join(' ');
121
+ }
122
+
123
+ return allowedAttrs.includes(name);
124
+ });
125
+ }
126
+
127
+ if (node.childNodes) {
128
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
129
+ const childNode = node.childNodes[i];
130
+ removeNonInteractive(childNode);
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+
136
+ // Remove non-interactive elements starting from the root element
137
+ removeNonInteractive(document);
138
+
139
+ // Serialize the modified document tree back to HTML
140
+ const serializedHTML = serialize(document);
141
+
142
+ return serializedHTML;
143
+ }
144
+
145
+ function scanForErrorMessages(html, errorClasses = []) {
146
+ // Parse the HTML into a document tree
147
+ const document = parse(html);
148
+
149
+ // Array to store error messages
150
+ const errorMessages = [];
151
+
152
+ // Function to recursively scan for error classes and messages
153
+ function scanErrors(node) {
154
+ if (node.attrs) {
155
+ const classAttr = node.attrs.find(attr => attr.name === 'class');
156
+ if (classAttr && classAttr.value) {
157
+ const classNameChunks = classAttr.value.split(' ');
158
+ const errorClassFound = errorClasses.some(errorClass => classNameChunks.includes(errorClass));
159
+ if (errorClassFound && node.childNodes) {
160
+ const errorMessage = sanitizeTextContent(node);
161
+ errorMessages.push(errorMessage);
162
+ }
163
+ }
164
+ }
165
+
166
+ if (node.childNodes) {
167
+ for (const childNode of node.childNodes) {
168
+ scanErrors(childNode);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Start scanning for error classes and messages from the root element
174
+ scanErrors(document);
175
+
176
+ return errorMessages;
177
+ }
178
+
179
+ function sanitizeTextContent(node) {
180
+ if (node.nodeName === '#text') {
181
+ return node.value.trim();
182
+ }
183
+
184
+ let sanitizedText = '';
185
+
186
+ if (node.childNodes) {
187
+ for (const childNode of node.childNodes) {
188
+ sanitizedText += sanitizeTextContent(childNode);
189
+ }
190
+ }
191
+
192
+ return sanitizedText;
193
+ }
194
+
195
+ function buildPath(node, path = '') {
196
+ const tag = node.nodeName;
197
+ let attributes = '';
198
+
199
+ if (node.attrs) {
200
+ attributes = node.attrs
201
+ .map(attr => `${attr.name}="${attr.value}"`)
202
+ .join(' ');
203
+ }
204
+
205
+ if (!tag.startsWith('#') && tag !== 'body' && tag !== 'html') {
206
+ path += `<${node.nodeName}${node.attrs ? ` ${attributes}` : ''}>`;
207
+ }
208
+
209
+ if (!node.childNodes) return path;
210
+
211
+ const children = node.childNodes.filter(child => !child.nodeName.startsWith('#'));
212
+
213
+ if (children.length) {
214
+ return buildPath(children[children.length - 1], path);
215
+ }
216
+ return path;
217
+ }
218
+
219
+ function splitByChunks(text, chunkSize) {
220
+ chunkSize -= 20;
221
+ const chunks = [];
222
+ for (let i = 0; i < text.length; i += chunkSize) {
223
+ chunks.push(text.slice(i, i + chunkSize));
224
+ }
225
+
226
+ const regex = /<\s*\w+(?:\s+\w+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]+)))*\s*$/;
227
+
228
+ // append tag to chunk if it was split out
229
+ for (const index in chunks) {
230
+ const nextIndex = parseInt(index, 10) + 1;
231
+ if (!chunks[nextIndex]) break;
232
+
233
+ const currentChunk = chunks[index];
234
+ const nextChunk = chunks[nextIndex];
235
+
236
+ const lastTag = currentChunk.match(regex);
237
+ if (lastTag) {
238
+ chunks[nextIndex] = lastTag[0] + nextChunk;
239
+ }
240
+
241
+ const path = buildPath(parse(currentChunk));
242
+ if (path) {
243
+ chunks[nextIndex] = path + chunks[nextIndex];
244
+ }
245
+
246
+ if (chunks[nextIndex].includes('<html')) continue;
247
+ chunks[nextIndex] = `<html><body>${chunks[nextIndex]}</body></html>`;
248
+ }
249
+
250
+ return chunks.map(chunk => chunk.trim());
251
+ }
252
+
253
+ module.exports = {
254
+ scanForErrorMessages,
255
+ removeNonInteractiveElements,
256
+ splitByChunks,
257
+ minifyHtml,
258
+ };
@@ -1,6 +1,7 @@
1
1
  const Gherkin = require('@cucumber/gherkin');
2
2
  const Messages = require('@cucumber/messages');
3
3
  const { Context, Suite, Test } = require('mocha');
4
+ const debug = require('debug')('codeceptjs:bdd');
4
5
 
5
6
  const { matchStep } = require('./bdd');
6
7
  const event = require('../event');
@@ -39,7 +40,9 @@ module.exports = (text, file) => {
39
40
  for (const step of steps) {
40
41
  const metaStep = new Step.MetaStep(null, step.text);
41
42
  metaStep.actor = step.keyword.trim();
43
+ let helperStep;
42
44
  const setMetaStep = (step) => {
45
+ helperStep = step;
43
46
  if (step.metaStep) {
44
47
  if (step.metaStep === metaStep) {
45
48
  return;
@@ -67,11 +70,15 @@ module.exports = (text, file) => {
67
70
  step.startTime = Date.now();
68
71
  step.match = fn.line;
69
72
  event.emit(event.bddStep.before, step);
73
+ event.emit(event.bddStep.started, metaStep);
70
74
  event.dispatcher.prependListener(event.step.before, setMetaStep);
71
75
  try {
76
+ debug(`Step '${step.text}' started...`);
72
77
  await fn(...fn.params);
78
+ debug('Step passed');
73
79
  step.status = 'passed';
74
80
  } catch (err) {
81
+ debug(`Step failed: ${err?.message}`);
75
82
  step.status = 'failed';
76
83
  step.err = err;
77
84
  throw err;
@@ -79,6 +86,7 @@ module.exports = (text, file) => {
79
86
  step.endTime = Date.now();
80
87
  event.dispatcher.removeListener(event.step.before, setMetaStep);
81
88
  }
89
+ event.emit(event.bddStep.finished, metaStep);
82
90
  event.emit(event.bddStep.after, step);
83
91
  }
84
92
  };
@@ -44,6 +44,7 @@ module.exports = function () {
44
44
  if (!retryConfig) return;
45
45
 
46
46
  if (Number.isInteger(+retryConfig)) {
47
+ if (test.retries() === -1) test.retries(retryConfig);
47
48
  return;
48
49
  }
49
50
 
@@ -59,7 +60,7 @@ module.exports = function () {
59
60
  }
60
61
 
61
62
  if (config.Scenario) {
62
- if (isNotSet(test.retries())) test.retries(config.Scenario);
63
+ if (test.retries() === -1) test.retries(config.Scenario);
63
64
  output.log(`Retries: ${config.Scenario}`);
64
65
  }
65
66
  }