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
@@ -321,7 +321,7 @@ export default function (config) {
321
321
  }
322
322
  if (config.saveToFile) {
323
323
  output.debug(`Saved user session into file for ${name}`)
324
- fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies))
324
+ fs.writeFileSync(path.join(store.outputDir, `${name}_session.json`), JSON.stringify(cookies))
325
325
  }
326
326
  store[`${name}_session`] = cookies
327
327
  }
@@ -350,7 +350,8 @@ export default function (config) {
350
350
  recorder.session.restore('auto login')
351
351
  recorder.session.restore('check login')
352
352
  section.end()
353
- recorder.throw(err)
353
+ // Use regular throw instead of recorder.throw to avoid promise chaining cycle
354
+ throw err
354
355
  })
355
356
  })
356
357
  recorder.add(() => {
@@ -376,7 +377,7 @@ export default function (config) {
376
377
  }
377
378
 
378
379
  if (!config.saveToFile) return
379
- const cookieFile = path.join(global.output_dir, `${name}_session.json`)
380
+ const cookieFile = path.join(store.outputDir, `${name}_session.json`)
380
381
 
381
382
  if (!fileExists(cookieFile)) {
382
383
  return
@@ -411,7 +412,7 @@ export default function (config) {
411
412
 
412
413
  function loadCookiesFromFile(config) {
413
414
  for (const name in config.users) {
414
- const fileName = path.join(global.output_dir, `${name}_session.json`)
415
+ const fileName = path.join(store.outputDir, `${name}_session.json`)
415
416
  if (!fileExists(fileName)) continue
416
417
  const data = fs.readFileSync(fileName).toString()
417
418
  try {
@@ -0,0 +1,77 @@
1
+ import output from '../output.js'
2
+
3
+ /**
4
+ * Overrides browser helper config from the command line. Works for all browser helpers
5
+ * (Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
6
+ *
7
+ * Enable it via `-p` option with one or more colon-chained args:
8
+ *
9
+ * ```
10
+ * npx codeceptjs run -p browser:show
11
+ * npx codeceptjs run -p browser:hide
12
+ * npx codeceptjs run -p browser:browser=firefox
13
+ * npx codeceptjs run -p browser:windowSize=1024x768:video=false
14
+ * npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
15
+ * ```
16
+ *
17
+ * #### Args
18
+ *
19
+ * * **show** — force visible browser
20
+ * * **hide** — force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
21
+ * * **`<key>=<value>`** — set `helpers.<eachBrowserHelper>.<key> = <value>`. Three keys
22
+ * get per-helper translation via `setBrowserConfig`:
23
+ * * `browser=<name>` — Puppeteer receives `product`, Playwright receives `browser`
24
+ * * `windowSize=WxH` — also adds `--window-size=W,H` chromium/chrome args
25
+ * * `show=true|false` — toggles `show` on Playwright/Puppeteer; injects/strips
26
+ * `--headless` in WebDriver chrome/firefox capability args
27
+ *
28
+ * Values stay as strings. `true` / `false` are coerced to booleans.
29
+ *
30
+ * Requires `@codeceptjs/configure` to be installed; if missing, the plugin
31
+ * logs a hint and skips the override.
32
+ */
33
+ export default async function (config = {}) {
34
+ const { _args, enabled, ...rest } = config
35
+ const opts = { ...rest, ...parseArgs(_args || []) }
36
+ if (Object.keys(opts).length === 0) return
37
+
38
+ const configure = await tryImportConfigure()
39
+ if (!configure) return
40
+
41
+ configure.setBrowserConfig(opts)
42
+ output.debug(`browser plugin: applied ${formatOpts(opts)}`)
43
+ }
44
+
45
+ async function tryImportConfigure() {
46
+ try {
47
+ return await import('@codeceptjs/configure')
48
+ } catch (err) {
49
+ output.error("browser plugin: '@codeceptjs/configure' is not installed; CLI overrides are skipped. Run `npm i @codeceptjs/configure` to enable.")
50
+ return null
51
+ }
52
+ }
53
+
54
+ function parseArgs(args) {
55
+ return args.filter(Boolean).reduce((acc, arg) => Object.assign(acc, parseArg(arg)), {})
56
+ }
57
+
58
+ function parseArg(arg) {
59
+ if (arg === 'show') return { show: true }
60
+ if (arg === 'hide') return { show: false }
61
+ if (arg.includes('=')) {
62
+ const [key, ...rest] = arg.split('=')
63
+ return { [key]: parseValue(rest.join('=')) }
64
+ }
65
+ output.error(`browser plugin: unknown arg "${arg}"`)
66
+ return {}
67
+ }
68
+
69
+ function parseValue(v) {
70
+ if (v === 'true') return true
71
+ if (v === 'false') return false
72
+ return v
73
+ }
74
+
75
+ function formatOpts(opts) {
76
+ return Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
77
+ }
@@ -0,0 +1,159 @@
1
+ import Container from '../container.js'
2
+
3
+ const RESERVED_NAMES = new Set(['I', 'test', 'suite'])
4
+ const SHORTHAND_PROPERTIES = new Set(['page', 'browser', 'browserContext', 'context'])
5
+
6
+ const defaultConfig = {
7
+ inject: {},
8
+ }
9
+
10
+ /**
11
+ * Exposes properties from helper instances as injectable test arguments.
12
+ * Use it to access the underlying Playwright/Puppeteer `page`, the wdio `browser` client,
13
+ * or any other helper internal directly from a Scenario:
14
+ *
15
+ * ```js
16
+ * Scenario('listen for requests', async ({ I, page, browser }) => {
17
+ * page.on('request', r => console.log(r.url()))
18
+ * await page.evaluate(() => 1 + 1)
19
+ * I.amOnPage('/')
20
+ * })
21
+ * ```
22
+ *
23
+ * The injected value is a live proxy: every property access reads the *current*
24
+ * helper property, so mid-test reassignments (popups, `switchToNextTab`,
25
+ * `openNewTab`) are reflected automatically. Calls are not wrapped as
26
+ * CodeceptJS steps — `await page.evaluate(...)` runs as native Playwright.
27
+ *
28
+ * #### Configuration
29
+ *
30
+ * `inject` maps an injection name to a `HelperName.propertyName` string. A
31
+ * value with no dot is shorthand for "first configured browser helper that
32
+ * exposes this property" (allowed properties: `page`, `browser`,
33
+ * `browserContext`, `context`).
34
+ *
35
+ * ```js
36
+ * plugins: {
37
+ * expose: {
38
+ * enabled: true,
39
+ * inject: {
40
+ * page: 'Playwright.page',
41
+ * browser: 'Playwright.browser',
42
+ * browserContext: 'Playwright.browserContext',
43
+ * frame: 'Playwright.context', // current frame set by switchTo
44
+ * wdio: 'WebDriver.browser',
45
+ * }
46
+ * }
47
+ * }
48
+ * ```
49
+ *
50
+ * Shorthand:
51
+ *
52
+ * ```js
53
+ * plugins: {
54
+ * expose: {
55
+ * enabled: true,
56
+ * inject: {
57
+ * page: 'page', // resolves to Playwright.page or Puppeteer.page
58
+ * }
59
+ * }
60
+ * }
61
+ * ```
62
+ *
63
+ * #### Caveats
64
+ *
65
+ * - The injected value is a `Proxy`, not the actual `Page`/`Browser` instance,
66
+ * so `page instanceof Page` is `false`. Use duck typing instead.
67
+ * - Cached method references lose the live binding. Call `page.click(...)`,
68
+ * not `const click = page.click; click(...)`.
69
+ * - In dry-run mode the underlying helper property is `undefined`; accessing
70
+ * any property on the proxy returns `undefined` rather than throwing.
71
+ */
72
+ export default function (config = {}) {
73
+ config = { ...defaultConfig, ...config }
74
+
75
+ const mappings = parseMappings(config.inject)
76
+
77
+ const support = {}
78
+ for (const [name, { helperName, property }] of Object.entries(mappings)) {
79
+ support[name] = makeLiveProxy(helperName, property)
80
+ }
81
+ Container.append({ support })
82
+ }
83
+
84
+ function parseMappings(inject) {
85
+ const out = {}
86
+ for (const [name, value] of Object.entries(inject || {})) {
87
+ if (RESERVED_NAMES.has(name)) {
88
+ throw new Error(`expose plugin: inject name '${name}' is reserved`)
89
+ }
90
+ if (typeof value !== 'string' || !value) {
91
+ throw new Error(`expose plugin: inject value for '${name}' must be a non-empty string`)
92
+ }
93
+
94
+ let helperName
95
+ let property
96
+
97
+ if (value.includes('.')) {
98
+ const dot = value.indexOf('.')
99
+ helperName = value.slice(0, dot)
100
+ property = value.slice(dot + 1)
101
+ if (!helperName || !property) {
102
+ throw new Error(`expose plugin: invalid inject value '${value}' for '${name}' (expected 'HelperName.propertyName')`)
103
+ }
104
+ if (!Container.helpers(helperName)) {
105
+ throw new Error(`expose plugin: helper '${helperName}' is not configured (needed for inject '${name}')`)
106
+ }
107
+ } else {
108
+ property = value
109
+ if (!SHORTHAND_PROPERTIES.has(property)) {
110
+ throw new Error(`expose plugin: shorthand '${property}' is not a known helper property for '${name}' (use 'HelperName.${property}' instead)`)
111
+ }
112
+ helperName = Container.STANDARD_ACTING_HELPERS.find(h => Container.helpers(h))
113
+ if (!helperName) {
114
+ throw new Error(`expose plugin: no standard browser helper configured (needed for inject '${name}')`)
115
+ }
116
+ }
117
+
118
+ out[name] = { helperName, property }
119
+ }
120
+ return out
121
+ }
122
+
123
+ function makeLiveProxy(helperName, property) {
124
+ const resolve = () => Container.helpers(helperName)?.[property]
125
+ return new Proxy(function () {}, {
126
+ get(_, prop) {
127
+ const target = resolve()
128
+ if (target == null) return undefined
129
+ const value = target[prop]
130
+ if (typeof value === 'function') return value.bind(target)
131
+ return value
132
+ },
133
+ has(_, prop) {
134
+ const target = resolve()
135
+ return target != null && prop in target
136
+ },
137
+ apply(_, thisArg, args) {
138
+ const target = resolve()
139
+ return target?.apply(thisArg, args)
140
+ },
141
+ set(_, prop, value) {
142
+ const target = resolve()
143
+ if (target != null) target[prop] = value
144
+ return true
145
+ },
146
+ getPrototypeOf() {
147
+ const target = resolve()
148
+ return target != null ? Object.getPrototypeOf(target) : null
149
+ },
150
+ ownKeys() {
151
+ const target = resolve()
152
+ return target != null ? Reflect.ownKeys(target) : []
153
+ },
154
+ getOwnPropertyDescriptor(_, prop) {
155
+ const target = resolve()
156
+ return target != null ? Object.getOwnPropertyDescriptor(target, prop) : undefined
157
+ },
158
+ })
159
+ }
@@ -10,21 +10,30 @@ import output from '../output.js'
10
10
  import healModule from '../heal.js'
11
11
  const heal = healModule.default || healModule
12
12
  import store from '../store.js'
13
+ import {
14
+ parsePluginArgs,
15
+ resolveTrigger,
16
+ matchStepFile,
17
+ matchUrl,
18
+ getBrowserHelper,
19
+ } from '../utils/pluginParser.js'
13
20
 
14
21
 
15
22
  const defaultConfig = {
23
+ on: 'fail',
16
24
  healLimit: 2,
17
25
  }
18
26
 
19
27
  /**
20
28
  * Self-healing tests with AI.
21
29
  *
22
- * Read more about heaking in [Self-Healing Tests](https://codecept.io/heal/)
30
+ * Read more about healing in [Self-Healing Tests](https://codecept.io/heal/)
23
31
  *
24
32
  * ```js
25
33
  * plugins: {
26
34
  * heal: {
27
35
  * enabled: true,
36
+ * on: 'fail',
28
37
  * }
29
38
  * }
30
39
  * ```
@@ -32,7 +41,17 @@ const defaultConfig = {
32
41
  * More config options are available:
33
42
  *
34
43
  * * `healLimit` - how many steps can be healed in a single test (default: 2)
44
+ * * `on` - trigger mode. `fail` (default), `file` (filter to a path), `url` (filter to a URL pattern).
35
45
  *
46
+ * #### `on=` modes
47
+ *
48
+ * Heal always runs on step failures; `on=` narrows when it engages.
49
+ *
50
+ * * **fail** — heal any failing step (default)
51
+ * * **file** — heal only failures in `path=...[;line=...]`
52
+ * * **url** — heal only failures when the current URL matches `pattern=...`
53
+ *
54
+ * `on=step` and `on=test` are not supported and are rejected with an error.
36
55
  */
37
56
  export default function (config = {}) {
38
57
  if (store.debugMode && !process.env.DEBUG) {
@@ -42,6 +61,13 @@ export default function (config = {}) {
42
61
  return
43
62
  }
44
63
 
64
+ const cliArgs = parsePluginArgs(config._args)
65
+ const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, {
66
+ name: 'heal',
67
+ validModes: ['fail', 'file', 'url'],
68
+ })
69
+ if (!trigger) return
70
+
45
71
  let currentTest = null
46
72
  let currentStep = null
47
73
  let healedSteps = 0
@@ -54,6 +80,7 @@ export default function (config = {}) {
54
80
  event.dispatcher.on(event.test.before, test => {
55
81
  currentTest = test
56
82
  healedSteps = 0
83
+ healTries = 0
57
84
  caughtError = null
58
85
  })
59
86
 
@@ -65,21 +92,38 @@ export default function (config = {}) {
65
92
 
66
93
  if (!heal.hasCorrespondingRecipes(step)) return
67
94
 
95
+ if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
96
+
68
97
  recorder.catchWithoutStop(async err => {
98
+ if (healTries >= config.healLimit) throw err
69
99
  isHealing = true
100
+ healTries++
70
101
  if (caughtError === err) throw err // avoid double handling
71
102
  caughtError = err
72
103
 
73
104
  const test = currentTest
74
105
 
106
+ if (trigger.on === 'url') {
107
+ try {
108
+ const helper = getBrowserHelper()
109
+ const url = helper && helper.grabCurrentUrl ? await helper.grabCurrentUrl() : null
110
+ if (!matchUrl(url, trigger.pattern)) {
111
+ isHealing = false
112
+ throw err
113
+ }
114
+ } catch (e) {
115
+ if (e === err) throw e
116
+ isHealing = false
117
+ throw err
118
+ }
119
+ }
120
+
75
121
  recorder.session.start('heal')
76
122
 
77
123
  debug('Self-healing started', step.toCode())
78
124
 
79
125
  await heal.healStep(step, err, { test })
80
126
 
81
- healTries++
82
-
83
127
  recorder.add('close healing session', () => {
84
128
  recorder.reset()
85
129
  recorder.session.restore('heal')
@@ -0,0 +1,303 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { mkdirp } from 'mkdirp'
5
+ import { DOMImplementation, XMLSerializer } from '@xmldom/xmldom'
6
+
7
+ import event from '../event.js'
8
+ import store from '../store.js'
9
+ import output from '../output.js'
10
+
11
+ const defaultConfig = {
12
+ outputName: 'report.xml',
13
+ output: null,
14
+ testGroupName: 'CodeceptJS',
15
+ attachSteps: true,
16
+ attachMeta: true,
17
+ stepsInFailure: true,
18
+ }
19
+
20
+ const INVALID_XML_CHARS = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\uFFFE\\uFFFF]', 'g')
21
+
22
+ /**
23
+ *
24
+ * Generates a JUnit-compatible XML report after a test run.
25
+ *
26
+ * Unlike Mocha's `mocha-junit-reporter`, this plugin understands CodeceptJS steps and substeps.
27
+ * For every `<testcase>` it includes:
28
+ *
29
+ * * `<properties>` — the test's meta information: every `meta` key from `Scenario('...', { meta })`, plus its `tags` and `retries`
30
+ * * `<system-out>` — an indented step/substep log (substeps are nested under their meta step); only failed steps are marked
31
+ * * `<failure>` — for failed tests: the error message, type, stack trace and (optionally) the step trace
32
+ *
33
+ * The produced file is consumable by Jenkins, GitLab CI, CircleCI, GitHub Actions test reporters, etc.
34
+ *
35
+ * #### Configuration
36
+ *
37
+ * ```js
38
+ * "plugins": {
39
+ * "junitReporter": {
40
+ * "enabled": true
41
+ * }
42
+ * }
43
+ * ```
44
+ *
45
+ * Possible config options:
46
+ *
47
+ * * `outputName`: file name for the report. Default: `report.xml`.
48
+ * * `output`: directory where the report is stored, relative to the project root. Default: the `output` directory.
49
+ * * `testGroupName`: value of the `name` attribute on the root `<testsuites>` element. Default: `CodeceptJS`.
50
+ * * `attachMeta`: add the test's meta information (`meta` keys, `tags`, `retries`) as `<properties>`. Default: true.
51
+ * * `attachSteps`: add the step/substep log as `<system-out>`. Default: true.
52
+ * * `stepsInFailure`: append the step trace to the `<failure>` body. Default: true.
53
+ *
54
+ * CLI examples:
55
+ *
56
+ * ```
57
+ * npx codeceptjs run -p junitReporter
58
+ * npx codeceptjs run -p junitReporter:outputName=junit.xml
59
+ * ```
60
+ *
61
+ * > ℹ When running with `run-workers`, steps are serialized between processes and substep nesting is flattened.
62
+ *
63
+ * @param {*} config
64
+ */
65
+ export default function (config = {}) {
66
+ config = Object.assign({}, defaultConfig, config)
67
+
68
+ let written = false
69
+
70
+ const writeReport = result => {
71
+ if (written) return
72
+ if (!result || !Array.isArray(result.tests)) return
73
+ written = true
74
+
75
+ const dir = config.output ? path.resolve(store.codeceptDir || process.cwd(), config.output) : store.outputDir || process.cwd()
76
+ mkdirp.sync(dir)
77
+ const file = path.join(dir, config.outputName)
78
+
79
+ fs.writeFileSync(file, buildXml(result, config))
80
+ output.plugin('junitReporter', `JUnit report saved to ${file}`)
81
+ }
82
+
83
+ event.dispatcher.on(event.all.result, writeReport)
84
+ event.dispatcher.on(event.workers.result, writeReport)
85
+ }
86
+
87
+ function buildXml(result, config) {
88
+ const doc = new DOMImplementation().createDocument(null, null, null)
89
+ const suites = groupBySuite(result.tests)
90
+
91
+ const root = doc.createElement('testsuites')
92
+ setAttr(root, 'name', config.testGroupName)
93
+ setAttr(root, 'tests', result.tests.length)
94
+ setAttr(root, 'failures', countState(result.tests, 'failed'))
95
+ setAttr(root, 'skipped', countSkipped(result.tests))
96
+ setAttr(root, 'errors', 0)
97
+ setAttr(root, 'time', toSeconds(sumDuration(result.tests)))
98
+ setAttr(root, 'timestamp', toIso(result.stats && result.stats.start))
99
+ doc.appendChild(root)
100
+
101
+ suites.forEach((tests, index) => {
102
+ const suite = tests[0] && tests[0].parent
103
+ const suiteName = (suite && suite.title) || 'Tests'
104
+ const suiteFile = (suite && suite.file) || (tests[0] && tests[0].file) || ''
105
+
106
+ const suiteEl = doc.createElement('testsuite')
107
+ setAttr(suiteEl, 'name', suiteName)
108
+ setAttr(suiteEl, 'id', index)
109
+ setAttr(suiteEl, 'tests', tests.length)
110
+ setAttr(suiteEl, 'failures', countState(tests, 'failed'))
111
+ setAttr(suiteEl, 'skipped', countSkipped(tests))
112
+ setAttr(suiteEl, 'errors', 0)
113
+ setAttr(suiteEl, 'time', toSeconds(sumDuration(tests)))
114
+ setAttr(suiteEl, 'timestamp', toIso(suite && suite.startedAt))
115
+ setAttr(suiteEl, 'hostname', os.hostname())
116
+ if (suiteFile) setAttr(suiteEl, 'file', suiteFile)
117
+ root.appendChild(suiteEl)
118
+
119
+ for (const test of tests) {
120
+ suiteEl.appendChild(buildTestCase(doc, test, suiteName, config))
121
+ }
122
+ })
123
+
124
+ return '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(doc) + '\n'
125
+ }
126
+
127
+ function buildTestCase(doc, test, suiteName, config) {
128
+ const testEl = doc.createElement('testcase')
129
+ setAttr(testEl, 'name', test.title || '(no title)')
130
+ setAttr(testEl, 'classname', suiteName)
131
+ setAttr(testEl, 'time', toSeconds(test.duration || 0))
132
+ const file = test.file || (test.parent && test.parent.file)
133
+ if (file) setAttr(testEl, 'file', file)
134
+
135
+ if (config.attachMeta) {
136
+ const properties = metaProperties(test)
137
+ if (properties.length) {
138
+ const propertiesEl = doc.createElement('properties')
139
+ for (const [name, value] of properties) {
140
+ const prop = doc.createElement('property')
141
+ setAttr(prop, 'name', name)
142
+ setAttr(prop, 'value', value)
143
+ propertiesEl.appendChild(prop)
144
+ }
145
+ testEl.appendChild(propertiesEl)
146
+ }
147
+ }
148
+
149
+ const flat = flattenSteps(Array.isArray(test.steps) ? test.steps : [])
150
+
151
+ if (test.state === 'skipped' || test.state === 'pending') {
152
+ const skipped = doc.createElement('skipped')
153
+ const reason = skipReason(test)
154
+ if (reason) setAttr(skipped, 'message', reason)
155
+ testEl.appendChild(skipped)
156
+ } else if (test.state === 'failed') {
157
+ const err = test.err || {}
158
+ const failure = doc.createElement('failure')
159
+ setAttr(failure, 'message', err.message || 'Test failed')
160
+ setAttr(failure, 'type', err.name || 'Error')
161
+ let body = err.stack || err.message || 'Test failed'
162
+ if (config.stepsInFailure && flat.length) {
163
+ body += '\n\nSteps:\n' + flat.map(stepLogLine).join('\n')
164
+ }
165
+ failure.appendChild(doc.createTextNode(cleanText(body)))
166
+ testEl.appendChild(failure)
167
+ }
168
+
169
+ if (config.attachSteps && flat.length) {
170
+ const out = doc.createElement('system-out')
171
+ out.appendChild(doc.createTextNode(cleanText(flat.map(stepLogLine).join('\n'))))
172
+ testEl.appendChild(out)
173
+ }
174
+
175
+ return testEl
176
+ }
177
+
178
+ function metaProperties(test) {
179
+ const props = []
180
+ const meta = test.meta || {}
181
+ for (const key of Object.keys(meta)) {
182
+ if (meta[key] === undefined || meta[key] === null) continue
183
+ props.push([key, stringifyMeta(meta[key])])
184
+ }
185
+ if (Array.isArray(test.tags) && test.tags.length) {
186
+ props.push(['tags', test.tags.join(' ')])
187
+ }
188
+ if (test.retries > 0 || test.retryNum > 0) {
189
+ props.push(['retries', String(test.retryNum || test.retries)])
190
+ }
191
+ return props
192
+ }
193
+
194
+ function stringifyMeta(value) {
195
+ if (typeof value === 'string') return value
196
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
197
+ try {
198
+ return JSON.stringify(value)
199
+ } catch (err) {
200
+ return String(value)
201
+ }
202
+ }
203
+
204
+ function flattenSteps(steps) {
205
+ const out = []
206
+ let prevChain = []
207
+
208
+ for (const step of steps) {
209
+ const chain = metaChain(step)
210
+
211
+ let common = 0
212
+ while (common < chain.length && common < prevChain.length && chain[common].key === prevChain[common].key) common++
213
+
214
+ for (let d = common; d < chain.length; d++) {
215
+ out.push({ depth: d, step: chain[d].step })
216
+ }
217
+ out.push({ depth: chain.length, step })
218
+ prevChain = chain
219
+ }
220
+
221
+ return out
222
+ }
223
+
224
+ function metaChain(step) {
225
+ const chain = []
226
+ let meta = step && step.metaStep
227
+ while (meta) {
228
+ chain.unshift({ step: meta, key: meta })
229
+ meta = meta.metaStep
230
+ }
231
+ if (!chain.length && step && step.parent && step.parent.title) {
232
+ chain.push({ step: { title: step.parent.title, status: step.status }, key: `meta:${step.parent.title}` })
233
+ }
234
+ return chain
235
+ }
236
+
237
+ function stepLogLine(entry) {
238
+ const indent = ' '.repeat(entry.depth)
239
+ const mark = entry.step && entry.step.status === 'failed' ? '[FAILED] ' : ''
240
+ return `${indent}${mark}${stepText(entry.step)} (${stepDuration(entry.step)}ms)`
241
+ }
242
+
243
+ function stepText(step) {
244
+ if (step && typeof step.toString === 'function' && step.toString !== Object.prototype.toString) return step.toString()
245
+ return (step && step.title) || 'step'
246
+ }
247
+
248
+ function stepDuration(step) {
249
+ if (!step) return 0
250
+ if (typeof step.duration === 'number' && step.duration >= 0) return step.duration
251
+ if (step.startTime && step.endTime) return Math.max(0, step.endTime - step.startTime)
252
+ return 0
253
+ }
254
+
255
+ function groupBySuite(tests) {
256
+ const groups = []
257
+ const byKey = new Map()
258
+ for (const test of tests) {
259
+ const key = test.parent || test
260
+ if (!byKey.has(key)) {
261
+ const list = []
262
+ byKey.set(key, list)
263
+ groups.push(list)
264
+ }
265
+ byKey.get(key).push(test)
266
+ }
267
+ return groups
268
+ }
269
+
270
+ function skipReason(test) {
271
+ if (test.opts && test.opts.skipInfo && test.opts.skipInfo.message) return test.opts.skipInfo.message
272
+ if (test.meta && test.meta.skipReason) return test.meta.skipReason
273
+ return ''
274
+ }
275
+
276
+ function countState(tests, state) {
277
+ return tests.filter(t => t.state === state).length
278
+ }
279
+
280
+ function countSkipped(tests) {
281
+ return tests.filter(t => t.state === 'skipped' || t.state === 'pending').length
282
+ }
283
+
284
+ function sumDuration(tests) {
285
+ return tests.reduce((sum, t) => sum + (t.duration || 0), 0)
286
+ }
287
+
288
+ function toSeconds(ms) {
289
+ return (Math.max(0, ms) / 1000).toFixed(3)
290
+ }
291
+
292
+ function toIso(value) {
293
+ const date = value ? new Date(value) : new Date()
294
+ return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString()
295
+ }
296
+
297
+ function cleanText(text) {
298
+ return String(text == null ? '' : text).replace(INVALID_XML_CHARS, '')
299
+ }
300
+
301
+ function setAttr(el, name, value) {
302
+ el.setAttribute(name, cleanText(value))
303
+ }