codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20

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 (294) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1187 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +159 -0
  7. package/docs/ai.md +537 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/assertions.md +415 -0
  11. package/docs/auth.md +318 -0
  12. package/docs/basics.md +424 -0
  13. package/docs/bdd.md +539 -0
  14. package/docs/best.md +240 -0
  15. package/docs/bootstrap.md +132 -0
  16. package/docs/commands.md +352 -0
  17. package/docs/community-helpers.md +63 -0
  18. package/docs/configuration.md +230 -0
  19. package/docs/continuous-integration.md +497 -0
  20. package/docs/custom-helpers.md +297 -0
  21. package/docs/data.md +448 -0
  22. package/docs/debugging.md +332 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/docker.md +136 -0
  25. package/docs/effects.md +179 -0
  26. package/docs/element-based-testing.md +295 -0
  27. package/docs/element-selection.md +125 -0
  28. package/docs/els.md +328 -0
  29. package/docs/examples.md +161 -0
  30. package/docs/heal.md +213 -0
  31. package/docs/helpers/ApiDataFactory.md +267 -0
  32. package/docs/helpers/Appium.md +1405 -0
  33. package/docs/helpers/Detox.md +665 -0
  34. package/docs/helpers/ExpectHelper.md +275 -0
  35. package/docs/helpers/FileSystem.md +152 -0
  36. package/docs/helpers/GraphQL.md +152 -0
  37. package/docs/helpers/GraphQLDataFactory.md +226 -0
  38. package/docs/helpers/JSONResponse.md +255 -0
  39. package/docs/helpers/Mochawesome.md +8 -0
  40. package/docs/helpers/MockRequest.md +377 -0
  41. package/docs/helpers/MockServer.md +212 -0
  42. package/docs/helpers/Playwright.md +2969 -0
  43. package/docs/helpers/Polly.md +44 -0
  44. package/docs/helpers/Protractor.md +1769 -0
  45. package/docs/helpers/Puppeteer-firefox.md +86 -0
  46. package/docs/helpers/Puppeteer.md +2690 -0
  47. package/docs/helpers/REST.md +289 -0
  48. package/docs/helpers/SoftExpectHelper.md +352 -0
  49. package/docs/helpers/WebDriver.md +2682 -0
  50. package/docs/hooks.md +339 -0
  51. package/docs/index.md +111 -0
  52. package/docs/installation.md +83 -0
  53. package/docs/internal-api.md +265 -0
  54. package/docs/internal-test-server.md +89 -0
  55. package/docs/locators.md +355 -0
  56. package/docs/mcp.md +485 -0
  57. package/docs/migration-4.md +556 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +585 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins.md +866 -0
  63. package/docs/puppeteer.md +314 -0
  64. package/docs/quickstart.md +120 -0
  65. package/docs/react.md +70 -0
  66. package/docs/reports.md +483 -0
  67. package/docs/retry.md +274 -0
  68. package/docs/secrets.md +150 -0
  69. package/docs/sessions.md +80 -0
  70. package/docs/shadow.md +68 -0
  71. package/docs/test-structure.md +275 -0
  72. package/docs/timeouts.md +183 -0
  73. package/docs/translation.md +247 -0
  74. package/docs/tutorial.md +271 -0
  75. package/docs/typescript.md +374 -0
  76. package/docs/web-element.md +251 -0
  77. package/docs/webdriver.md +708 -0
  78. package/docs/within.md +55 -0
  79. package/lib/ai.js +3 -2
  80. package/lib/aria.js +260 -0
  81. package/lib/assertions.js +18 -0
  82. package/lib/codecept.js +26 -23
  83. package/lib/command/check.js +2 -1
  84. package/lib/command/dryRun.js +24 -5
  85. package/lib/command/generate.js +2 -0
  86. package/lib/command/gherkin/snippets.js +5 -4
  87. package/lib/command/init.js +248 -269
  88. package/lib/command/list.js +150 -10
  89. package/lib/command/query.js +218 -0
  90. package/lib/command/run-multiple.js +2 -0
  91. package/lib/command/run-workers.js +2 -0
  92. package/lib/command/run.js +1 -1
  93. package/lib/command/workers/runTests.js +10 -10
  94. package/lib/config.js +77 -4
  95. package/lib/container.js +114 -17
  96. package/lib/effects.js +17 -0
  97. package/lib/element/WebElement.js +246 -2
  98. package/lib/els.js +12 -6
  99. package/lib/globals.js +32 -19
  100. package/lib/heal.js +4 -3
  101. package/lib/helper/ApiDataFactory.js +2 -1
  102. package/lib/helper/Appium.js +8 -8
  103. package/lib/helper/FileSystem.js +3 -2
  104. package/lib/helper/GraphQLDataFactory.js +2 -1
  105. package/lib/helper/Playwright.js +228 -162
  106. package/lib/helper/Puppeteer.js +208 -76
  107. package/lib/helper/WebDriver.js +173 -68
  108. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  109. package/lib/helper/errors/NonFocusedType.js +8 -0
  110. package/lib/helper/extras/Download.js +45 -0
  111. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  112. package/lib/helper/extras/elementSelection.js +58 -0
  113. package/lib/helper/extras/focusCheck.js +43 -0
  114. package/lib/helper/extras/richTextEditor.js +178 -0
  115. package/lib/helper/scripts/dropFile.js +11 -0
  116. package/lib/history.js +3 -2
  117. package/lib/html.js +103 -16
  118. package/lib/index.js +9 -1
  119. package/lib/listener/config.js +6 -4
  120. package/lib/listener/emptyRun.js +2 -1
  121. package/lib/listener/globalRetry.js +32 -6
  122. package/lib/listener/helpers.js +4 -1
  123. package/lib/listener/mocha.js +2 -1
  124. package/lib/listener/pageobjects.js +43 -0
  125. package/lib/listener/result.js +3 -2
  126. package/lib/locator.js +126 -3
  127. package/lib/mocha/cli.js +14 -2
  128. package/lib/mocha/factory.js +7 -2
  129. package/lib/mocha/inject.js +1 -1
  130. package/lib/mocha/scenarioConfig.js +2 -1
  131. package/lib/mocha/ui.js +5 -6
  132. package/lib/parser.js +2 -2
  133. package/lib/pause.js +38 -4
  134. package/lib/plugin/aiTrace.js +453 -0
  135. package/lib/plugin/analyze.js +1 -1
  136. package/lib/plugin/auth.js +3 -3
  137. package/lib/plugin/browser.js +77 -0
  138. package/lib/plugin/expose.js +159 -0
  139. package/lib/plugin/heal.js +44 -1
  140. package/lib/plugin/pageInfo.js +53 -49
  141. package/lib/plugin/pause.js +131 -0
  142. package/lib/plugin/pauseOnFail.js +10 -34
  143. package/lib/plugin/retryFailedStep.js +28 -19
  144. package/lib/plugin/screencast.js +287 -0
  145. package/lib/plugin/screenshot.js +563 -0
  146. package/lib/plugin/screenshotOnFail.js +8 -171
  147. package/lib/rerun.js +2 -1
  148. package/lib/result.js +2 -1
  149. package/lib/step/base.js +3 -2
  150. package/lib/step/config.js +15 -2
  151. package/lib/step/record.js +2 -2
  152. package/lib/store.js +72 -3
  153. package/lib/translation.js +2 -1
  154. package/lib/utils/mask_data.js +2 -1
  155. package/lib/utils/pluginParser.js +151 -0
  156. package/lib/utils/trace.js +297 -0
  157. package/lib/utils.js +77 -3
  158. package/lib/workers.js +52 -22
  159. package/package.json +19 -13
  160. package/typings/index.d.ts +19 -5
  161. package/docs/webapi/amOnPage.mustache +0 -11
  162. package/docs/webapi/appendField.mustache +0 -11
  163. package/docs/webapi/attachFile.mustache +0 -12
  164. package/docs/webapi/blur.mustache +0 -18
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -9
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -29
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  177. package/docs/webapi/dontSeeElement.mustache +0 -8
  178. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  179. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  180. package/docs/webapi/dontSeeInField.mustache +0 -11
  181. package/docs/webapi/dontSeeInSource.mustache +0 -8
  182. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  183. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  184. package/docs/webapi/doubleClick.mustache +0 -13
  185. package/docs/webapi/downloadFile.mustache +0 -12
  186. package/docs/webapi/dragAndDrop.mustache +0 -9
  187. package/docs/webapi/dragSlider.mustache +0 -11
  188. package/docs/webapi/executeAsyncScript.mustache +0 -24
  189. package/docs/webapi/executeScript.mustache +0 -26
  190. package/docs/webapi/fillField.mustache +0 -16
  191. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  192. package/docs/webapi/focus.mustache +0 -13
  193. package/docs/webapi/forceClick.mustache +0 -28
  194. package/docs/webapi/forceRightClick.mustache +0 -18
  195. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  196. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  197. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  198. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  199. package/docs/webapi/grabCookie.mustache +0 -11
  200. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  201. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  202. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  203. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  204. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  205. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  206. package/docs/webapi/grabGeoLocation.mustache +0 -8
  207. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  208. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  209. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  210. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  211. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  212. package/docs/webapi/grabPopupText.mustache +0 -5
  213. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  214. package/docs/webapi/grabSource.mustache +0 -8
  215. package/docs/webapi/grabTextFrom.mustache +0 -10
  216. package/docs/webapi/grabTextFromAll.mustache +0 -9
  217. package/docs/webapi/grabTitle.mustache +0 -8
  218. package/docs/webapi/grabValueFrom.mustache +0 -9
  219. package/docs/webapi/grabValueFromAll.mustache +0 -8
  220. package/docs/webapi/grabWebElement.mustache +0 -9
  221. package/docs/webapi/grabWebElements.mustache +0 -9
  222. package/docs/webapi/moveCursorTo.mustache +0 -12
  223. package/docs/webapi/openNewTab.mustache +0 -7
  224. package/docs/webapi/pressKey.mustache +0 -12
  225. package/docs/webapi/pressKeyDown.mustache +0 -12
  226. package/docs/webapi/pressKeyUp.mustache +0 -12
  227. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  228. package/docs/webapi/refreshPage.mustache +0 -6
  229. package/docs/webapi/resizeWindow.mustache +0 -6
  230. package/docs/webapi/rightClick.mustache +0 -14
  231. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  232. package/docs/webapi/saveScreenshot.mustache +0 -12
  233. package/docs/webapi/say.mustache +0 -10
  234. package/docs/webapi/scrollIntoView.mustache +0 -11
  235. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  236. package/docs/webapi/scrollPageToTop.mustache +0 -6
  237. package/docs/webapi/scrollTo.mustache +0 -12
  238. package/docs/webapi/see.mustache +0 -11
  239. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  240. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  241. package/docs/webapi/seeCookie.mustache +0 -8
  242. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  243. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  244. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  245. package/docs/webapi/seeElement.mustache +0 -8
  246. package/docs/webapi/seeElementInDOM.mustache +0 -8
  247. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  248. package/docs/webapi/seeInField.mustache +0 -12
  249. package/docs/webapi/seeInPopup.mustache +0 -8
  250. package/docs/webapi/seeInSource.mustache +0 -7
  251. package/docs/webapi/seeInTitle.mustache +0 -8
  252. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  253. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  254. package/docs/webapi/seeTextEquals.mustache +0 -9
  255. package/docs/webapi/seeTitleEquals.mustache +0 -8
  256. package/docs/webapi/seeTraffic.mustache +0 -36
  257. package/docs/webapi/selectOption.mustache +0 -21
  258. package/docs/webapi/setCookie.mustache +0 -16
  259. package/docs/webapi/setGeoLocation.mustache +0 -12
  260. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  261. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  262. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  263. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  264. package/docs/webapi/switchTo.mustache +0 -9
  265. package/docs/webapi/switchToNextTab.mustache +0 -10
  266. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  267. package/docs/webapi/type.mustache +0 -21
  268. package/docs/webapi/uncheckOption.mustache +0 -13
  269. package/docs/webapi/wait.mustache +0 -8
  270. package/docs/webapi/waitForClickable.mustache +0 -11
  271. package/docs/webapi/waitForCookie.mustache +0 -9
  272. package/docs/webapi/waitForDetached.mustache +0 -10
  273. package/docs/webapi/waitForDisabled.mustache +0 -6
  274. package/docs/webapi/waitForElement.mustache +0 -11
  275. package/docs/webapi/waitForEnabled.mustache +0 -6
  276. package/docs/webapi/waitForFunction.mustache +0 -17
  277. package/docs/webapi/waitForInvisible.mustache +0 -10
  278. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  279. package/docs/webapi/waitForText.mustache +0 -13
  280. package/docs/webapi/waitForValue.mustache +0 -10
  281. package/docs/webapi/waitForVisible.mustache +0 -10
  282. package/docs/webapi/waitInUrl.mustache +0 -9
  283. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  284. package/docs/webapi/waitToHide.mustache +0 -10
  285. package/docs/webapi/waitUrlEquals.mustache +0 -10
  286. package/lib/helper/AI.js +0 -214
  287. package/lib/listener/enhancedGlobalRetry.js +0 -110
  288. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  289. package/lib/plugin/htmlReporter.js +0 -3648
  290. package/lib/plugin/stepByStepReport.js +0 -427
  291. package/lib/plugin/subtitles.js +0 -89
  292. package/lib/retryCoordinator.js +0 -207
  293. package/typings/promiseBasedTypes.d.ts +0 -9469
  294. package/typings/types.d.ts +0 -11402
@@ -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
@@ -65,6 +91,8 @@ export default function (config = {}) {
65
91
 
66
92
  if (!heal.hasCorrespondingRecipes(step)) return
67
93
 
94
+ if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
95
+
68
96
  recorder.catchWithoutStop(async err => {
69
97
  isHealing = true
70
98
  if (caughtError === err) throw err // avoid double handling
@@ -72,6 +100,21 @@ export default function (config = {}) {
72
100
 
73
101
  const test = currentTest
74
102
 
103
+ if (trigger.on === 'url') {
104
+ try {
105
+ const helper = getBrowserHelper()
106
+ const url = helper && helper.grabCurrentUrl ? await helper.grabCurrentUrl() : null
107
+ if (!matchUrl(url, trigger.pattern)) {
108
+ isHealing = false
109
+ throw err
110
+ }
111
+ } catch (e) {
112
+ if (e === err) throw e
113
+ isHealing = false
114
+ throw err
115
+ }
116
+ }
117
+
75
118
  recorder.session.start('heal')
76
119
 
77
120
  debug('Self-healing started', step.toCode())
@@ -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,63 +38,64 @@ 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
43
+ const helper = pickActingHelper(Container.helpers())
44
+ if (!helper) return
51
45
 
52
46
  event.dispatcher.on(event.test.failed, test => {
53
47
  const pageState = {}
54
48
 
55
- recorder.add('URL of failed test', async () => {
56
- 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 () => {
49
+ recorder.add('pageInfo capture', async () => {
64
50
  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
+ // Scan raw HTML (pre-cleanHtml) so error classes containing digits
64
+ // or trash-class prefixes aren't stripped before detection.
65
+ const htmlForScan = captured.htmlRaw || (() => {
66
+ try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
67
+ })()
68
+ if (htmlForScan) {
69
+ try {
70
+ const errors = scanForErrorMessages(htmlForScan, config.errorClasses)
71
+ if (errors.length) {
72
+ output.debug('Detected errors in HTML code')
73
+ errors.forEach(error => output.debug(error))
74
+ pageState.htmlErrors = errors
75
+ }
76
+ } catch {}
77
+ }
74
78
  }
75
- } catch (err) {
76
- // not really needed
77
- }
78
- })
79
-
80
- recorder.add('Browser logs for failed test', async () => {
81
- try {
82
- const logs = await helper.grabBrowserLogs()
83
79
 
84
- if (!logs) return
80
+ if (captured.aria) {
81
+ pageState.ariaSnapshot = path.join(store.outputDir, captured.aria)
82
+ }
85
83
 
86
- pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
87
- } catch (err) {
88
- // not really needed
89
- }
84
+ if (captured.console) {
85
+ const consolePath = path.join(store.outputDir, captured.console)
86
+ pageState.consoleSnapshot = consolePath
87
+ try {
88
+ const logs = JSON.parse(fs.readFileSync(consolePath, 'utf8'))
89
+ pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
90
+ } catch {}
91
+ }
92
+ } catch {}
90
93
  })
91
94
 
92
95
  recorder.add('Save page info', () => {
93
96
  test.addNote('pageInfo', pageStateToMarkdown(pageState))
94
97
 
95
- const pageStateFileName = path.join(global.output_dir, `${testToFileName(test)}.pageInfo.md`)
98
+ const pageStateFileName = path.join(store.outputDir, `${testToFileName(test)}.pageInfo.md`)
96
99
  fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
97
100
  test.artifacts.pageInfo = pageStateFileName
98
101
  return pageState
@@ -126,15 +129,16 @@ function pageStateToMarkdown(pageState) {
126
129
  }
127
130
 
128
131
  function getBrowserErrors(logs, type = ['error']) {
129
- // Playwright & WebDriver console messages
130
- let errors = logs
132
+ // Accepts Playwright ConsoleMessage objects, normalized {type, text}, or strings
133
+ return logs
131
134
  .map(log => {
132
135
  if (typeof log === 'string') return log
133
- if (!log.type) return null
134
- return { type: log.type(), text: log.text() }
136
+ if (!log) return null
137
+ const t = typeof log.type === 'function' ? log.type() : log.type
138
+ const text = typeof log.text === 'function' ? log.text() : log.text
139
+ if (!t) return null
140
+ return { type: t, text }
135
141
  })
136
142
  .filter(l => l && (typeof l === 'string' || type.includes(l.type)))
137
143
  .map(l => (typeof l === 'string' ? l : l.text))
138
-
139
- return errors
140
144
  }
@@ -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
+ }
@@ -1,39 +1,15 @@
1
- import event from '../event.js'
1
+ import output from '../output.js'
2
+ import pause from './pause.js'
2
3
 
3
- import pause from '../pause.js'
4
+ let warned = false
4
5
 
5
6
  /**
6
- * Automatically launches [interactive pause](/basics/#pause) when a test fails.
7
- *
8
- * Useful for debugging flaky tests on local environment.
9
- * Add this plugin to config file:
10
- *
11
- * ```js
12
- * plugins: {
13
- * pauseOnFail: {},
14
- * }
15
- * ```
16
- *
17
- * Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default.
18
- * Enable it manually on each run via `-p` option:
19
- *
20
- * ```
21
- * npx codeceptjs run -p pauseOnFail
22
- * ```
23
- *
7
+ * @deprecated Use the `pause` plugin with `on: 'fail'` (the default).
24
8
  */
25
- export default function() {
26
- let failed = false
27
-
28
- event.dispatcher.on(event.test.started, () => {
29
- failed = false
30
- })
31
-
32
- event.dispatcher.on(event.step.failed, () => {
33
- failed = true
34
- })
35
-
36
- event.dispatcher.on(event.test.after, () => {
37
- if (failed) pause()
38
- })
9
+ export default function (config = {}) {
10
+ if (!warned) {
11
+ output.error('pauseOnFail is deprecated; use the `pause` plugin (default on=fail).')
12
+ warned = true
13
+ }
14
+ return pause({ ...config, on: 'fail' })
39
15
  }