codeceptjs 4.0.2-beta.9 → 4.0.2

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 (326) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +84 -41
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. package/lib/retryCoordinator.js +0 -207
package/lib/html.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { parse, serialize } from 'parse5'
2
2
  import { minify } from 'html-minifier-terser'
3
+ import beautify from 'js-beautify'
4
+
5
+ const { html: html_beautify } = beautify
3
6
 
4
7
  async function minifyHtml(html) {
5
8
  return minify(html, {
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
14
17
  })
15
18
  }
16
19
 
20
+ const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
21
+
22
+ function isTrashClass(className) {
23
+ if (!className) return true
24
+ if (/\d/.test(className)) return true
25
+ if (TRASH_HTML_CLASSES.test(className)) return true
26
+ if (/(:|__)/.test(className)) return true
27
+ return false
28
+ }
29
+
30
+ function filterClassValue(value) {
31
+ return (value || '')
32
+ .split(/\s+/)
33
+ .filter(c => c && !isTrashClass(c))
34
+ .join(' ')
35
+ }
36
+
37
+ const DROP_TAGS = new Set(['style', 'noscript'])
38
+ const DROP_ATTRS = new Set(['style'])
39
+
40
+ function cleanHtml(html) {
41
+ const document = parse(html)
42
+
43
+ function walk(node) {
44
+ if (!node) return false
45
+
46
+ if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
47
+ const parent = node.parentNode
48
+ const idx = parent.childNodes.indexOf(node)
49
+ if (idx >= 0) parent.childNodes.splice(idx, 1)
50
+ return true
51
+ }
52
+
53
+ if (node.attrs) {
54
+ node.attrs = node.attrs.filter(attr => {
55
+ if (DROP_ATTRS.has(attr.name)) return false
56
+ if (attr.name === 'class') {
57
+ attr.value = filterClassValue(attr.value)
58
+ if (!attr.value) return false
59
+ }
60
+ return true
61
+ })
62
+ }
63
+
64
+ if (node.childNodes) {
65
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
66
+ walk(node.childNodes[i])
67
+ }
68
+ }
69
+ return false
70
+ }
71
+
72
+ walk(document)
73
+ return serialize(document)
74
+ }
75
+
17
76
  const defaultHtmlOpts = {
18
77
  interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
19
78
  textElements: ['label', 'h1', 'h2'],
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
28
87
  // Parse the HTML into a document tree
29
88
  const document = parse(html)
30
89
 
31
- const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
32
90
  // Array to store interactive elements
33
91
  const removeElements = ['path', 'script']
34
92
 
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
103
161
  if (node.attrs) {
104
162
  // Filter and keep allowed attributes, accessibility attributes
105
163
  node.attrs = node.attrs.filter(attr => {
106
- const { name, value } = attr
107
- if (name === 'class') {
108
- // Remove classes containing digits
109
- attr.value = value
110
- .split(' ')
111
- // remove classes containing digits/
112
- .filter(className => !/\d/.test(className))
113
- // remove popular trash classes
114
- .filter(className => !className.match(trashHtmlClasses))
115
- // remove classes with : and __ in them
116
- .filter(className => !className.match(/(:|__)/))
117
- .join(' ')
164
+ if (attr.name === 'class') {
165
+ attr.value = filterClassValue(attr.value)
118
166
  }
119
-
120
- return allowedAttrs.includes(name)
167
+ return allowedAttrs.includes(attr.name)
121
168
  })
122
169
  }
123
170
 
@@ -245,4 +292,44 @@ function splitByChunks(text, chunkSize) {
245
292
  return chunks.map(chunk => chunk.trim())
246
293
  }
247
294
 
248
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml }
295
+ function simplifyHtmlElement(html, maxLength = 300) {
296
+ try {
297
+ html = removeNonInteractiveElements(html)
298
+ html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
299
+ } catch (e) {
300
+ // keep raw html if minification fails
301
+ }
302
+ if (html.length > maxLength) {
303
+ html = html.slice(0, maxLength) + '...'
304
+ }
305
+ return html
306
+ }
307
+
308
+ async function formatHtml(html) {
309
+ let processed = html
310
+ try {
311
+ processed = await minifyHtml(processed)
312
+ } catch (e) {
313
+ // keep raw html if minification fails
314
+ }
315
+ try {
316
+ processed = cleanHtml(processed)
317
+ } catch (e) {
318
+ // keep minified html if cleaning fails
319
+ }
320
+ try {
321
+ return html_beautify(processed, {
322
+ indent_size: 2,
323
+ wrap_line_length: 0,
324
+ preserve_newlines: false,
325
+ end_with_newline: false,
326
+ // Force every element onto its own line so line numbers in trace HTML
327
+ // map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
328
+ inline: [],
329
+ })
330
+ } catch (e) {
331
+ return processed
332
+ }
333
+ }
334
+
335
+ export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
package/lib/index.js CHANGED
@@ -23,6 +23,10 @@ import heal from './heal.js'
23
23
  import ai from './ai.js'
24
24
  import Workers from './workers.js'
25
25
  import Secret, { secret } from './secret.js'
26
+ import session from './session.js'
27
+
28
+ const inject = (name) => container.support(name)
29
+ const locate = (query) => locator.build(query)
26
30
 
27
31
  export default {
28
32
  /** @type {typeof CodeceptJS.Codecept} */
@@ -67,7 +71,11 @@ export default {
67
71
  Secret,
68
72
  /** @type {typeof CodeceptJS.secret} */
69
73
  secret,
74
+
75
+ session,
76
+ inject,
77
+ locate,
70
78
  }
71
79
 
72
80
  // Named exports for ESM compatibility
73
- export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
81
+ export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
@@ -2,16 +2,18 @@ import event from '../event.js'
2
2
  import recorder from '../recorder.js'
3
3
  import { deepMerge, deepClone, ucfirst } from '../utils.js'
4
4
  import output from '../output.js'
5
+ import container from '../container.js'
5
6
 
6
7
  /**
7
8
  * Enable Helpers to listen to test events
8
9
  */
10
+ let initialized = false
11
+
9
12
  export default function () {
10
- // Use global flag to prevent duplicate initialization across module re-imports
11
- if (global.__codeceptConfigListenerInitialized) {
13
+ if (initialized) {
12
14
  return
13
15
  }
14
- global.__codeceptConfigListenerInitialized = true
16
+ initialized = true
15
17
 
16
18
  enableDynamicConfigFor('suite')
17
19
  enableDynamicConfigFor('test')
@@ -20,7 +22,7 @@ export default function () {
20
22
  event.dispatcher.on(event[type].before, (context = {}) => {
21
23
  // Get helpers dynamically at runtime, not at initialization time
22
24
  // This ensures we get the actual helper instances, not placeholders
23
- const helpers = global.container.helpers()
25
+ const helpers = container.helpers()
24
26
 
25
27
  function updateHelperConfig(helper, config) {
26
28
  // Guard against undefined or invalid helpers
@@ -1,6 +1,7 @@
1
1
  import figures from 'figures'
2
2
  import event from '../event.js'
3
3
  import output from '../output.js'
4
+ import container from '../container.js'
4
5
  import { searchWithFusejs } from '../utils.js'
5
6
 
6
7
  export default function () {
@@ -12,7 +13,7 @@ export default function () {
12
13
 
13
14
  event.dispatcher.on(event.all.result, () => {
14
15
  if (isEmptyRun) {
15
- const mocha = global.container.mocha()
16
+ const mocha = container.mocha()
16
17
 
17
18
  if (mocha.options.grep) {
18
19
  output.print()
@@ -5,16 +5,27 @@ import { isNotSet } from '../utils.js'
5
5
 
6
6
  const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
7
 
8
+ const RETRY_PRIORITIES = {
9
+ MANUAL_STEP: 100,
10
+ STEP_PLUGIN: 50,
11
+ SCENARIO_CONFIG: 30,
12
+ FEATURE_CONFIG: 20,
13
+ HOOK_CONFIG: 10,
14
+ }
15
+
8
16
  export default function () {
9
17
  event.dispatcher.on(event.suite.before, suite => {
10
18
  let retryConfig = Config.get('retry')
11
19
  if (!retryConfig) return
12
20
 
13
21
  if (Number.isInteger(+retryConfig)) {
14
- // is number
15
22
  const retryNum = +retryConfig
16
23
  output.log(`Retries: ${retryNum}`)
17
- suite.retries(retryNum)
24
+
25
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
26
+ suite.retries(retryNum)
27
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
28
+ }
18
29
  return
19
30
  }
20
31
 
@@ -30,11 +41,18 @@ export default function () {
30
41
  hooks
31
42
  .filter(hook => !!config[hook])
32
43
  .forEach(hook => {
33
- if (isNotSet(suite.opts[`retry${hook}`])) suite.opts[`retry${hook}`] = config[hook]
44
+ const retryKey = `retry${hook}`
45
+ if (isNotSet(suite.opts[retryKey])) {
46
+ suite.opts[retryKey] = config[hook]
47
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
48
+ }
34
49
  })
35
50
 
36
51
  if (config.Feature) {
37
- if (isNotSet(suite.retries())) suite.retries(config.Feature)
52
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
53
+ suite.retries(config.Feature)
54
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
55
+ }
38
56
  }
39
57
 
40
58
  output.log(`Retries: ${JSON.stringify(config)}`)
@@ -46,7 +64,10 @@ export default function () {
46
64
  if (!retryConfig) return
47
65
 
48
66
  if (Number.isInteger(+retryConfig)) {
49
- if (test.retries() === -1) test.retries(retryConfig)
67
+ if (test.retries() === -1) {
68
+ test.retries(retryConfig)
69
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
70
+ }
50
71
  return
51
72
  }
52
73
 
@@ -62,9 +83,14 @@ export default function () {
62
83
  }
63
84
 
64
85
  if (config.Scenario) {
65
- if (test.retries() === -1) test.retries(config.Scenario)
86
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
87
+ test.retries(config.Scenario)
88
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
89
+ }
66
90
  output.log(`Retries: ${config.Scenario}`)
67
91
  }
68
92
  }
69
93
  })
70
94
  }
95
+
96
+ export { RETRY_PRIORITIES }
@@ -3,11 +3,12 @@ import event from '../event.js'
3
3
  import recorder from '../recorder.js'
4
4
  import store from '../store.js'
5
5
  import output from '../output.js'
6
+ import container from '../container.js'
6
7
  /**
7
8
  * Enable Helpers to listen to test events
8
9
  */
9
10
  export default function () {
10
- const helpers = global.container.helpers()
11
+ const helpers = container.helpers()
11
12
 
12
13
  const runHelpersHook = (hook, param) => {
13
14
  if (store.dryRun) return
@@ -29,11 +30,13 @@ export default function () {
29
30
  event.dispatcher.on(event.suite.before, suite => {
30
31
  // if (suite.parent) return; // only for root suite
31
32
  runAsyncHelpersHook('_beforeSuite', suite, true)
33
+ recorder.catch()
32
34
  })
33
35
 
34
36
  event.dispatcher.on(event.suite.after, suite => {
35
37
  // if (suite.parent) return; // only for root suite
36
38
  runAsyncHelpersHook('_afterSuite', suite, true)
39
+ recorder.catch()
37
40
  })
38
41
 
39
42
  event.dispatcher.on(event.test.started, test => {
@@ -1,10 +1,11 @@
1
1
  import event from '../event.js'
2
+ import container from '../container.js'
2
3
 
3
4
  export default function () {
4
5
  let mocha
5
6
 
6
7
  event.dispatcher.on(event.all.before, () => {
7
- mocha = global.container.mocha()
8
+ mocha = container.mocha()
8
9
  })
9
10
 
10
11
  event.dispatcher.on(event.test.passed, test => {
@@ -0,0 +1,43 @@
1
+ import event from '../event.js'
2
+ import recorder from '../recorder.js'
3
+ import store from '../store.js'
4
+ import container from '../container.js'
5
+ import { resetBeforeCalledSet, getBeforeCalledSet } from '../container.js'
6
+
7
+ export default function () {
8
+ const runAsyncSupportHook = (hook, param, force) => {
9
+ if (store.dryRun) return
10
+ const support = container.supportObjects()
11
+ Object.keys(support).forEach(key => {
12
+ if (key === 'I') return
13
+ const obj = support[key]
14
+ if (!obj || typeof obj !== 'object' || !obj[hook]) return
15
+ recorder.add(`pageobject ${key}.${hook}()`, () => obj[hook](param), force, false)
16
+ })
17
+ }
18
+
19
+ event.dispatcher.on(event.test.started, () => {
20
+ resetBeforeCalledSet()
21
+ })
22
+
23
+ event.dispatcher.on(event.test.after, () => {
24
+ if (store.dryRun) return
25
+ const support = container.supportObjects()
26
+ const called = getBeforeCalledSet()
27
+ called.forEach(name => {
28
+ const obj = support[name]
29
+ if (obj && obj._after) {
30
+ recorder.add(`pageobject ${name}._after()`, () => obj._after(), true, false)
31
+ }
32
+ })
33
+ recorder.catchWithoutStop(() => {})
34
+ })
35
+
36
+ event.dispatcher.on(event.suite.after, suite => {
37
+ runAsyncSupportHook('_afterSuite', suite, true)
38
+ })
39
+
40
+ event.dispatcher.on(event.suite.before, suite => {
41
+ runAsyncSupportHook('_beforeSuite', suite, true)
42
+ })
43
+ }
@@ -1,11 +1,12 @@
1
1
  import event from '../event.js'
2
+ import container from '../container.js'
2
3
 
3
4
  export default function () {
4
5
  event.dispatcher.on(event.hook.failed, err => {
5
- global.container.result().addStats({ failedHooks: 1 })
6
+ container.result().addStats({ failedHooks: 1 })
6
7
  })
7
8
 
8
9
  event.dispatcher.on(event.test.before, test => {
9
- global.container.result().addTest(test)
10
+ container.result().addTest(test)
10
11
  })
11
12
  }
package/lib/locator.js CHANGED
@@ -40,6 +40,11 @@ class Locator {
40
40
  return
41
41
  }
42
42
 
43
+ // Try to parse JSON strings that look like objects
44
+ if (this.parsedJsonAsString(locator)) {
45
+ return
46
+ }
47
+
43
48
  this.type = defaultType || 'fuzzy'
44
49
  this.output = locator
45
50
  this.value = locator
@@ -53,9 +58,6 @@ class Locator {
53
58
  if (isShadow(locator)) {
54
59
  this.type = 'shadow'
55
60
  }
56
- if (isPlaywrightLocator(locator)) {
57
- this.type = 'pw'
58
- }
59
61
 
60
62
  Locator.filters.forEach(f => f(locator, this))
61
63
  }
@@ -89,6 +91,33 @@ class Locator {
89
91
  return { [this.type]: this.value }
90
92
  }
91
93
 
94
+ parsedJsonAsString(locator) {
95
+ if (typeof locator !== 'string') {
96
+ return false
97
+ }
98
+
99
+ const trimmed = locator.trim()
100
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
101
+ return false
102
+ }
103
+
104
+ try {
105
+ const parsed = JSON.parse(trimmed)
106
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
107
+ this.locator = parsed
108
+ this.type = Object.keys(parsed)[0]
109
+ this.value = parsed[this.type]
110
+ this.strict = true
111
+
112
+ Locator.filters.forEach(f => f(parsed, this))
113
+ return true
114
+ }
115
+ } catch (e) {
116
+ // continue with normal string processing
117
+ }
118
+ return false
119
+ }
120
+
92
121
  /**
93
122
  * @returns {string}
94
123
  */
@@ -349,9 +378,121 @@ class Locator {
349
378
  return new Locator({ xpath })
350
379
  }
351
380
 
381
+ /**
382
+ * Find an element with all of the provided CSS classes (word-exact match).
383
+ * Accepts variadic class names; all must be present.
384
+ *
385
+ * Example:
386
+ * locate('button').withClass('btn-primary', 'btn-lg')
387
+ *
388
+ * @param {...string} classes
389
+ * @returns {Locator}
390
+ */
391
+ withClass(...classes) {
392
+ if (!classes.length) return this
393
+ const predicates = classes.map(c => `contains(concat(' ', normalize-space(@class), ' '), ' ${c} ')`)
394
+ const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
395
+ return new Locator({ xpath })
396
+ }
397
+
398
+ /**
399
+ * Find an element with none of the provided CSS classes.
400
+ *
401
+ * Example:
402
+ * locate('tr').withoutClass('deleted')
403
+ *
404
+ * @param {...string} classes
405
+ * @returns {Locator}
406
+ */
407
+ withoutClass(...classes) {
408
+ if (!classes.length) return this
409
+ const predicates = classes.map(c => `not(contains(concat(' ', normalize-space(@class), ' '), ' ${c} '))`)
410
+ const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
411
+ return new Locator({ xpath })
412
+ }
413
+
414
+ /**
415
+ * Find an element that does NOT contain the provided text.
416
+ * @param {string} text
417
+ * @returns {Locator}
418
+ */
419
+ withoutText(text) {
420
+ text = xpathLocator.literal(text)
421
+ const xpath = sprintf('%s[%s]', this.toXPath(), `not(contains(., ${text}))`)
422
+ return new Locator({ xpath })
423
+ }
424
+
425
+ /**
426
+ * Find an element that does NOT have any of the provided attribute/value pairs.
427
+ * @param {Object.<string, string>} attributes
428
+ * @returns {Locator}
429
+ */
430
+ withoutAttr(attributes) {
431
+ const operands = []
432
+ for (const attr of Object.keys(attributes)) {
433
+ operands.push(`not(@${attr} = ${xpathLocator.literal(attributes[attr])})`)
434
+ }
435
+ const xpath = sprintf('%s[%s]', this.toXPath(), operands.join(' and '))
436
+ return new Locator({ xpath })
437
+ }
438
+
439
+ /**
440
+ * Find an element that has no direct child matching the provided locator.
441
+ * @param {CodeceptJS.LocatorOrString} locator
442
+ * @returns {Locator}
443
+ */
444
+ withoutChild(locator) {
445
+ const xpath = sprintf('%s[not(./child::%s)]', this.toXPath(), convertToSubSelector(locator))
446
+ return new Locator({ xpath })
447
+ }
448
+
449
+ /**
450
+ * Find an element that has no descendant matching the provided locator.
451
+ *
452
+ * Example:
453
+ * locate('button').withoutDescendant('svg')
454
+ *
455
+ * @param {CodeceptJS.LocatorOrString} locator
456
+ * @returns {Locator}
457
+ */
458
+ withoutDescendant(locator) {
459
+ const xpath = sprintf('%s[not(./descendant::%s)]', this.toXPath(), convertToSubSelector(locator))
460
+ return new Locator({ xpath })
461
+ }
462
+
463
+ /**
464
+ * Append a raw XPath predicate. Escape hatch for expressions not covered by the DSL.
465
+ * Argument is inserted as-is inside `[ ]`; quoting/escaping is the caller's responsibility.
466
+ *
467
+ * Example:
468
+ * locate('input').and('@type="text" or @type="email"')
469
+ *
470
+ * @param {string} xpathExpression
471
+ * @returns {Locator}
472
+ */
473
+ and(xpathExpression) {
474
+ const xpath = sprintf('%s[%s]', this.toXPath(), xpathExpression)
475
+ return new Locator({ xpath })
476
+ }
477
+
478
+ /**
479
+ * Append a negated raw XPath predicate: `[not(expr)]`.
480
+ *
481
+ * Example:
482
+ * locate('button').andNot('.//svg') // button without a descendant svg
483
+ *
484
+ * @param {string} xpathExpression
485
+ * @returns {Locator}
486
+ */
487
+ andNot(xpathExpression) {
488
+ const xpath = sprintf('%s[not(%s)]', this.toXPath(), xpathExpression)
489
+ return new Locator({ xpath })
490
+ }
491
+
352
492
  /**
353
493
  * @param {String} text
354
494
  * @returns {Locator}
495
+ * @deprecated Use {@link Locator#withClass} for word-exact class matching, or {@link Locator#withAttrContains} for substring matching.
355
496
  */
356
497
  withClassAttr(text) {
357
498
  const xpath = sprintf('%s[%s]', this.toXPath(), `contains(@class, '${text}')`)
@@ -445,15 +586,26 @@ Locator.clickable = {
445
586
  `.//button[./@name = ${literal}]`,
446
587
  `.//*[@aria-label = ${literal}]`,
447
588
  `.//*[@title = ${literal}]`,
448
- `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
589
+ `.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
449
590
  `.//*[@role='button'][normalize-space(.)=${literal}]`,
591
+ `.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
450
592
  ]),
451
593
 
452
594
  /**
453
595
  * @param {string} literal
454
596
  * @returns {string}
455
597
  */
456
- self: literal => `./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
598
+ self: literal => {
599
+ // Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
600
+ // Falling back to `self` without the `not(descendant...)` guard would match a container
601
+ // whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
602
+ // tab labels all sit in its string-value) and click the container itself.
603
+ const narrowest = `contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
604
+ return xpathLocator.combine([
605
+ `.//*[${narrowest}]`,
606
+ `./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
607
+ ])
608
+ },
457
609
  }
458
610
 
459
611
  Locator.field = {
@@ -477,7 +629,7 @@ Locator.field = {
477
629
  `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
478
630
  `.//*[@aria-label = ${literal}]`,
479
631
  `.//*[@title = ${literal}]`,
480
- `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
632
+ `.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
481
633
  ]),
482
634
 
483
635
  /**
@@ -598,16 +750,6 @@ function removePrefix(xpath) {
598
750
  return xpath.replace(/^(\.|\/)+/, '')
599
751
  }
600
752
 
601
- /**
602
- * @private
603
- * check if the locator is a Playwright locator
604
- * @param {string} locator
605
- * @returns {boolean}
606
- */
607
- function isPlaywrightLocator(locator) {
608
- return locator.includes('_react') || locator.includes('_vue')
609
- }
610
-
611
753
  /**
612
754
  * @private
613
755
  * check if the locator is a role locator
package/lib/mocha/cli.js CHANGED
@@ -8,7 +8,9 @@ import { dirname, join } from 'path'
8
8
  import event from '../event.js'
9
9
  import AssertionFailedError from '../assert/error.js'
10
10
  import output from '../output.js'
11
+ import store from '../store.js'
11
12
  import test, { cloneTest } from './test.js'
13
+ import { fixErrorStack } from '../utils/typescript.js'
12
14
 
13
15
  // Get version from package.json to avoid circular dependency
14
16
  const __filename = fileURLToPath(import.meta.url)
@@ -40,7 +42,7 @@ class Cli extends Base {
40
42
  if (opts.verbose) level = 3
41
43
  output.level(level)
42
44
  output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
43
- output.print(`Using test root "${global.codecept_dir}"`)
45
+ output.print(`Using test root "${store.codeceptDir}"`)
44
46
 
45
47
  const showSteps = level >= 1
46
48
 
@@ -201,7 +203,18 @@ class Cli extends Base {
201
203
 
202
204
  // failures
203
205
  if (stats.failures) {
206
+ for (const test of this.failures) {
207
+ if (test.err && typeof test.err.fetchDetails === 'function') {
208
+ try {
209
+ await test.err.fetchDetails()
210
+ } catch (e) {
211
+ // ignore fetch errors
212
+ }
213
+ }
214
+ }
215
+
204
216
  // append step traces
217
+ const Container = await getContainer()
205
218
  this.failures = this.failures.map(test => {
206
219
  // we will change the stack trace, so we need to clone the test
207
220
  const err = test.err
@@ -264,6 +277,11 @@ class Cli extends Base {
264
277
  }
265
278
 
266
279
  try {
280
+ const fileMapping = Container?.tsFileMapping?.()
281
+ if (fileMapping) {
282
+ fixErrorStack(err, fileMapping)
283
+ }
284
+
267
285
  let stack = err.stack
268
286
  stack = (stack || '').replace(originalMessage, '')
269
287
  stack = stack ? stack.split('\n') : []