codeceptjs 3.4.1 → 3.5.1-2.beta.7

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 (281) hide show
  1. package/README.md +31 -30
  2. package/bin/codecept.js +1 -1
  3. package/lib/actor.js +6 -3
  4. package/lib/ai.js +180 -0
  5. package/lib/cli.js +13 -3
  6. package/lib/codecept.js +8 -0
  7. package/lib/colorUtils.js +10 -0
  8. package/lib/command/definitions.js +2 -7
  9. package/lib/command/dryRun.js +11 -2
  10. package/lib/command/generate.js +46 -3
  11. package/lib/command/info.js +24 -0
  12. package/lib/command/init.js +64 -6
  13. package/lib/command/interactive.js +15 -1
  14. package/lib/command/run-multiple/collection.js +17 -5
  15. package/lib/command/run-multiple.js +4 -2
  16. package/lib/command/run-workers.js +68 -5
  17. package/lib/command/run.js +7 -0
  18. package/lib/command/workers/runTests.js +39 -0
  19. package/lib/container.js +13 -3
  20. package/lib/data/context.js +14 -6
  21. package/lib/event.js +4 -0
  22. package/lib/helper/ApiDataFactory.js +2 -1
  23. package/lib/helper/Appium.js +116 -29
  24. package/lib/helper/Expect.js +422 -0
  25. package/lib/helper/FileSystem.js +1 -1
  26. package/lib/helper/GraphQL.js +25 -0
  27. package/lib/helper/JSONResponse.js +4 -4
  28. package/lib/helper/Nightmare.js +10 -5
  29. package/lib/helper/OpenAI.js +126 -0
  30. package/lib/helper/Playwright.js +1298 -229
  31. package/lib/helper/Protractor.js +12 -7
  32. package/lib/helper/Puppeteer.js +204 -64
  33. package/lib/helper/REST.js +15 -5
  34. package/lib/helper/TestCafe.js +45 -10
  35. package/lib/helper/WebDriver.js +252 -83
  36. package/lib/helper/errors/ElementNotFound.js +2 -1
  37. package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
  38. package/lib/helper/scripts/blurElement.js +17 -0
  39. package/lib/helper/scripts/focusElement.js +17 -0
  40. package/lib/helper/scripts/highlightElement.js +20 -0
  41. package/lib/html.js +258 -0
  42. package/lib/interfaces/bdd.js +1 -1
  43. package/lib/interfaces/gherkin.js +37 -3
  44. package/lib/interfaces/scenarioConfig.js +1 -0
  45. package/lib/listener/retry.js +2 -1
  46. package/lib/locator.js +17 -4
  47. package/lib/mochaFactory.js +2 -1
  48. package/lib/output.js +1 -1
  49. package/lib/pause.js +78 -19
  50. package/lib/plugin/autoLogin.js +45 -10
  51. package/lib/plugin/debugErrors.js +67 -0
  52. package/lib/plugin/fakerTransform.js +4 -6
  53. package/lib/plugin/heal.js +209 -0
  54. package/lib/plugin/retryFailedStep.js +10 -1
  55. package/lib/plugin/retryTo.js +2 -4
  56. package/lib/plugin/screenshotOnFail.js +11 -2
  57. package/lib/plugin/selenoid.js +6 -1
  58. package/lib/plugin/standardActingHelpers.js +0 -2
  59. package/lib/plugin/stepByStepReport.js +2 -2
  60. package/lib/plugin/tryTo.js +5 -7
  61. package/lib/plugin/wdio.js +0 -1
  62. package/lib/recorder.js +22 -11
  63. package/lib/secret.js +5 -4
  64. package/lib/session.js +1 -1
  65. package/lib/step.js +36 -12
  66. package/lib/ui.js +5 -3
  67. package/lib/utils.js +22 -1
  68. package/lib/workers.js +83 -10
  69. package/package.json +117 -95
  70. package/translations/de-DE.js +5 -0
  71. package/translations/fr-FR.js +14 -1
  72. package/translations/it-IT.js +1 -0
  73. package/translations/ja-JP.js +14 -9
  74. package/translations/pl-PL.js +5 -0
  75. package/translations/pt-BR.js +1 -0
  76. package/translations/ru-RU.js +1 -0
  77. package/translations/zh-CN.js +5 -0
  78. package/translations/zh-TW.js +5 -0
  79. package/typings/index.d.ts +51 -15
  80. package/typings/promiseBasedTypes.d.ts +864 -802
  81. package/typings/types.d.ts +1339 -744
  82. package/CHANGELOG.md +0 -2427
  83. package/docs/advanced.md +0 -351
  84. package/docs/api.md +0 -323
  85. package/docs/basics.md +0 -980
  86. package/docs/bdd.md +0 -535
  87. package/docs/best.md +0 -237
  88. package/docs/books.md +0 -37
  89. package/docs/bootstrap.md +0 -135
  90. package/docs/build/ApiDataFactory.js +0 -409
  91. package/docs/build/Appium.js +0 -1938
  92. package/docs/build/FileSystem.js +0 -228
  93. package/docs/build/GraphQL.js +0 -204
  94. package/docs/build/GraphQLDataFactory.js +0 -309
  95. package/docs/build/JSONResponse.js +0 -338
  96. package/docs/build/Mochawesome.js +0 -71
  97. package/docs/build/Nightmare.js +0 -2145
  98. package/docs/build/Playwright.js +0 -3986
  99. package/docs/build/Polly.js +0 -42
  100. package/docs/build/Protractor.js +0 -2699
  101. package/docs/build/Puppeteer.js +0 -3710
  102. package/docs/build/REST.js +0 -334
  103. package/docs/build/SeleniumWebdriver.js +0 -76
  104. package/docs/build/TestCafe.js +0 -2057
  105. package/docs/build/WebDriver.js +0 -4017
  106. package/docs/changelog.md +0 -2436
  107. package/docs/commands.md +0 -254
  108. package/docs/community-helpers.md +0 -58
  109. package/docs/configuration.md +0 -157
  110. package/docs/continuous-integration.md +0 -22
  111. package/docs/custom-helpers.md +0 -306
  112. package/docs/data.md +0 -375
  113. package/docs/detox.md +0 -235
  114. package/docs/docker.md +0 -137
  115. package/docs/email.md +0 -183
  116. package/docs/examples.md +0 -149
  117. package/docs/helpers/ApiDataFactory.md +0 -266
  118. package/docs/helpers/Appium.md +0 -1312
  119. package/docs/helpers/Detox.md +0 -586
  120. package/docs/helpers/FileSystem.md +0 -152
  121. package/docs/helpers/GraphQL.md +0 -130
  122. package/docs/helpers/GraphQLDataFactory.md +0 -226
  123. package/docs/helpers/JSONResponse.md +0 -254
  124. package/docs/helpers/Mochawesome.md +0 -8
  125. package/docs/helpers/MockRequest.md +0 -377
  126. package/docs/helpers/Nightmare.md +0 -1256
  127. package/docs/helpers/Playwright.md +0 -2208
  128. package/docs/helpers/Polly.md +0 -44
  129. package/docs/helpers/Puppeteer-firefox.md +0 -86
  130. package/docs/helpers/Puppeteer.md +0 -2141
  131. package/docs/helpers/REST.md +0 -217
  132. package/docs/helpers/TestCafe.md +0 -1222
  133. package/docs/helpers/WebDriver.md +0 -2319
  134. package/docs/hooks.md +0 -340
  135. package/docs/index.md +0 -111
  136. package/docs/installation.md +0 -75
  137. package/docs/internal-api.md +0 -265
  138. package/docs/locators.md +0 -331
  139. package/docs/mobile-react-native-locators.md +0 -67
  140. package/docs/mobile.md +0 -297
  141. package/docs/nightmare.md +0 -223
  142. package/docs/pageobjects.md +0 -291
  143. package/docs/parallel.md +0 -232
  144. package/docs/playwright.md +0 -609
  145. package/docs/plugins.md +0 -1171
  146. package/docs/puppeteer.md +0 -316
  147. package/docs/quickstart.md +0 -163
  148. package/docs/react.md +0 -69
  149. package/docs/reports.md +0 -392
  150. package/docs/secrets.md +0 -30
  151. package/docs/shadow.md +0 -68
  152. package/docs/shared/keys.mustache +0 -31
  153. package/docs/shared/react.mustache +0 -1
  154. package/docs/testcafe.md +0 -174
  155. package/docs/translation.md +0 -247
  156. package/docs/tutorial.md +0 -271
  157. package/docs/typescript.md +0 -180
  158. package/docs/ui.md +0 -59
  159. package/docs/videos.md +0 -28
  160. package/docs/visual.md +0 -202
  161. package/docs/vue.md +0 -121
  162. package/docs/webapi/amOnPage.mustache +0 -11
  163. package/docs/webapi/appendField.mustache +0 -9
  164. package/docs/webapi/attachFile.mustache +0 -12
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -10
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -25
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeElement.mustache +0 -8
  177. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  178. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  179. package/docs/webapi/dontSeeInField.mustache +0 -11
  180. package/docs/webapi/dontSeeInSource.mustache +0 -8
  181. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  182. package/docs/webapi/doubleClick.mustache +0 -13
  183. package/docs/webapi/downloadFile.mustache +0 -12
  184. package/docs/webapi/dragAndDrop.mustache +0 -9
  185. package/docs/webapi/dragSlider.mustache +0 -11
  186. package/docs/webapi/executeAsyncScript.mustache +0 -24
  187. package/docs/webapi/executeScript.mustache +0 -26
  188. package/docs/webapi/fillField.mustache +0 -16
  189. package/docs/webapi/forceClick.mustache +0 -28
  190. package/docs/webapi/forceRightClick.mustache +0 -18
  191. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  192. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  193. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  194. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  195. package/docs/webapi/grabCookie.mustache +0 -11
  196. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  197. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  198. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  199. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  200. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  201. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  202. package/docs/webapi/grabGeoLocation.mustache +0 -8
  203. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  204. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  205. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  206. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  207. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  208. package/docs/webapi/grabPopupText.mustache +0 -5
  209. package/docs/webapi/grabSource.mustache +0 -8
  210. package/docs/webapi/grabTextFrom.mustache +0 -10
  211. package/docs/webapi/grabTextFromAll.mustache +0 -9
  212. package/docs/webapi/grabTitle.mustache +0 -8
  213. package/docs/webapi/grabValueFrom.mustache +0 -9
  214. package/docs/webapi/grabValueFromAll.mustache +0 -8
  215. package/docs/webapi/moveCursorTo.mustache +0 -12
  216. package/docs/webapi/openNewTab.mustache +0 -7
  217. package/docs/webapi/pressKey.mustache +0 -12
  218. package/docs/webapi/pressKeyDown.mustache +0 -12
  219. package/docs/webapi/pressKeyUp.mustache +0 -12
  220. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  221. package/docs/webapi/refreshPage.mustache +0 -6
  222. package/docs/webapi/resizeWindow.mustache +0 -6
  223. package/docs/webapi/rightClick.mustache +0 -14
  224. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  225. package/docs/webapi/saveScreenshot.mustache +0 -12
  226. package/docs/webapi/say.mustache +0 -10
  227. package/docs/webapi/scrollIntoView.mustache +0 -11
  228. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  229. package/docs/webapi/scrollPageToTop.mustache +0 -6
  230. package/docs/webapi/scrollTo.mustache +0 -12
  231. package/docs/webapi/see.mustache +0 -11
  232. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  233. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  234. package/docs/webapi/seeCookie.mustache +0 -8
  235. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  236. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  237. package/docs/webapi/seeElement.mustache +0 -8
  238. package/docs/webapi/seeElementInDOM.mustache +0 -8
  239. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  240. package/docs/webapi/seeInField.mustache +0 -12
  241. package/docs/webapi/seeInPopup.mustache +0 -8
  242. package/docs/webapi/seeInSource.mustache +0 -7
  243. package/docs/webapi/seeInTitle.mustache +0 -8
  244. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  245. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  246. package/docs/webapi/seeTextEquals.mustache +0 -9
  247. package/docs/webapi/seeTitleEquals.mustache +0 -8
  248. package/docs/webapi/selectOption.mustache +0 -21
  249. package/docs/webapi/setCookie.mustache +0 -16
  250. package/docs/webapi/setGeoLocation.mustache +0 -12
  251. package/docs/webapi/switchTo.mustache +0 -9
  252. package/docs/webapi/switchToNextTab.mustache +0 -10
  253. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  254. package/docs/webapi/type.mustache +0 -18
  255. package/docs/webapi/uncheckOption.mustache +0 -13
  256. package/docs/webapi/wait.mustache +0 -8
  257. package/docs/webapi/waitForClickable.mustache +0 -11
  258. package/docs/webapi/waitForDetached.mustache +0 -10
  259. package/docs/webapi/waitForElement.mustache +0 -11
  260. package/docs/webapi/waitForEnabled.mustache +0 -6
  261. package/docs/webapi/waitForFunction.mustache +0 -17
  262. package/docs/webapi/waitForInvisible.mustache +0 -10
  263. package/docs/webapi/waitForText.mustache +0 -13
  264. package/docs/webapi/waitForValue.mustache +0 -10
  265. package/docs/webapi/waitForVisible.mustache +0 -10
  266. package/docs/webapi/waitInUrl.mustache +0 -9
  267. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  268. package/docs/webapi/waitToHide.mustache +0 -10
  269. package/docs/webapi/waitUrlEquals.mustache +0 -10
  270. package/docs/webdriver.md +0 -657
  271. package/docs/wiki/Books-&-Posts.md +0 -27
  272. package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
  273. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
  274. package/docs/wiki/Examples.md +0 -139
  275. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
  276. package/docs/wiki/Home.md +0 -16
  277. package/docs/wiki/Release-Process.md +0 -24
  278. package/docs/wiki/Roadmap.md +0 -23
  279. package/docs/wiki/Tests.md +0 -1393
  280. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
  281. package/docs/wiki/Videos.md +0 -19
@@ -5,7 +5,8 @@ const Locator = require('../../locator');
5
5
  */
6
6
  class ElementNotFound {
7
7
  constructor(
8
- locator, prefixMessage = 'Element',
8
+ locator,
9
+ prefixMessage = 'Element',
9
10
  postfixMessage = 'was not found by text|CSS|XPath',
10
11
  ) {
11
12
  if (typeof locator === 'object') {
@@ -0,0 +1,38 @@
1
+ async function findReact(matcher, locator) {
2
+ let _locator = `_react=${locator.react}`;
3
+ let props = '';
4
+
5
+ if (locator.props) {
6
+ props += propBuilder(locator.props);
7
+ _locator += props;
8
+ }
9
+ return matcher.locator(_locator).all();
10
+ }
11
+
12
+ async function findVue(matcher, locator) {
13
+ let _locator = `_vue=${locator.vue}`;
14
+ let props = '';
15
+
16
+ if (locator.props) {
17
+ props += propBuilder(locator.props);
18
+ _locator += props;
19
+ }
20
+ return matcher.locator(_locator).all();
21
+ }
22
+
23
+ function propBuilder(props) {
24
+ let _props = '';
25
+
26
+ for (const [key, value] of Object.entries(props)) {
27
+ if (typeof value === 'object') {
28
+ for (const [k, v] of Object.entries(value)) {
29
+ _props += `[${key}.${k} = "${v}"]`;
30
+ }
31
+ } else {
32
+ _props += `[${key} = "${value}"]`;
33
+ }
34
+ }
35
+ return _props;
36
+ }
37
+
38
+ module.exports = { findReact, findVue };
@@ -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
+ // Puppeteer
11
+ context.evaluate(clientSideHighlightFn, element).catch(err => console.error(err));
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-terser');
3
+
4
+ async 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
+ });
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
+ };
@@ -30,7 +30,7 @@ const parameterTypeRegistry = new ParameterTypeRegistry();
30
30
  const matchStep = (step) => {
31
31
  for (const stepName in steps) {
32
32
  if (stepName.indexOf('/') === 0) {
33
- const regExpArr = stepName.match(new RegExp('^/(.*?)/([gimy]*)$')) || [];
33
+ const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || [];
34
34
  const res = step.match(new RegExp(regExpArr[1], regExpArr[2]));
35
35
  if (res) {
36
36
  const fn = steps[stepName];
@@ -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');
@@ -17,6 +18,11 @@ parser.stopAtFirstError = false;
17
18
 
18
19
  module.exports = (text, file) => {
19
20
  const ast = parser.parse(text);
21
+ let currentLanguage;
22
+
23
+ if (ast.feature) {
24
+ currentLanguage = getTranslation(ast.feature.language);
25
+ }
20
26
 
21
27
  if (!ast.feature) {
22
28
  throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);
@@ -39,7 +45,9 @@ module.exports = (text, file) => {
39
45
  for (const step of steps) {
40
46
  const metaStep = new Step.MetaStep(null, step.text);
41
47
  metaStep.actor = step.keyword.trim();
48
+ let helperStep;
42
49
  const setMetaStep = (step) => {
50
+ helperStep = step;
43
51
  if (step.metaStep) {
44
52
  if (step.metaStep === metaStep) {
45
53
  return;
@@ -67,11 +75,15 @@ module.exports = (text, file) => {
67
75
  step.startTime = Date.now();
68
76
  step.match = fn.line;
69
77
  event.emit(event.bddStep.before, step);
78
+ event.emit(event.bddStep.started, metaStep);
70
79
  event.dispatcher.prependListener(event.step.before, setMetaStep);
71
80
  try {
81
+ debug(`Step '${step.text}' started...`);
72
82
  await fn(...fn.params);
83
+ debug('Step passed');
73
84
  step.status = 'passed';
74
85
  } catch (err) {
86
+ debug(`Step failed: ${err?.message}`);
75
87
  step.status = 'failed';
76
88
  step.err = err;
77
89
  throw err;
@@ -79,6 +91,7 @@ module.exports = (text, file) => {
79
91
  step.endTime = Date.now();
80
92
  event.dispatcher.removeListener(event.step.before, setMetaStep);
81
93
  }
94
+ event.emit(event.bddStep.finished, metaStep);
82
95
  event.emit(event.bddStep.after, step);
83
96
  }
84
97
  };
@@ -88,7 +101,7 @@ module.exports = (text, file) => {
88
101
  suite.beforeEach('Before', scenario.injected(async () => runSteps(child.background.steps), suite, 'before'));
89
102
  continue;
90
103
  }
91
- if (child.scenario && child.scenario.keyword === 'Scenario Outline') {
104
+ if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) {
92
105
  for (const examples of child.scenario.examples) {
93
106
  const fields = examples.tableHeader.cells.map(c => c.value);
94
107
  for (const example of examples.tableBody) {
@@ -106,7 +119,14 @@ module.exports = (text, file) => {
106
119
  });
107
120
  }
108
121
  const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
109
- 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
+
110
130
  const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
111
131
  test.tags = suite.tags.concat(tags);
112
132
  test.file = file;
@@ -133,7 +153,7 @@ function transformTable(table) {
133
153
  let str = '';
134
154
  for (const id in table.rows) {
135
155
  const cells = table.rows[id].cells;
136
- str += cells.map(c => c.value).map(c => c.slice(0, 15).padEnd(15)).join(' | ');
156
+ str += cells.map(c => c.value).map(c => c.padEnd(15)).join(' | ');
137
157
  str += '\n';
138
158
  }
139
159
  return str;
@@ -154,3 +174,17 @@ function addExampleInTable(exampleSteps, placeholders) {
154
174
  }
155
175
  return steps;
156
176
  }
177
+
178
+ function getTranslation(language) {
179
+ const translations = Object.keys(require('../../translations'));
180
+
181
+ for (const availableTranslation of translations) {
182
+ if (!language) {
183
+ break;
184
+ }
185
+
186
+ if (availableTranslation.includes(language)) {
187
+ return require('../../translations')[availableTranslation];
188
+ }
189
+ }
190
+ }
@@ -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
  }
@@ -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
  }
package/lib/locator.js CHANGED
@@ -1,4 +1,4 @@
1
- const cssToXPath = require('css-to-xpath');
1
+ const cssToXPath = require('csstoxpath');
2
2
  const { sprintf } = require('sprintf-js');
3
3
 
4
4
  const { xpathLocator } = require('./utils');
@@ -158,11 +158,12 @@ class Locator {
158
158
  }
159
159
 
160
160
  /**
161
+ * @param {string} [pseudoSelector] CSS to XPath extension pseudo: https://www.npmjs.com/package/csstoxpath?activeTab=explore#extension-pseudos
161
162
  * @returns {string}
162
163
  */
163
- toXPath() {
164
+ toXPath(pseudoSelector = '') {
164
165
  if (this.isXPath()) return this.value;
165
- if (this.isCSS()) return cssToXPath(this.value);
166
+ if (this.isCSS()) return cssToXPath(`${this.value}${pseudoSelector}`);
166
167
 
167
168
  throw new Error('Can\'t be converted to XPath');
168
169
  }
@@ -243,12 +244,24 @@ class Locator {
243
244
  }
244
245
 
245
246
  /**
247
+ * Find an element containing a text
246
248
  * @param {string} text
247
249
  * @returns {Locator}
248
250
  */
249
251
  withText(text) {
250
252
  text = xpathLocator.literal(text);
251
- const xpath = sprintf('%s[%s]', this.toXPath(), `contains(., ${text})`);
253
+ const xpath = this.toXPath(`:text-contains-case(${text})`);
254
+ return new Locator({ xpath });
255
+ }
256
+
257
+ /**
258
+ * Find an element with exact text
259
+ * @param {string} text
260
+ * @returns {Locator}
261
+ */
262
+ withTextEquals(text) {
263
+ text = xpathLocator.literal(text);
264
+ const xpath = this.toXPath(`:text-case(${text})`);
252
265
  return new Locator({ xpath });
253
266
  }
254
267
 
@@ -97,7 +97,8 @@ class MochaFactory {
97
97
  const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter');
98
98
  if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
99
99
  Object.defineProperty(
100
- reporterOptions, 'codeceptjs/lib/cli',
100
+ reporterOptions,
101
+ 'codeceptjs/lib/cli',
101
102
  attributes,
102
103
  );
103
104
  delete reporterOptions['codeceptjs-cli-reporter'];
package/lib/output.js CHANGED
@@ -106,7 +106,7 @@ module.exports = {
106
106
  if (!step) return;
107
107
  // Avoid to print non-gherkin steps, when gherkin is running for --steps mode
108
108
  if (outputLevel === 1) {
109
- if (step.hasBDDAncestor()) {
109
+ if (typeof step === 'object' && step.hasBDDAncestor()) {
110
110
  return;
111
111
  }
112
112
  }