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/actor.js CHANGED
@@ -1,10 +1,7 @@
1
1
  import Step, { MetaStep } from './step.js'
2
2
  import recordStep from './step/record.js'
3
- import retryStep from './step/retry.js'
4
3
  import { methodsOfObject } from './utils.js'
5
- import { TIMEOUT_ORDER } from './timeout.js'
6
4
  import event from './event.js'
7
- import store from './store.js'
8
5
  import output from './output.js'
9
6
  import Container from './container.js'
10
7
 
@@ -30,38 +27,6 @@ class Actor {
30
27
  output.say(msg, `${color}`)
31
28
  })
32
29
  }
33
-
34
- /**
35
- * set the maximum execution time for the next step
36
- * @function
37
- * @param {number} timeout - step timeout in seconds
38
- * @return {this}
39
- * @inner
40
- */
41
- limitTime(timeout) {
42
- if (!store.timeouts) return this
43
-
44
- console.log('I.limitTime() is deprecated, use step.timeout() instead')
45
-
46
- event.dispatcher.prependOnceListener(event.step.before, step => {
47
- output.log(`Timeout to ${step}: ${timeout}s`)
48
- step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime)
49
- })
50
-
51
- return this
52
- }
53
-
54
- /**
55
- * @function
56
- * @param {*} [opts]
57
- * @return {this}
58
- * @inner
59
- */
60
- retry(opts) {
61
- console.log('I.retry() is deprecated, use step.retry() instead')
62
- retryStep(opts)
63
- return this
64
- }
65
30
  }
66
31
 
67
32
  /**
@@ -94,7 +59,7 @@ export default function (obj = {}, container) {
94
59
  actor[action] = actor[actionAlias] = function () {
95
60
  const step = new Step(helper, action)
96
61
  if (translation.loaded) {
97
- step.name = actionAlias
62
+ step.title = actionAlias
98
63
  step.actor = translation.I
99
64
  }
100
65
  // add methods to promise chain
package/lib/ai.js CHANGED
@@ -7,6 +7,7 @@ import { generateText } from 'ai'
7
7
  import { fileURLToPath } from 'url'
8
8
  import path from 'path'
9
9
  import { fileExists } from './utils.js'
10
+ import store from './store.js'
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
13
 
@@ -24,8 +25,8 @@ async function loadPrompts() {
24
25
  for (const name of promptNames) {
25
26
  let promptPath
26
27
 
27
- if (global.codecept_dir) {
28
- promptPath = path.join(global.codecept_dir, `prompts/${name}.js`)
28
+ if (store.codeceptDir) {
29
+ promptPath = path.join(store.codeceptDir, `prompts/${name}.js`)
29
30
  }
30
31
 
31
32
  if (!promptPath || !fileExists(promptPath)) {
package/lib/aria.js ADDED
@@ -0,0 +1,260 @@
1
+ import yaml from 'js-yaml'
2
+
3
+ // ─────────────────────────────────────────────────────────────────
4
+ // Roles
5
+ // ─────────────────────────────────────────────────────────────────
6
+
7
+ const INTERACTIVE_ROLES = new Set([
8
+ 'button',
9
+ 'link',
10
+ 'textbox',
11
+ 'searchbox',
12
+ 'checkbox',
13
+ 'radio',
14
+ 'switch',
15
+ 'combobox',
16
+ 'listbox',
17
+ 'listitem',
18
+ 'menu',
19
+ 'menuitem',
20
+ 'menuitemcheckbox',
21
+ 'menuitemradio',
22
+ 'option',
23
+ 'tab',
24
+ 'tabpanel',
25
+ 'slider',
26
+ 'spinbutton',
27
+ 'treeitem',
28
+ 'gridcell',
29
+ ])
30
+
31
+ // Long groups of same-role siblings get summarised as: first N + "...M omitted..." + last N
32
+ const SIBLING_COLLAPSE_THRESHOLD = 50
33
+ const SIBLING_COLLAPSE_KEEP_EACH_SIDE = 5
34
+
35
+ // ─────────────────────────────────────────────────────────────────
36
+ // STEP 1 · Parse: YAML text → AriaNode[]
37
+ // ─────────────────────────────────────────────────────────────────
38
+
39
+ function unquote(value) {
40
+ const v = value.trim()
41
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
42
+ return v.slice(1, -1)
43
+ }
44
+ return v
45
+ }
46
+
47
+ // Parse one YAML node label like: `button "Save"`, `textbox "Email" [focused]`, `heading "Title" [level=2]`
48
+ function parseLabel(label) {
49
+ if (!label) return null
50
+ const trimmed = label.trim()
51
+ const roleMatch = trimmed.match(/^(\w+)/)
52
+ if (!roleMatch) return null
53
+ const role = roleMatch[1].toLowerCase()
54
+ let rest = trimmed.slice(roleMatch[0].length)
55
+
56
+ let name
57
+ const nameMatch = rest.match(/^\s*"((?:[^"\\]|\\.)*)"/) || rest.match(/^\s*'((?:[^'\\]|\\.)*)'/)
58
+ if (nameMatch) {
59
+ name = nameMatch[1]
60
+ rest = rest.slice(nameMatch[0].length)
61
+ }
62
+
63
+ const attributes = {}
64
+ const attrMatch = rest.match(/\[([^\]]*)\]/)
65
+ if (attrMatch) {
66
+ for (const tok of attrMatch[1].split(/[\s,]+/).filter(Boolean)) {
67
+ const eq = tok.indexOf('=')
68
+ if (eq === -1) {
69
+ attributes[tok.toLowerCase()] = true
70
+ continue
71
+ }
72
+ attributes[tok.slice(0, eq).trim().toLowerCase()] = unquote(tok.slice(eq + 1))
73
+ }
74
+ }
75
+
76
+ return { role, name, attributes }
77
+ }
78
+
79
+ function yamlItemToNode(item) {
80
+ if (typeof item === 'string') {
81
+ const label = parseLabel(item)
82
+ if (!label) return null
83
+ const node = { role: label.role, name: label.name, attributes: label.attributes, children: [] }
84
+ return node
85
+ }
86
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return null
87
+
88
+ const entries = Object.entries(item)
89
+ if (entries.length === 0) return null
90
+ const [key, value] = entries[0]
91
+ const label = parseLabel(key)
92
+ if (!label) return null
93
+ const node = { role: label.role, name: label.name, attributes: label.attributes, children: [] }
94
+
95
+ if (Array.isArray(value)) {
96
+ node.children = value.map(yamlItemToNode).filter(n => n !== null)
97
+ return node
98
+ }
99
+ if (value !== null && value !== undefined) node.value = String(value)
100
+ return node
101
+ }
102
+
103
+ function parseSnapshot(snapshot) {
104
+ if (!snapshot) return []
105
+ let parsed
106
+ try {
107
+ parsed = yaml.load(snapshot)
108
+ } catch {
109
+ return []
110
+ }
111
+ if (!Array.isArray(parsed)) return []
112
+ return parsed.map(yamlItemToNode).filter(n => n !== null)
113
+ }
114
+
115
+ // ─────────────────────────────────────────────────────────────────
116
+ // STEP 2 · Transform: drop containers that contribute nothing.
117
+ // ─────────────────────────────────────────────────────────────────
118
+
119
+ function dropEmpty(nodes) {
120
+ return nodes.flatMap(node => {
121
+ const children = dropEmpty(node.children)
122
+ if (INTERACTIVE_ROLES.has(node.role)) return [{ ...node, children }]
123
+ if (children.length > 0) return [{ ...node, children }]
124
+ return []
125
+ })
126
+ }
127
+
128
+ // ─────────────────────────────────────────────────────────────────
129
+ // STEP 3 · Render: AriaNode[] → indented YAML text
130
+ // ─────────────────────────────────────────────────────────────────
131
+
132
+ // One-line representation of a node. Stable attr order so diff comparisons are deterministic.
133
+ function formatNode(node) {
134
+ let line = node.role
135
+ if (node.name && node.name.trim()) line += ` "${node.name.trim()}"`
136
+ const attrParts = []
137
+ for (const k of Object.keys(node.attributes).sort()) {
138
+ const v = node.attributes[k]
139
+ if (v === undefined || v === null || v === '') continue
140
+ if (v === true) attrParts.push(k)
141
+ else attrParts.push(`${k}=${v}`)
142
+ }
143
+ if (attrParts.length > 0) line += ` [${attrParts.join(' ')}]`
144
+ if (node.value !== undefined && node.value !== null) {
145
+ const text = String(node.value).trim()
146
+ if (text) line += `: ${text}`
147
+ }
148
+ return line
149
+ }
150
+
151
+ // Group consecutive same-role siblings. [a,a,b,a,a,a] → [[a,a],[b],[a,a,a]]
152
+ function groupByConsecutiveRole(nodes) {
153
+ return nodes.reduce((groups, node) => {
154
+ const last = groups[groups.length - 1]
155
+ if (last && last[0].role === node.role) {
156
+ last.push(node)
157
+ return groups
158
+ }
159
+ groups.push([node])
160
+ return groups
161
+ }, [])
162
+ }
163
+
164
+ // Large group of same-role siblings → first N + placeholder line + last N.
165
+ // Returns mix of AriaNode (to render) and pre-rendered placeholder strings.
166
+ function collapseGroup(group, depth) {
167
+ if (group.length <= SIBLING_COLLAPSE_THRESHOLD) return group
168
+ const keep = SIBLING_COLLAPSE_KEEP_EACH_SIDE
169
+ const omitted = group.length - keep * 2
170
+ const placeholder = `${' '.repeat(depth)}- ...${omitted} similar "${group[0].role}" items omitted...`
171
+ return [...group.slice(0, keep), placeholder, ...group.slice(-keep)]
172
+ }
173
+
174
+ function renderTree(nodes, depth = 0) {
175
+ const items = groupByConsecutiveRole(nodes).flatMap(group => collapseGroup(group, depth))
176
+ return items
177
+ .map(item => {
178
+ if (typeof item === 'string') return item
179
+ const indent = ' '.repeat(depth)
180
+ const head = `${indent}- ${formatNode(item)}`
181
+ if (item.children.length === 0) return head
182
+ return `${head}:\n${renderTree(item.children, depth + 1)}`
183
+ })
184
+ .join('\n')
185
+ }
186
+
187
+ // ─────────────────────────────────────────────────────────────────
188
+ // STEP 4 · Diff: collect interactive summaries → bag diff → text
189
+ // ─────────────────────────────────────────────────────────────────
190
+
191
+ // Walk tree, emit one summary string per meaningful interactive node.
192
+ function collectSummaries(nodes) {
193
+ return nodes.flatMap(node => {
194
+ const fromChildren = collectSummaries(node.children)
195
+ if (!INTERACTIVE_ROLES.has(node.role)) return fromChildren
196
+ const summary = formatNode(node)
197
+ if (summary === node.role) return fromChildren // skip empty unnamed interactive nodes
198
+ return [summary, ...fromChildren]
199
+ })
200
+ }
201
+
202
+ function countBy(items) {
203
+ return items.reduce((map, item) => {
204
+ map.set(item, (map.get(item) ?? 0) + 1)
205
+ return map
206
+ }, new Map())
207
+ }
208
+
209
+ // Bag diff: any summary appearing more in one bag than the other becomes added/removed.
210
+ function diffSummaries(prev, curr) {
211
+ const before = countBy(prev)
212
+ const after = countBy(curr)
213
+ const added = []
214
+ const removed = []
215
+ for (const summary of new Set([...before.keys(), ...after.keys()])) {
216
+ const b = before.get(summary) ?? 0
217
+ const a = after.get(summary) ?? 0
218
+ for (let i = 0; i < a - b; i += 1) added.push(summary)
219
+ for (let i = 0; i < b - a; i += 1) removed.push(summary)
220
+ }
221
+ return { added, removed }
222
+ }
223
+
224
+ function formatDiff(added, removed) {
225
+ if (added.length === 0 && removed.length === 0) return null
226
+ const lines = ['ariaDiff:']
227
+ if (added.length === 0) {
228
+ lines.push(' added: []')
229
+ } else {
230
+ lines.push(' added:')
231
+ for (const [item, count] of [...countBy(added).entries()].sort(([a], [b]) => a.localeCompare(b))) {
232
+ const suffix = count > 1 ? ` (x${count})` : ''
233
+ lines.push(` - ${item}${suffix}`)
234
+ }
235
+ }
236
+ if (removed.length === 0) {
237
+ lines.push(' removed: []')
238
+ } else {
239
+ lines.push(` removed: ${removed.length} interactive elements`)
240
+ }
241
+ return lines.join('\n')
242
+ }
243
+
244
+ // ─────────────────────────────────────────────────────────────────
245
+ // Public API — pipelines composed visibly, top-to-bottom
246
+ // ─────────────────────────────────────────────────────────────────
247
+
248
+ function compactAriaSnapshot(snapshot) {
249
+ if (!snapshot) return ''
250
+ const tree = dropEmpty(parseSnapshot(snapshot))
251
+ return renderTree(tree)
252
+ }
253
+
254
+ function diffAriaSnapshots(previous, current) {
255
+ const summariesOf = snap => collectSummaries(dropEmpty(parseSnapshot(snap)))
256
+ const { added, removed } = diffSummaries(summariesOf(previous), summariesOf(current))
257
+ return formatDiff(added, removed)
258
+ }
259
+
260
+ export { diffAriaSnapshots, compactAriaSnapshot }
@@ -0,0 +1,18 @@
1
+ import Assertion from './assert.js'
2
+ import { equals, urlEquals, fileEquals } from './assert/equal.js'
3
+ import { includes, fileIncludes } from './assert/include.js'
4
+ import { empty } from './assert/empty.js'
5
+ import { truth } from './assert/truth.js'
6
+
7
+ export { Assertion, equals, urlEquals, fileEquals, includes, fileIncludes, empty, truth }
8
+
9
+ export default {
10
+ Assertion,
11
+ equals,
12
+ urlEquals,
13
+ fileEquals,
14
+ includes,
15
+ fileIncludes,
16
+ empty,
17
+ truth,
18
+ }
package/lib/codecept.js CHANGED
@@ -19,8 +19,9 @@ import ActorFactory from './actor.js'
19
19
  import output from './output.js'
20
20
  import { emptyFolder } from './utils.js'
21
21
  import { initCodeceptGlobals } from './globals.js'
22
- import { validateTypeScriptSetup } from './utils/loaderCheck.js'
22
+ import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
23
23
  import recorder from './recorder.js'
24
+ import store from './store.js'
24
25
 
25
26
  import storeListener from './listener/store.js'
26
27
  import stepsListener from './listener/steps.js'
@@ -71,7 +72,7 @@ class Codecept {
71
72
  } else {
72
73
  // For npm packages, resolve from the user's directory
73
74
  // This ensures packages like tsx are found in user's node_modules
74
- const userDir = global.codecept_dir || process.cwd()
75
+ const userDir = store.codeceptDir || process.cwd()
75
76
 
76
77
  try {
77
78
  // Use createRequire to resolve from user's directory
@@ -102,8 +103,6 @@ class Codecept {
102
103
  await this.requireModules(this.requiringModules)
103
104
  // initializing listeners
104
105
  await container.create(this.config, this.opts)
105
- // Store container globally for easy access
106
- global.container = container
107
106
  await this.runHooks()
108
107
  }
109
108
 
@@ -120,23 +119,27 @@ class Codecept {
120
119
  * Executes hooks.
121
120
  */
122
121
  async runHooks() {
123
- // default hooks - dynamic imports for ESM
124
- const listenerModules = [
125
- './listener/store.js',
126
- './listener/steps.js',
127
- './listener/config.js',
128
- './listener/result.js',
129
- './listener/helpers.js',
130
- './listener/globalTimeout.js',
131
- './listener/globalRetry.js',
132
- './listener/retryEnhancer.js',
133
- './listener/exit.js',
134
- './listener/emptyRun.js',
135
- ]
136
-
137
- for (const modulePath of listenerModules) {
138
- const module = await import(modulePath)
139
- runHook(module.default || module)
122
+ // For workers parent process we only need plugins/hooks.
123
+ // Core listeners are executed inside worker threads.
124
+ if (!this.opts?.skipDefaultListeners) {
125
+ const listenerModules = [
126
+ './listener/store.js',
127
+ './listener/steps.js',
128
+ './listener/config.js',
129
+ './listener/result.js',
130
+ './listener/helpers.js',
131
+ './listener/pageobjects.js',
132
+ './listener/globalTimeout.js',
133
+ './listener/globalRetry.js',
134
+ './listener/retryEnhancer.js',
135
+ './listener/exit.js',
136
+ './listener/emptyRun.js',
137
+ ]
138
+
139
+ for (const modulePath of listenerModules) {
140
+ const module = await import(modulePath)
141
+ runHook(module.default || module)
142
+ }
140
143
  }
141
144
 
142
145
  // custom hooks (previous iteration of plugins)
@@ -168,7 +171,7 @@ class Codecept {
168
171
  */
169
172
  loadTests(pattern) {
170
173
  const options = {
171
- cwd: global.codecept_dir,
174
+ cwd: store.codeceptDir,
172
175
  }
173
176
 
174
177
  let patterns = [pattern]
@@ -200,7 +203,7 @@ class Codecept {
200
203
  globSync(pattern, options).forEach(file => {
201
204
  if (file.includes('node_modules')) return
202
205
  if (!fsPath.isAbsolute(file)) {
203
- file = fsPath.join(global.codecept_dir, file)
206
+ file = fsPath.join(store.codeceptDir, file)
204
207
  }
205
208
  if (!this.testFiles.includes(fsPath.resolve(file))) {
206
209
  this.testFiles.push(fsPath.resolve(file))
@@ -270,6 +273,12 @@ class Codecept {
270
273
  process.exit(1)
271
274
  }
272
275
 
276
+ // Show warning if ts-node/esm is being used
277
+ const tsWarning = getTSNodeESMWarning(this.requiringModules || [])
278
+ if (tsWarning) {
279
+ output.print(output.colors.yellow(tsWarning))
280
+ }
281
+
273
282
  // Ensure translations are loaded for Gherkin features
274
283
  try {
275
284
  const { loadTranslations } = await import('./mocha/gherkin.js')
@@ -284,7 +293,7 @@ class Codecept {
284
293
 
285
294
  if (test) {
286
295
  if (!fsPath.isAbsolute(test)) {
287
- test = fsPath.join(global.codecept_dir, test)
296
+ test = fsPath.join(store.codeceptDir, test)
288
297
  }
289
298
  const testBasename = fsPath.basename(test, '.js')
290
299
  const testFeatureBasename = fsPath.basename(test, '.feature')
@@ -307,7 +316,7 @@ class Codecept {
307
316
 
308
317
  try {
309
318
  event.emit(event.all.before, this)
310
- mocha.run(async (failures) => await done(failures))
319
+ mocha.runner = mocha.run(async (failures) => await done(failures))
311
320
  } catch (e) {
312
321
  output.error(e.stack)
313
322
  reject(e)
@@ -135,6 +135,7 @@ export default async function (options) {
135
135
  printCheck('plugins', checks['plugins'], Object.keys(Container.plugins()).join(', '))
136
136
 
137
137
  if (Object.keys(helpers).length) {
138
+ store.dryRun = false
138
139
  const suite = Container.mocha().suite
139
140
  const test = createTest('test', () => {})
140
141
  checks.setup = true
@@ -154,7 +155,6 @@ export default async function (options) {
154
155
  checks.teardown = true
155
156
  for (const helper of Object.values(helpers).reverse()) {
156
157
  try {
157
- if (helper._passed) await helper._passed(test)
158
158
  if (helper._after) await helper._after(test)
159
159
  if (helper._finishTest) await helper._finishTest(suite)
160
160
  if (helper._afterSuite) await helper._afterSuite(suite)
@@ -166,6 +166,7 @@ export default async function (options) {
166
166
  }
167
167
 
168
168
  printCheck('Helpers After', checks['teardown'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Closing browser' : '')
169
+ store.dryRun = true
169
170
  }
170
171
 
171
172
  try {
@@ -229,13 +229,12 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
229
229
  const importStrings = []
230
230
 
231
231
  for (const name in pathsToType) {
232
- const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath)
233
- // For ESM modules with default exports, we need to access the default export type
234
- if (relativePath.endsWith('.js')) {
235
- importStrings.push(`type ${name} = typeof import('${relativePath}')['default'];`)
236
- } else {
237
- importStrings.push(`type ${name} = typeof import('${relativePath}');`)
238
- }
232
+ const originalPath = pathsToType[name]
233
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
234
+ // 4.x is ESM-first: step files and page objects use `export default`,
235
+ // so the type is reached via `.default` regardless of file extension
236
+ // (.js, .ts, or no extension, as set in `include`).
237
+ importStrings.push(`type ${name} = typeof import('${relativePath}').default;`)
239
238
  }
240
239
 
241
240
  for (const name in pathsToValue) {
@@ -1,3 +1,4 @@
1
+ import chalk from 'chalk'
1
2
  import { getConfig, getTestRoot } from './utils.js'
2
3
  import Config from '../config.js'
3
4
  import Codecept from '../codecept.js'
@@ -8,6 +9,8 @@ import Container from '../container.js'
8
9
 
9
10
  export default async function (test, options) {
10
11
  if (options.grep) process.env.grep = options.grep
12
+ if (options.ansi === false) chalk.level = 0
13
+ store.dryRun = true
11
14
  const configFile = options.config
12
15
  let codecept
13
16
 
@@ -18,10 +21,15 @@ export default async function (test, options) {
18
21
  }
19
22
 
20
23
  if (config.plugins) {
21
- // disable all plugins by default, they can be enabled with -p option
24
+ // Disable plugins that block (interactive) or perform external I/O (AI/network).
25
+ // Leave the rest enabled so they can register support objects (e.g. auth registers
26
+ // `login`); helper calls inside those support fns are already no-op'd by HelperStep
27
+ // when store.dryRun is true.
28
+ const disableInDryRun = new Set(['pause', 'pauseOnFail', 'analyze', 'aiTrace', 'pageInfo', 'heal'])
22
29
  for (const plugin in config.plugins) {
23
- // if `-p all` is passed, then enabling all plugins, otherwise plugins could be enabled by `-p customLocator,commentStep,tryTo`
24
- config.plugins[plugin].enabled = options.plugins === 'all'
30
+ if (disableInDryRun.has(plugin)) {
31
+ config.plugins[plugin].enabled = false
32
+ }
25
33
  }
26
34
  }
27
35
 
@@ -32,12 +40,12 @@ export default async function (test, options) {
32
40
  if (options.bootstrap) await codecept.bootstrap()
33
41
 
34
42
  codecept.loadTests()
35
- store.dryRun = true
36
43
 
37
44
  if (!options.steps && !options.verbose && !options.debug) {
38
45
  await printTests(codecept.testFiles)
39
46
  return
40
47
  }
48
+ if (options.numbers) numberSteps()
41
49
  event.dispatcher.on(event.all.result, printFooter)
42
50
  await codecept.run(test)
43
51
  } catch (err) {
@@ -46,11 +54,22 @@ export default async function (test, options) {
46
54
  }
47
55
  }
48
56
 
57
+ function numberSteps() {
58
+ let stepIndex = 0
59
+ event.dispatcher.on(event.test.before, () => {
60
+ stepIndex = 0
61
+ })
62
+ event.dispatcher.prependListener(event.step.before, step => {
63
+ stepIndex++
64
+ step.prefix = `${stepIndex}. ${step.prefix || ''}`
65
+ })
66
+ }
67
+
49
68
  async function printTests(files) {
50
69
  const { default: figures } = await import('figures')
51
70
  const { default: colors } = await import('chalk')
52
71
 
53
- output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`))
72
+ output.print(output.styles.debug(`Tests from ${store.codeceptDir}:`))
54
73
  output.print()
55
74
 
56
75
  const mocha = Container.mocha()
@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
5
5
  import path from 'path'
6
6
  import { fileExists, ucfirst, lcfirst, beautify } from '../utils.js'
7
7
  import output from '../output.js'
8
+ import store from '../store.js'
8
9
  import generateDefinitions from './definitions.js'
9
10
  import { getConfig, getTestRoot, safeFileWrite, readConfig } from './utils.js'
10
11
 
@@ -20,6 +21,7 @@ Scenario('test something', async ({ {{actor}} }) => {
20
21
  // generates empty test
21
22
  export async function test(genPath) {
22
23
  const testsPath = getTestRoot(genPath)
24
+ store.codeceptDir = testsPath
23
25
  global.codecept_dir = testsPath
24
26
  const config = await getConfig(testsPath)
25
27
  if (!config) return
@@ -85,7 +87,7 @@ export default {
85
87
 
86
88
  const poModuleTemplateTS = `const { I } = inject();
87
89
 
88
- export = {
90
+ export default {
89
91
 
90
92
  // insert your locators and methods here
91
93
  }
@@ -8,6 +8,7 @@ import fsPath from 'path'
8
8
  import { getConfig, getTestRoot } from '../utils.js'
9
9
  import Codecept from '../../codecept.js'
10
10
  import output from '../../output.js'
11
+ import store from '../../store.js'
11
12
  import { matchStep } from '../../mocha/bdd.js'
12
13
 
13
14
  const uuidFn = IdGenerator.uuid()
@@ -43,9 +44,9 @@ export default async function (genPath, options) {
43
44
  }
44
45
 
45
46
  const files = []
46
- globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : global.codecept_dir }).forEach(file => {
47
+ globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : store.codeceptDir }).forEach(file => {
47
48
  if (!fsPath.isAbsolute(file)) {
48
- file = fsPath.join(global.codecept_dir, file)
49
+ file = fsPath.join(store.codeceptDir, file)
49
50
  }
50
51
  files.push(fsPath.resolve(file))
51
52
  })
@@ -92,7 +93,7 @@ export default async function (genPath, options) {
92
93
  if (child.scenario.keyword === 'Scenario Outline') continue // skip scenario outline
93
94
  parseSteps(child.scenario.steps)
94
95
  .map(step => {
95
- return Object.assign(step, { file: file.replace(global.codecept_dir, '').slice(1) })
96
+ return Object.assign(step, { file: file.replace(store.codeceptDir, '').slice(1) })
96
97
  })
97
98
  .map(step => newSteps.set(`${step.type}(${step})`, step))
98
99
  }
@@ -107,7 +108,7 @@ export default async function (genPath, options) {
107
108
  }
108
109
 
109
110
  if (!fsPath.isAbsolute(stepFile)) {
110
- stepFile = fsPath.join(global.codecept_dir, stepFile)
111
+ stepFile = fsPath.join(store.codeceptDir, stepFile)
111
112
  }
112
113
 
113
114
  const snippets = [...newSteps.values()]