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
@@ -7,6 +7,8 @@ import gherkinParser, { loadTranslations } from './gherkin.js'
7
7
  import output from '../output.js'
8
8
  import scenarioUiFunction from './ui.js'
9
9
  import { initMochaGlobals } from '../globals.js'
10
+ import { fixErrorStack } from '../utils/typescript.js'
11
+ import container from '../container.js'
10
12
 
11
13
  const __filename = fileURLToPath(import.meta.url)
12
14
  const __dirname = fsPath.dirname(__filename)
@@ -15,7 +17,11 @@ let mocha
15
17
 
16
18
  class MochaFactory {
17
19
  static create(config, opts) {
18
- mocha = new Mocha(Object.assign(config, opts))
20
+ const merged = Object.assign({}, config, opts)
21
+ mocha = new Mocha(merged)
22
+ if (merged.cleanReferencesAfterRun !== true) {
23
+ mocha.cleanReferencesAfterRun(false)
24
+ }
19
25
  output.process(opts.child)
20
26
  mocha.ui(scenarioUiFunction)
21
27
 
@@ -34,6 +40,10 @@ class MochaFactory {
34
40
  // Handle ECONNREFUSED without dynamic import for now
35
41
  err = new Error('Connection refused: ' + err.toString())
36
42
  }
43
+ const fileMapping = container?.tsFileMapping?.()
44
+ if (fileMapping) {
45
+ fixErrorStack(err, fileMapping)
46
+ }
37
47
  output.error(err)
38
48
  output.print(err.stack)
39
49
  process.exit(1)
@@ -5,7 +5,7 @@ const getInjectedArguments = async (fn, test, suite) => {
5
5
  const container = containerModule.default || containerModule
6
6
 
7
7
  const testArgs = {}
8
- const params = getParams(fn) || []
8
+ const params = getParams(fn, { warnOnLegacyFormat: true }) || []
9
9
  const objects = container.support()
10
10
 
11
11
  for (const key of params) {
@@ -1,4 +1,5 @@
1
1
  import { isAsyncFunction } from '../utils.js'
2
+ import store from '../store.js'
2
3
 
3
4
  /** @class */
4
5
  class ScenarioConfig {
@@ -40,7 +41,7 @@ class ScenarioConfig {
40
41
  * @returns {this}
41
42
  */
42
43
  retry(retries) {
43
- if (process.env.SCENARIO_ONLY) retries = -retries
44
+ if (store.scenarioOnly) retries = -retries
44
45
  this.test.retries(retries)
45
46
  return this
46
47
  }
package/lib/mocha/ui.js CHANGED
@@ -9,13 +9,12 @@ import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } fr
9
9
  import { initMochaGlobals } from '../globals.js'
10
10
  import common from 'mocha/lib/interfaces/common.js'
11
11
  import container from '../container.js'
12
+ import store from '../store.js'
12
13
 
13
14
  const setContextTranslation = context => {
14
- // Try global container first, then local container instance
15
- const containerToUse = global.container || container
16
- if (!containerToUse) return
15
+ if (!container) return
17
16
 
18
- const translation = containerToUse.translation?.() || containerToUse.translation
17
+ const translation = container.translation?.() || container.translation
19
18
  const contexts = translation?.value?.('contexts')
20
19
 
21
20
  if (contexts) {
@@ -119,7 +118,7 @@ export default function (suite) {
119
118
  context.Feature.only = function (title, opts) {
120
119
  const reString = `^${escapeRe(`${title}:`)}`
121
120
  mocha.grep(new RegExp(reString))
122
- process.env.FEATURE_ONLY = true
121
+ store.featureOnly = true
123
122
  return context.Feature(title, opts)
124
123
  }
125
124
 
@@ -171,7 +170,7 @@ export default function (suite) {
171
170
  context.Scenario.only = function (title, opts, fn) {
172
171
  const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`
173
172
  mocha.grep(new RegExp(reString))
174
- process.env.SCENARIO_ONLY = true
173
+ store.scenarioOnly = true
175
174
  return addScenario(title, opts, fn)
176
175
  }
177
176
 
package/lib/parser.js CHANGED
@@ -14,11 +14,11 @@ export const getParamsToString = function (fn) {
14
14
  return getParams(newFn).join(', ')
15
15
  }
16
16
 
17
- function getParams(fn) {
17
+ function getParams(fn, { warnOnLegacyFormat = false } = {}) {
18
18
  if (fn.isSinonProxy) return []
19
19
  try {
20
20
  const reflected = parser.parse(fn)
21
- if (reflected.args.length > 1 || reflected.args[0] === 'I') {
21
+ if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
22
22
  output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
23
23
  }
24
24
  if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs]
package/lib/pause.js CHANGED
@@ -18,6 +18,8 @@ let nextStep
18
18
  let finish
19
19
  let next
20
20
  let registeredVariables = {}
21
+ let externalHandler = null
22
+
21
23
  /**
22
24
  * Pauses test execution and starts interactive shell
23
25
  * @param {Object<string, *>} [passedObject]
@@ -37,10 +39,10 @@ const pause = function (passedObject = {}) {
37
39
  })
38
40
 
39
41
  event.dispatcher.on(event.test.finished, () => {
40
- finish()
42
+ if (typeof finish === 'function') finish()
41
43
  recorder.session.restore('pause')
42
- rl.close()
43
- history.save()
44
+ if (rl) rl.close()
45
+ if (!externalHandler) history.save()
44
46
  })
45
47
 
46
48
  recorder.add('Start new session', () => pauseSession(passedObject))
@@ -49,6 +51,15 @@ const pause = function (passedObject = {}) {
49
51
  function pauseSession(passedObject = {}) {
50
52
  registeredVariables = passedObject
51
53
  recorder.session.start('pause')
54
+
55
+ if (externalHandler) {
56
+ store.onPause = true
57
+ return externalHandler({ registeredVariables }).then(() => {
58
+ store.onPause = false
59
+ recorder.session.restore('pause')
60
+ })
61
+ }
62
+
52
63
  if (!next) {
53
64
  let vars = Object.keys(registeredVariables).join(', ')
54
65
  if (vars) vars = `(vars: ${vars})`
@@ -234,5 +245,28 @@ function registerVariable(name, value) {
234
245
  registeredVariables[name] = value
235
246
  }
236
247
 
248
+ /**
249
+ * Hook for external pause drivers (e.g. the MCP server). When set, pauseSession
250
+ * delegates to the handler instead of opening a readline REPL. The handler
251
+ * receives `{ registeredVariables }` and returns a Promise that resolves when
252
+ * the driver decides to continue (resume) or step.
253
+ *
254
+ * The driver controls step-vs-resume by mutating `next` via setNextStep before
255
+ * resolving its Promise.
256
+ */
257
+ function setPauseHandler(handler) {
258
+ externalHandler = handler
259
+ }
260
+
261
+ /**
262
+ * Trigger a one-shot pause from outside the test (e.g. the MCP server,
263
+ * pausing the test at a specific step index without modifying the test).
264
+ * Schedules pauseSession through the recorder so it slots between steps.
265
+ */
266
+ function pauseNow(passedObject = {}) {
267
+ if (store.dryRun) return
268
+ recorder.add('Triggered pause', () => pauseSession(passedObject))
269
+ }
270
+
237
271
  export default pause
238
- export { registerVariable }
272
+ export { registerVariable, setPauseHandler, pauseNow }
@@ -0,0 +1,457 @@
1
+ import fs from 'fs'
2
+ import { mkdirp } from 'mkdirp'
3
+ import path from 'path'
4
+
5
+ import store from '../store.js'
6
+ import Container from '../container.js'
7
+ import recorder from '../recorder.js'
8
+ import event from '../event.js'
9
+ import output from '../output.js'
10
+ import { deleteDir, clearString } from '../utils.js'
11
+ import { captureSnapshot, pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
12
+ import {
13
+ parsePluginArgs,
14
+ resolveTrigger,
15
+ matchStepFile,
16
+ matchUrl,
17
+ } from '../utils/pluginParser.js'
18
+
19
+ const defaultConfig = {
20
+ on: 'step',
21
+ deleteSuccessful: false,
22
+ fullPageScreenshots: false,
23
+ output: store.outputDir,
24
+ captureHTML: true,
25
+ captureARIA: true,
26
+ captureBrowserLogs: true,
27
+ captureHTTP: true,
28
+ captureDebugOutput: true,
29
+ ignoreSteps: [],
30
+ }
31
+
32
+ /**
33
+ *
34
+ * Generates AI-friendly trace files for debugging with AI agents.
35
+ * This plugin creates a markdown file with test execution logs and links to all artifacts
36
+ * (screenshots, HTML, ARIA snapshots, browser logs, HTTP requests) for each step.
37
+ *
38
+ * #### Configuration
39
+ *
40
+ * ```js
41
+ * "plugins": {
42
+ * "aiTrace": {
43
+ * "enabled": true
44
+ * }
45
+ * }
46
+ * ```
47
+ *
48
+ * Possible config options:
49
+ *
50
+ * * `deleteSuccessful`: delete traces for successfully executed tests. Default: false.
51
+ * * `fullPageScreenshots`: should full page screenshots be used. Default: false.
52
+ * * `output`: a directory where traces should be stored. Default: `output`.
53
+ * * `captureHTML`: capture HTML for each step. Default: true.
54
+ * * `captureARIA`: capture ARIA snapshot for each step. Default: true.
55
+ * * `captureBrowserLogs`: capture browser console logs. Default: true.
56
+ * * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
57
+ * * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
58
+ * * `ignoreSteps`: steps to ignore in trace. Array of RegExps is expected.
59
+ * * `on`: trigger mode — `step` (default), `fail`, `test`, `file`, `url`.
60
+ *
61
+ * #### `on=` modes
62
+ *
63
+ * * **step** — persist every step (default)
64
+ * * **fail** — persist only the failed step
65
+ * * **test** — persist only the last step of each test
66
+ * * **file** — persist steps from `path=...[;line=...]`
67
+ * * **url** — persist when the current URL matches `pattern=...`
68
+ *
69
+ * @param {*} config
70
+ */
71
+ export default function (config = {}) {
72
+ const cliArgs = parsePluginArgs(config._args)
73
+ const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'aiTrace' })
74
+ if (!trigger) return
75
+
76
+ config = Object.assign(defaultConfig, config)
77
+
78
+ const helper = pickActingHelper(Container.helpers())
79
+
80
+ if (!helper) {
81
+ output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
82
+ return
83
+ }
84
+
85
+ let dir
86
+ let stepNum
87
+ let steps = []
88
+ let debugOutput = []
89
+ let error
90
+ let savedSteps = new Set()
91
+ let currentTest = null
92
+ let testStartTime
93
+ let currentUrl = null
94
+ let testFailed = false
95
+ let pendingArtifactCapture = null
96
+ let firstFailedStepSaved = false
97
+
98
+ const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
99
+
100
+ if (config.captureDebugOutput) {
101
+ const originalDebug = output.debug
102
+ output.debug = function (...args) {
103
+ debugOutput.push(args.join(' '))
104
+ originalDebug.apply(output, args)
105
+ }
106
+ }
107
+
108
+ event.dispatcher.on(event.suite.before, suite => {
109
+ stepNum = -1
110
+ })
111
+
112
+ event.dispatcher.on(event.test.before, test => {
113
+ let title
114
+ try {
115
+ title = test.fullTitle ? test.fullTitle() : test.title
116
+ } catch (err) {
117
+ title = test.title
118
+ }
119
+ dir = traceDirFor(test.file, title, reportDir)
120
+ mkdirp.sync(dir)
121
+ deleteDir(dir)
122
+ mkdirp.sync(dir)
123
+ stepNum = 0
124
+ error = null
125
+ steps = []
126
+ debugOutput = []
127
+ savedSteps.clear()
128
+ currentTest = test
129
+ testStartTime = Date.now()
130
+ currentUrl = null
131
+ testFailed = false
132
+ firstFailedStepSaved = false
133
+ pendingArtifactCapture = null
134
+ })
135
+
136
+ event.dispatcher.on(event.step.after, step => {
137
+ if (!currentTest) return
138
+ if (step.status === 'failed') {
139
+ testFailed = true
140
+ }
141
+ if (step.status === 'queued' && testFailed) {
142
+ output.debug(`aiTrace: Skipping queued step "${step.toString()}" - testFailed: ${testFailed}`)
143
+ return
144
+ }
145
+ if (step.status === 'failed' && firstFailedStepSaved) {
146
+ output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
147
+ return
148
+ }
149
+
150
+ // on= filtering
151
+ if (trigger.on === 'fail') return // failed steps handled by step.failed
152
+ if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
153
+ if (trigger.on === 'url') {
154
+ recorder.add('aiTrace:url check', async () => {
155
+ try {
156
+ if (!helper.grabCurrentUrl) return
157
+ const url = await helper.grabCurrentUrl()
158
+ if (!matchUrl(url, trigger.pattern)) return
159
+ await persistStep(step)
160
+ } catch (err) {
161
+ output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
162
+ }
163
+ }, true)
164
+ return
165
+ }
166
+
167
+ recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
168
+ output.debug(`aiTrace: Error saving step: ${err.message}`)
169
+ }), true)
170
+ })
171
+
172
+ event.dispatcher.on(event.step.failed, step => {
173
+ if (!currentTest) return
174
+ if (step.status === 'queued' && testFailed) {
175
+ output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
176
+ return
177
+ }
178
+ if (firstFailedStepSaved) {
179
+ output.debug(`aiTrace: Skipping subsequent failed step "${step.toString()}" - already saved first failed step`)
180
+ return
181
+ }
182
+
183
+ const stepKey = step.toString()
184
+ if (savedSteps.has(stepKey)) {
185
+ const existingStep = steps.find(s => s.step === stepKey)
186
+ if (!existingStep) {
187
+ output.debug(`aiTrace: Step "${stepKey}" marked as saved but not found in steps array`)
188
+ return
189
+ }
190
+ existingStep.status = 'failed'
191
+
192
+ pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
193
+ output.debug(`aiTrace: Error updating failed step: ${err.message}`)
194
+ })
195
+ } else {
196
+ if (stepNum === -1) return
197
+ if (isStepIgnored(step)) return
198
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
199
+
200
+ const stepPrefix = generateStepPrefix(step, stepNum)
201
+ stepNum++
202
+
203
+ const stepData = {
204
+ step: stepKey,
205
+ status: 'failed',
206
+ prefix: stepPrefix,
207
+ artifacts: {},
208
+ meta: {},
209
+ debugOutput: [],
210
+ }
211
+
212
+ if (step.startTime && step.endTime) {
213
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
214
+ }
215
+
216
+ savedSteps.add(stepKey)
217
+ steps.push(stepData)
218
+ firstFailedStepSaved = true
219
+
220
+ pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
221
+ output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
222
+ })
223
+ }
224
+ })
225
+
226
+ event.dispatcher.on(event.test.passed, test => {
227
+ if (config.deleteSuccessful) {
228
+ deleteDir(dir)
229
+ return
230
+ }
231
+ persist(test, 'passed')
232
+ })
233
+
234
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
235
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
236
+ return
237
+ }
238
+ recorder.add('aiTrace:persist failed', async () => {
239
+ if (pendingArtifactCapture) {
240
+ await pendingArtifactCapture
241
+ pendingArtifactCapture = null
242
+ }
243
+ persist(test, 'failed')
244
+ }, true)
245
+ })
246
+
247
+ async function persistStep(step) {
248
+ if (stepNum === -1) return
249
+ if (isStepIgnored(step)) return
250
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
251
+
252
+ const stepKey = step.toString()
253
+
254
+ if (savedSteps.has(stepKey)) {
255
+ const existingStep = steps.find(s => s.step === stepKey)
256
+ if (existingStep && step.status === 'failed') {
257
+ existingStep.status = 'failed'
258
+ step.artifacts = {}
259
+ await captureArtifactsForStep(step, existingStep, existingStep.prefix)
260
+ }
261
+ return
262
+ }
263
+ savedSteps.add(stepKey)
264
+
265
+ const stepPrefix = generateStepPrefix(step, stepNum)
266
+ stepNum++
267
+
268
+ const stepData = {
269
+ step: step.toString(),
270
+ status: step.status,
271
+ prefix: stepPrefix,
272
+ artifacts: {},
273
+ meta: {},
274
+ debugOutput: [],
275
+ }
276
+
277
+ if (step.startTime && step.endTime) {
278
+ stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
279
+ }
280
+
281
+ if (config.captureDebugOutput && debugOutput.length > 0) {
282
+ stepData.debugOutput = [...debugOutput]
283
+ debugOutput = []
284
+ }
285
+
286
+ await captureArtifactsForStep(step, stepData, stepPrefix)
287
+ steps.push(stepData)
288
+ }
289
+
290
+ async function captureArtifactsForStep(step, stepData, stepPrefix) {
291
+ if (!step.artifacts) {
292
+ step.artifacts = {}
293
+ }
294
+
295
+ let browserAvailable = true
296
+
297
+ try {
298
+ try {
299
+ if (helper.grabCurrentUrl) {
300
+ const url = await helper.grabCurrentUrl()
301
+ stepData.meta.url = url
302
+ currentUrl = url
303
+ }
304
+ } catch (err) {
305
+ browserAvailable = false
306
+ output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
307
+ }
308
+
309
+ let preExistingScreenshot = false
310
+ if (step.artifacts?.screenshot) {
311
+ const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
312
+ ? step.artifacts.screenshot
313
+ : path.resolve(dir, step.artifacts.screenshot)
314
+ const screenshotFile = path.basename(screenshotPath)
315
+ stepData.artifacts.screenshot = screenshotFile
316
+ step.artifacts.screenshot = screenshotPath
317
+ preExistingScreenshot = true
318
+
319
+ if (!fs.existsSync(screenshotPath)) {
320
+ try {
321
+ await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
322
+ } catch (err) {
323
+ output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
324
+ }
325
+ }
326
+ }
327
+
328
+ const captured = await captureSnapshot(helper, {
329
+ dir,
330
+ prefix: stepPrefix,
331
+ fullPage: config.fullPageScreenshots,
332
+ captureHTML: config.captureHTML && browserAvailable,
333
+ captureARIA: config.captureARIA && browserAvailable,
334
+ captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
335
+ captureStorage: false,
336
+ })
337
+
338
+ if (!preExistingScreenshot && captured.screenshot) {
339
+ stepData.artifacts.screenshot = captured.screenshot
340
+ step.artifacts.screenshot = path.join(dir, captured.screenshot)
341
+ }
342
+ if (step.artifacts?.html) {
343
+ stepData.artifacts.html = step.artifacts.html
344
+ } else if (captured.html) {
345
+ stepData.artifacts.html = captured.html
346
+ }
347
+ if (captured.aria) stepData.artifacts.aria = captured.aria
348
+ if (captured.console) {
349
+ stepData.artifacts.console = captured.console
350
+ stepData.meta.consoleCount = captured.consoleCount
351
+ }
352
+ } catch (err) {
353
+ output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
354
+ }
355
+ }
356
+
357
+ function persist(test, status) {
358
+ if (!steps.length) {
359
+ output.debug('aiTrace: No steps to save in trace')
360
+ return
361
+ }
362
+
363
+ // on=test: only render the last step in markdown; artifacts of earlier steps
364
+ // remain on disk unreferenced.
365
+ if (trigger.on === 'test') {
366
+ steps = steps.slice(-1)
367
+ }
368
+
369
+ const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
370
+
371
+ let markdown = `file: ${test.file || 'unknown'}\n`
372
+ markdown += `name: ${test.title}\n`
373
+ markdown += `time: ${testDuration}s\n`
374
+ markdown += `---\n\n`
375
+
376
+ if (status === 'failed') {
377
+ if (test.art && test.art.message) {
378
+ markdown += `Error: ${test.art.message}\n\n`
379
+ }
380
+ if (test.art && test.art.stack) {
381
+ markdown += `${test.art.stack}\n\n`
382
+ }
383
+ markdown += `---\n\n`
384
+ }
385
+
386
+ if (config.captureDebugOutput && debugOutput.length > 0) {
387
+ markdown += `CodeceptJS Debug Output:\n\n`
388
+ debugOutput.forEach(line => {
389
+ markdown += `> ${line}\n`
390
+ })
391
+ markdown += `\n---\n\n`
392
+ }
393
+
394
+ steps.forEach((stepData, index) => {
395
+ const stepAnchor = clearString(stepData.step).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)
396
+ markdown += `### Step ${index + 1}: ${stepData.step}\n`
397
+ markdown += `<a id="${stepAnchor}"></a>\n`
398
+
399
+ if (stepData.meta.duration) {
400
+ markdown += ` > duration: ${stepData.meta.duration}\n`
401
+ }
402
+
403
+ if (stepData.meta.url) {
404
+ markdown += ` > navigated to ${stepData.meta.url}\n`
405
+ }
406
+
407
+ if (config.captureDebugOutput && stepData.debugOutput && stepData.debugOutput.length > 0) {
408
+ stepData.debugOutput.forEach(line => {
409
+ markdown += ` > ${line}\n`
410
+ })
411
+ }
412
+
413
+ const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
414
+ if (links) markdown += links + '\n'
415
+
416
+ if (config.captureHTTP) {
417
+ if (test.artifacts && test.artifacts.har) {
418
+ const harPath = path.relative(reportDir, test.artifacts.har)
419
+ markdown += ` > HTTP: see [HAR file](../${harPath}) for network requests\n`
420
+ } else if (test.artifacts && test.artifacts.trace) {
421
+ const tracePath = path.relative(reportDir, test.artifacts.trace)
422
+ markdown += ` > HTTP: see [Playwright trace](../${tracePath}) for network requests\n`
423
+ }
424
+ }
425
+
426
+ markdown += `\n`
427
+ })
428
+
429
+ const traceFile = path.join(dir, 'trace.md')
430
+ fs.writeFileSync(traceFile, markdown)
431
+
432
+ output.print(`Trace Saved: file://${traceFile}`)
433
+
434
+ if (!test.artifacts) test.artifacts = {}
435
+ test.artifacts.aiTrace = traceFile
436
+ }
437
+
438
+ function isStepIgnored(step) {
439
+ if (!config.ignoreSteps) return false
440
+ if (!step.title) return false
441
+ for (const pattern of config.ignoreSteps || []) {
442
+ if (step.title.match(pattern)) return true
443
+ }
444
+ return false
445
+ }
446
+
447
+ function generateStepPrefix(step, index) {
448
+ const stepName = step.toString()
449
+ const cleanedName = clearString(stepName)
450
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
451
+ .replace(/_{2,}/g, '_')
452
+ .slice(0, 80)
453
+ .trim()
454
+
455
+ return `${String(index).padStart(4, '0')}_${cleanedName}`
456
+ }
457
+ }
@@ -12,6 +12,7 @@ const ai = aiModule.default || aiModule
12
12
  import colors from 'chalk'
13
13
  import ora from 'ora'
14
14
  import event from '../event.js'
15
+ import recorder from '../recorder.js'
15
16
 
16
17
  import output from '../output.js'
17
18
 
@@ -154,10 +155,9 @@ const defaultConfig = {
154
155
  if (config.vision && test.artifacts.screenshot) {
155
156
  debug('Adding screenshot to prompt')
156
157
  messages[0].content.push({
157
- type: 'image_url',
158
- image_url: {
159
- url: 'data:image/png;base64,' + base64EncodeFile(test.artifacts.screenshot),
160
- },
158
+ type: 'image',
159
+ image: base64EncodeFile(test.artifacts.screenshot),
160
+ mediaType: 'image/png',
161
161
  })
162
162
  }
163
163
 
@@ -227,14 +227,14 @@ export default function (config = {}) {
227
227
  console.log('Enabled AI analysis')
228
228
  })
229
229
 
230
- event.dispatcher.on(event.all.result, async result => {
230
+ event.dispatcher.on(event.all.result, result => {
231
231
  if (!isMainThread) return // run only on main thread
232
232
  if (!ai.isEnabled) {
233
233
  console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
234
234
  return
235
235
  }
236
236
 
237
- printReport(result)
237
+ recorder.add('analyze:print-ai-report', () => printReport(result), true)
238
238
  })
239
239
 
240
240
  event.dispatcher.on(event.workers.result, async result => {
@@ -248,7 +248,7 @@ export default function (config = {}) {
248
248
  return
249
249
  }
250
250
 
251
- printReport(result)
251
+ await printReport(result)
252
252
  })
253
253
 
254
254
  async function printReport(result) {
@@ -294,7 +294,7 @@ export default function (config = {}) {
294
294
  console.error('Error analyzing failed tests', err)
295
295
  }
296
296
 
297
- if (!Object.keys(container.plugins()).includes('pageInfo')) {
297
+ if (!Object.keys(Container.plugins()).includes('pageInfo')) {
298
298
  console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
299
299
  }
300
300
  }
@@ -353,7 +353,7 @@ function serializeError(error) {
353
353
  errorMessage +=
354
354
  '\n' +
355
355
  error.stack
356
- .replace(global.codecept_dir || '', '.')
356
+ .replace(store.codeceptDir || '', '.')
357
357
  .split('\n')
358
358
  .map(line => line.replace(ansiRegExp(), ''))
359
359
  .slice(0, 5)