codeceptjs 4.0.0-rc.9 → 4.0.0

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 (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  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 +743 -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 +198 -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 +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +194 -2
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +63 -70
  126. package/lib/helper/Puppeteer.js +20 -109
  127. package/lib/helper/WebDriver.js +13 -30
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +10 -3
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +7 -0
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +0 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -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
+ }
@@ -1,13 +1,15 @@
1
1
  import path from 'path'
2
2
  import fs from 'fs'
3
3
  import Container from '../container.js'
4
- const supportedHelpers = Container.STANDARD_ACTING_HELPERS
5
4
  import recorder from '../recorder.js'
6
5
  import event from '../event.js'
7
6
  import { scanForErrorMessages } from '../html.js'
7
+ import { captureSnapshot, pickActingHelper } from '../utils/trace.js'
8
8
  import { output } from '../index.js'
9
+ import store from '../store.js'
9
10
  import { humanizeString, ucfirst } from '../utils.js'
10
11
  import { testToFileName } from '../mocha/test.js'
12
+
11
13
  const defaultConfig = {
12
14
  errorClasses: ['error', 'warning', 'alert', 'danger'],
13
15
  browserLogs: ['error'],
@@ -36,67 +38,66 @@ const defaultConfig = {
36
38
  *
37
39
  */
38
40
  export default function (config = {}) {
39
- const helpers = Container.helpers()
40
- let helper
41
-
42
41
  config = Object.assign(defaultConfig, config)
43
42
 
44
- for (const helperName of supportedHelpers) {
45
- if (Object.keys(helpers).indexOf(helperName) > -1) {
46
- helper = helpers[helperName]
47
- }
48
- }
49
-
50
- if (!helper) return // no helpers for screenshot
51
-
52
43
  event.dispatcher.on(event.test.failed, test => {
44
+ const helper = pickActingHelper(Container.helpers())
45
+ if (!helper) return
46
+
53
47
  const pageState = {}
54
48
 
55
- recorder.add('URL of failed test', async () => {
49
+ recorder.add('pageInfo capture', async () => {
56
50
  try {
57
- const url = await helper.grabCurrentUrl()
58
- pageState.url = url
59
- } catch (err) {
60
- // not really needed
61
- }
62
- })
63
- recorder.add('HTML snapshot failed test', async () => {
64
- try {
65
- const html = await helper.grabHTMLFrom('body')
66
-
67
- if (!html) return
68
-
69
- const errors = scanForErrorMessages(html, config.errorClasses)
70
- if (errors.length) {
71
- output.debug('Detected errors in HTML code')
72
- errors.forEach(error => output.debug(error))
73
- pageState.htmlErrors = errors
51
+ const prefix = `${testToFileName(test)}.pageInfo`
52
+ const captured = await captureSnapshot(helper, {
53
+ dir: store.outputDir,
54
+ prefix,
55
+ captureScreenshot: false,
56
+ })
57
+
58
+ if (captured.url) pageState.url = captured.url
59
+
60
+ if (captured.html) {
61
+ const htmlPath = path.join(store.outputDir, captured.html)
62
+ pageState.htmlSnapshot = htmlPath
63
+ const htmlForScan = captured.htmlRaw || (() => {
64
+ try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
65
+ })()
66
+ if (htmlForScan) {
67
+ try {
68
+ const errors = scanForErrorMessages(htmlForScan, config.errorClasses)
69
+ if (errors.length) {
70
+ output.debug('Detected errors in HTML code')
71
+ errors.forEach(error => output.debug(error))
72
+ pageState.htmlErrors = errors
73
+ }
74
+ } catch {}
75
+ }
74
76
  }
75
- } catch (err) {
76
- // not really needed
77
- }
78
- })
79
77
 
80
- recorder.add('Browser logs for failed test', async () => {
81
- try {
82
- const logs = await helper.grabBrowserLogs()
83
-
84
- if (!logs) return
78
+ if (captured.aria) {
79
+ pageState.ariaSnapshot = path.join(store.outputDir, captured.aria)
80
+ }
85
81
 
86
- pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
87
- } catch (err) {
88
- // not really needed
89
- }
90
- })
82
+ if (captured.console) {
83
+ const consolePath = path.join(store.outputDir, captured.console)
84
+ pageState.consoleSnapshot = consolePath
85
+ try {
86
+ const logs = JSON.parse(fs.readFileSync(consolePath, 'utf8'))
87
+ pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
88
+ } catch {}
89
+ }
90
+ } catch {}
91
+ }, true)
91
92
 
92
93
  recorder.add('Save page info', () => {
93
94
  test.addNote('pageInfo', pageStateToMarkdown(pageState))
94
95
 
95
- const pageStateFileName = path.join(global.output_dir, `${testToFileName(test)}.pageInfo.md`)
96
+ const pageStateFileName = path.join(store.outputDir, `${testToFileName(test)}.pageInfo.md`)
96
97
  fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
97
98
  test.artifacts.pageInfo = pageStateFileName
98
99
  return pageState
99
- })
100
+ }, true)
100
101
  })
101
102
  }
102
103
 
@@ -126,15 +127,16 @@ function pageStateToMarkdown(pageState) {
126
127
  }
127
128
 
128
129
  function getBrowserErrors(logs, type = ['error']) {
129
- // Playwright & WebDriver console messages
130
- let errors = logs
130
+ // Accepts Playwright ConsoleMessage objects, normalized {type, text}, or strings
131
+ return logs
131
132
  .map(log => {
132
133
  if (typeof log === 'string') return log
133
- if (!log.type) return null
134
- return { type: log.type(), text: log.text() }
134
+ if (!log) return null
135
+ const t = typeof log.type === 'function' ? log.type() : log.type
136
+ const text = typeof log.text === 'function' ? log.text() : log.text
137
+ if (!t) return null
138
+ return { type: t, text }
135
139
  })
136
140
  .filter(l => l && (typeof l === 'string' || type.includes(l.type)))
137
141
  .map(l => (typeof l === 'string' ? l : l.text))
138
-
139
- return errors
140
142
  }
@@ -0,0 +1,131 @@
1
+ import event from '../event.js'
2
+ import pause from '../pause.js'
3
+ import recorder from '../recorder.js'
4
+ import output from '../output.js'
5
+ import {
6
+ parsePluginArgs,
7
+ resolveTrigger,
8
+ matchStepFile,
9
+ matchUrl,
10
+ getBrowserHelper,
11
+ } from '../utils/pluginParser.js'
12
+
13
+ /**
14
+ * Pauses test execution interactively. Replaces the legacy `pauseOnFail`
15
+ * plugin. The default `on=fail` matches the old `pauseOnFail` behavior.
16
+ *
17
+ * #### Configuration
18
+ *
19
+ * ```js
20
+ * plugins: {
21
+ * pause: {
22
+ * enabled: false,
23
+ * on: 'fail',
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * #### `on=` modes
29
+ *
30
+ * * **fail** — pause when a step fails (default)
31
+ * * **test** — pause after each test
32
+ * * **step** — pause before the first step (interactive walk-through)
33
+ * * **file** — pause when execution reaches `path=...[;line=...]`
34
+ * * **url** — pause when the browser URL matches `pattern=...`
35
+ *
36
+ * CLI examples:
37
+ *
38
+ * ```
39
+ * npx codeceptjs run -p pause
40
+ * npx codeceptjs run -p pause:on=step
41
+ * npx codeceptjs run -p pause:on=file:path=tests/login_test.js;line=43
42
+ * npx codeceptjs run -p pause:on=url:pattern=/users/*
43
+ * ```
44
+ */
45
+ export default function (config = {}) {
46
+ const cliArgs = parsePluginArgs(config._args)
47
+ const trigger = resolveTrigger(cliArgs, config, { on: 'fail' }, { name: 'pause' })
48
+ if (!trigger) return
49
+
50
+ switch (trigger.on) {
51
+ case 'fail':
52
+ return initFailMode()
53
+ case 'test':
54
+ return initTestMode()
55
+ case 'step':
56
+ return initStepMode()
57
+ case 'file':
58
+ return initFileMode(trigger.path, trigger.line)
59
+ case 'url':
60
+ return initUrlMode(trigger.pattern)
61
+ }
62
+ }
63
+
64
+ function initFailMode() {
65
+ let failed = false
66
+
67
+ event.dispatcher.on(event.test.started, () => {
68
+ failed = false
69
+ })
70
+
71
+ event.dispatcher.on(event.step.failed, () => {
72
+ failed = true
73
+ })
74
+
75
+ event.dispatcher.on(event.test.after, () => {
76
+ if (failed) pause()
77
+ })
78
+ }
79
+
80
+ function initTestMode() {
81
+ event.dispatcher.on(event.test.after, () => pause())
82
+ }
83
+
84
+ function initStepMode() {
85
+ let activated = false
86
+
87
+ event.dispatcher.on(event.test.before, () => {
88
+ if (activated) return
89
+ activated = true
90
+ recorder.add('pause:step', () => pause())
91
+ })
92
+ }
93
+
94
+ function initFileMode(targetPath, targetLine) {
95
+ let paused = false
96
+
97
+ event.dispatcher.on(event.step.before, step => {
98
+ if (paused) return
99
+ if (!matchStepFile(step, targetPath, targetLine)) return
100
+ paused = true
101
+ recorder.add('pause:file', () => pause())
102
+ })
103
+ }
104
+
105
+ function initUrlMode(pattern) {
106
+ const helper = getBrowserHelper()
107
+
108
+ if (!helper) {
109
+ output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium)')
110
+ return
111
+ }
112
+
113
+ let paused = false
114
+
115
+ event.dispatcher.on(event.step.after, () => {
116
+ if (paused) return
117
+
118
+ recorder.add('pause:url check', async () => {
119
+ if (paused) return
120
+ try {
121
+ const currentUrl = await helper.grabCurrentUrl()
122
+ if (matchUrl(currentUrl, pattern)) {
123
+ paused = true
124
+ return pause()
125
+ }
126
+ } catch (err) {
127
+ // page may not be loaded yet
128
+ }
129
+ })
130
+ })
131
+ }