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
@@ -1,178 +1,15 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
-
4
- import Container from '../container.js'
5
-
6
- import recorder from '../recorder.js'
7
-
8
- import event from '../event.js'
9
-
10
1
  import output from '../output.js'
2
+ import screenshot from './screenshot.js'
11
3
 
12
- import { fileExists } from '../utils.js'
13
- import Codeceptjs from '../index.js'
14
- import { testToFileName } from '../mocha/test.js'
15
-
16
- const defaultConfig = {
17
- uniqueScreenshotNames: false,
18
- disableScreenshots: false,
19
- fullPageScreenshots: false,
20
- }
21
-
22
- const supportedHelpers = Container.STANDARD_ACTING_HELPERS
4
+ let warned = false
23
5
 
24
6
  /**
25
- * Creates screenshot on failure. Screenshot is saved into `output` directory.
26
- *
27
- * Initially this functionality was part of corresponding helper but has been moved into plugin since 1.4
28
- *
29
- * This plugin is **enabled by default**.
30
- *
31
- * #### Configuration
32
- *
33
- * Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).
34
- *
35
- * ```js
36
- * plugins: {
37
- * screenshotOnFail: {
38
- * enabled: true
39
- * }
40
- * }
41
- * ```
42
- *
43
- * Possible config options:
44
- *
45
- * * `uniqueScreenshotNames`: use unique names for screenshot. Default: false.
46
- * * `fullPageScreenshots`: make full page screenshots. Default: false.
47
- *
48
- *
7
+ * @deprecated Use the `screenshot` plugin with `on: 'fail'` (the default).
49
8
  */
50
- export default function (config) {
51
- const helpers = Container.helpers()
52
- let helper
53
-
54
- for (const helperName of supportedHelpers) {
55
- if (Object.keys(helpers).indexOf(helperName) > -1) {
56
- helper = helpers[helperName]
57
- }
58
- }
59
-
60
- if (!helper) return // no helpers for screenshot
61
-
62
- const options = Object.assign(defaultConfig, helper.options, config)
63
-
64
- if (helpers.Mochawesome) {
65
- if (helpers.Mochawesome.config) {
66
- options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames
67
- }
68
- }
69
-
70
- if (Codeceptjs.container.mocha()) {
71
- options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions && Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
72
- }
73
-
74
- if (options.disableScreenshots) {
75
- // old version of disabling screenshots
76
- return
9
+ export default function (config = {}) {
10
+ if (!warned) {
11
+ output.error('screenshotOnFail is deprecated; use the `screenshot` plugin (default on=fail).')
12
+ warned = true
77
13
  }
78
-
79
- event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
80
- if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
81
- // no browser here
82
- return
83
- }
84
-
85
- recorder.add(
86
- 'screenshot of failed test',
87
- async () => {
88
- const dataType = 'image/png'
89
- // This prevents data driven to be included in the failed screenshot file name
90
- let fileName
91
-
92
- if (options.uniqueScreenshotNames && test) {
93
- fileName = `${testToFileName(test, { suffix: '', unique: true })}.failed.png`
94
- } else {
95
- fileName = `${testToFileName(test, { suffix: '', unique: false })}.failed.png`
96
- }
97
- const quietMode = !('output_dir' in global) || !global.output_dir
98
- if (!quietMode) {
99
- output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot')
100
- }
101
-
102
- // Re-check helpers at runtime in case they weren't ready during plugin init
103
- const runtimeHelpers = Container.helpers()
104
- let runtimeHelper = null
105
- for (const helperName of supportedHelpers) {
106
- if (Object.keys(runtimeHelpers).indexOf(helperName) > -1) {
107
- runtimeHelper = runtimeHelpers[helperName]
108
- break
109
- }
110
- }
111
-
112
- if (runtimeHelper && typeof runtimeHelper.saveScreenshot === 'function') {
113
- helper = runtimeHelper
114
- }
115
-
116
- try {
117
- if (options.reportDir) {
118
- fileName = path.join(options.reportDir, fileName)
119
- const mochaReportDir = path.resolve(process.cwd(), options.reportDir)
120
- if (!fileExists(mochaReportDir)) {
121
- fs.mkdirSync(mochaReportDir)
122
- }
123
- }
124
-
125
- // Check if browser/page is still available before attempting screenshot
126
- if (helper.page && helper.page.isClosed && helper.page.isClosed()) {
127
- throw new Error('Browser page has been closed')
128
- }
129
- if (helper.browser && helper.browser.isConnected && !helper.browser.isConnected()) {
130
- throw new Error('Browser has been disconnected')
131
- }
132
-
133
- // Add timeout wrapper to prevent hanging with shorter timeout for ESM
134
- const screenshotPromise = helper.saveScreenshot(fileName, options.fullPageScreenshots)
135
- const timeoutPromise = new Promise((_, reject) => {
136
- setTimeout(() => reject(new Error('Screenshot timeout after 5 seconds')), 5000)
137
- })
138
-
139
- await Promise.race([screenshotPromise, timeoutPromise])
140
-
141
- if (!test.artifacts) test.artifacts = {}
142
- // Some unit tests may not define global.output_dir; avoid throwing when it is undefined
143
- // Detect output directory safely (may not be initialized in narrow unit tests)
144
- const baseOutputDir = 'output_dir' in global && typeof global.output_dir === 'string' && global.output_dir ? global.output_dir : null
145
- if (baseOutputDir) {
146
- test.artifacts.screenshot = path.join(baseOutputDir, fileName)
147
- if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
148
- test.attachments = [path.join(baseOutputDir, fileName)]
149
- }
150
- } else {
151
- // Fallback: just store the file name to keep tests stable without triggering path errors
152
- test.artifacts.screenshot = fileName
153
- }
154
- } catch (err) {
155
- if (!quietMode) {
156
- output.plugin('screenshotOnFail', `Failed to save screenshot: ${err.message}`)
157
- }
158
- // Enhanced error handling for browser closed scenarios
159
- if (
160
- err &&
161
- ((err.message &&
162
- (err.message.includes('Target page, context or browser has been closed') ||
163
- err.message.includes('Browser page has been closed') ||
164
- err.message.includes('Browser has been disconnected') ||
165
- err.message.includes('was terminated due to') ||
166
- err.message.includes('no such window: target window already closed') ||
167
- err.message.includes('Screenshot timeout after'))) ||
168
- (err.type && err.type === 'RuntimeError'))
169
- ) {
170
- output.log(`Can't make screenshot, ${err.message}`)
171
- helper.isRunning = false
172
- }
173
- }
174
- },
175
- true,
176
- )
177
- })
14
+ return screenshot({ ...config, on: 'fail' })
178
15
  }
package/lib/rerun.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fsPath from 'path'
2
+ import store from './store.js'
2
3
  import container from './container.js'
3
4
  import event from './event.js'
4
5
  import BaseCodecept from './codecept.js'
@@ -29,7 +30,7 @@ class CodeceptRerunner extends BaseCodecept {
29
30
  let filesToRun = this.testFiles
30
31
  if (test) {
31
32
  if (!fsPath.isAbsolute(test)) {
32
- test = fsPath.join(global.codecept_dir, test)
33
+ test = fsPath.join(store.codeceptDir, test)
33
34
  }
34
35
  filesToRun = this.testFiles.filter(t => fsPath.basename(t, '.js') === test || t === test)
35
36
  }
package/lib/result.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { serializeTest } from './mocha/test.js'
4
+ import store from './store.js'
4
5
 
5
6
  /**
6
7
  * @typedef {Object} Stats Statistics for a test result.
@@ -212,7 +213,7 @@ class Result {
212
213
  */
213
214
  save(fileName) {
214
215
  if (!fileName) fileName = 'result.json'
215
- fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(this.simplify(), null, 2))
216
+ fs.writeFileSync(path.join(store.outputDir, fileName), JSON.stringify(this.simplify(), null, 2))
216
217
  }
217
218
 
218
219
  /**
package/lib/step/base.js CHANGED
@@ -3,6 +3,7 @@ import Secret from '../secret.js'
3
3
  import { getCurrentTimeout } from '../timeout.js'
4
4
  import { ucfirst, humanizeString, serializeError } from '../utils.js'
5
5
  import recordStep from './record.js'
6
+ import store from '../store.js'
6
7
 
7
8
  const STACK_LINE = 5
8
9
 
@@ -148,11 +149,11 @@ class Step {
148
149
  const lines = this.stack.split('\n')
149
150
  if (lines[STACK_LINE]) {
150
151
  let line = lines[STACK_LINE].trim()
151
- .replace(global.codecept_dir || '', '.')
152
+ .replace(store.codeceptDir || '', '.')
152
153
  .trim()
153
154
 
154
155
  // Map .temp.mjs back to original .ts files using container's tsFileMapping
155
- const fileMapping = global.container?.tsFileMapping?.()
156
+ const fileMapping = store.tsFileMapping
156
157
  if (line.includes('.temp.mjs') && fileMapping) {
157
158
  for (const [tsFile, mjsFile] of fileMapping.entries()) {
158
159
  if (line.includes(mjsFile)) {
@@ -1,20 +1,33 @@
1
+ /**
2
+ * @typedef {Object} StepOptions
3
+ * @property {number|'first'|'last'} [elementIndex] - Select a specific element when multiple match. 1-based positive index, negative from end, or 'first'/'last'.
4
+ * @property {boolean} [exact] - Enable strict mode for this step. Throws if multiple elements match.
5
+ * @property {boolean} [strictMode] - Alias for exact.
6
+ * @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
7
+ */
8
+
1
9
  /**
2
10
  * StepConfig is a configuration object for a step.
3
11
  * It is used to create a new step that is a combination of other steps.
4
12
  */
5
13
  class StepConfig {
6
14
  constructor(opts = {}) {
7
- /** @member {{ opts: Record<string, any>, timeout: number|undefined, retry: number|undefined }} */
15
+ /** @member {{ opts: StepOptions, timeout: number|undefined, retry: number|undefined }} */
8
16
  this.config = {
9
17
  opts,
10
18
  timeout: undefined,
11
19
  retry: undefined,
12
20
  }
21
+ this.__isStepConfig = true
22
+ }
23
+
24
+ static isStepConfig(obj) {
25
+ return obj && (obj instanceof StepConfig || obj.__isStepConfig === true)
13
26
  }
14
27
 
15
28
  /**
16
29
  * Set the options for the step.
17
- * @param {object} opts - The options for the step.
30
+ * @param {StepOptions} opts - The options for the step.
18
31
  * @returns {StepConfig} - The step configuration object.
19
32
  */
20
33
  opts(opts) {
@@ -11,7 +11,7 @@ function recordStep(step, args) {
11
11
 
12
12
  // apply step configuration
13
13
  const lastArg = args[args.length - 1]
14
- if (lastArg instanceof StepConfig) {
14
+ if (StepConfig.isStepConfig(lastArg)) {
15
15
  const stepConfig = args.pop()
16
16
  const { opts, timeout, retry } = stepConfig.getConfig()
17
17
 
@@ -63,7 +63,7 @@ function recordStep(step, args) {
63
63
  step.endTime = +Date.now()
64
64
 
65
65
  // Fix error stack to point to original .ts files (lazy import to avoid circular dependency)
66
- const fileMapping = global.container?.tsFileMapping?.()
66
+ const fileMapping = store.tsFileMapping
67
67
  if (fileMapping) {
68
68
  fixErrorStack(err, fileMapping)
69
69
  }
package/lib/store.js CHANGED
@@ -1,8 +1,34 @@
1
1
  /**
2
- * global values for current session
2
+ * Global store for current session
3
3
  * @namespace
4
4
  */
5
5
  const store = {
6
+ // --- Required (set once via initialize(), immutable after) ---
7
+
8
+ /** @type {string | null} */
9
+ _codeceptDir: null,
10
+ /** @type {string | null} */
11
+ _outputDir: null,
12
+
13
+ get codeceptDir() {
14
+ return this._codeceptDir || global.codecept_dir || null
15
+ },
16
+ set codeceptDir(val) {
17
+ this._codeceptDir = val
18
+ },
19
+
20
+ get outputDir() {
21
+ return this._outputDir || global.output_dir || null
22
+ },
23
+ set outputDir(val) {
24
+ this._outputDir = val
25
+ },
26
+
27
+ /** @type {boolean} */
28
+ workerMode: false,
29
+
30
+ // --- Session config (per-session, mutable, set at session start) ---
31
+
6
32
  /**
7
33
  * If we are in --debug mode
8
34
  * @type {boolean}
@@ -27,20 +53,63 @@ const store = {
27
53
  * @type {boolean}
28
54
  */
29
55
  dryRun: false,
56
+
57
+ /**
58
+ * Feature.only() was used
59
+ * @type {boolean}
60
+ */
61
+ featureOnly: false,
62
+
63
+ /**
64
+ * Scenario.only() was used
65
+ * @type {boolean}
66
+ */
67
+ scenarioOnly: false,
68
+
69
+ /**
70
+ * Mask sensitive data config
71
+ * @type {boolean|object}
72
+ */
73
+ maskSensitiveData: false,
74
+
75
+ /**
76
+ * noGlobals mode — user imports everything
77
+ * @type {boolean}
78
+ */
79
+ noGlobals: false,
80
+
81
+ // --- State (tracks current execution, changes constantly) ---
82
+
30
83
  /**
31
84
  * If we are in pause mode
32
85
  * @type {boolean}
33
86
  */
34
87
  onPause: false,
35
88
 
36
- // current object states
37
-
38
89
  /** @type {CodeceptJS.Test | null} */
39
90
  currentTest: null,
40
91
  /** @type {CodeceptJS.Step | null} */
41
92
  currentStep: null,
42
93
  /** @type {CodeceptJS.Suite | null} */
43
94
  currentSuite: null,
95
+
96
+ /** @type {Map<string, string> | null} */
97
+ tsFileMapping: null,
98
+
99
+ /**
100
+ * Initialize required store fields.
101
+ * These values cannot be overwritten after initialization.
102
+ * @param {object} opts
103
+ * @param {string} opts.codeceptDir - root directory of tests
104
+ * @param {string} opts.outputDir - resolved output directory
105
+ */
106
+ initialize(opts) {
107
+ if (!opts.codeceptDir) throw new Error('codeceptDir is required')
108
+ if (!opts.outputDir) throw new Error('outputDir is required')
109
+
110
+ this._codeceptDir = opts.codeceptDir
111
+ this._outputDir = opts.outputDir
112
+ },
44
113
  }
45
114
 
46
115
  export default store
@@ -1,6 +1,7 @@
1
1
  import merge from 'lodash.merge'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
+ import store from './store.js'
4
5
 
5
6
  const defaultVocabulary = {
6
7
  I: 'I',
@@ -15,7 +16,7 @@ class Translation {
15
16
 
16
17
  loadVocabulary(vocabularyFile) {
17
18
  if (!vocabularyFile) return
18
- const filePath = path.join(global.codecept_dir, vocabularyFile)
19
+ const filePath = path.join(store.codeceptDir, vocabularyFile)
19
20
 
20
21
  try {
21
22
  const require = createRequire(import.meta.url)
@@ -1,4 +1,5 @@
1
1
  import { maskSensitiveData } from 'invisi-data'
2
+ import store from '../store.js'
2
3
 
3
4
  /**
4
5
  * Mask sensitive data utility for CodeceptJS
@@ -33,7 +34,7 @@ export function maskData(input, config) {
33
34
  * @returns {boolean|object} - Current masking configuration
34
35
  */
35
36
  export function getMaskConfig() {
36
- return global.maskSensitiveData || false
37
+ return store.maskSensitiveData || global.maskSensitiveData || false
37
38
  }
38
39
 
39
40
  /**
@@ -0,0 +1,151 @@
1
+ import Container from '../container.js'
2
+ import output from '../output.js'
3
+
4
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
5
+
6
+ const RESERVED_KEYS = new Set(['on', 'path', 'line', 'pattern'])
7
+ const ALL_MODES = ['fail', 'test', 'step', 'file', 'url']
8
+
9
+ /**
10
+ * Parse a plugin's _args (from CLI `-p plugin:key=value:key=value`) into a flat dict.
11
+ * Each entry is split on `;` then on the first `=`. Bare segments become `{ key: true }`.
12
+ *
13
+ * Examples:
14
+ * parsePluginArgs(['on=fail'])
15
+ * → { on: 'fail' }
16
+ * parsePluginArgs(['on=file', 'path=tests/foo.js;line=43'])
17
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
18
+ * parsePluginArgs(['on=file', 'path=tests/foo.js', 'line=43'])
19
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
20
+ * parsePluginArgs(['show'])
21
+ * → { show: true }
22
+ */
23
+ export function parsePluginArgs(args = []) {
24
+ const opts = {}
25
+ for (const arg of args) {
26
+ if (!arg) continue
27
+ for (const segment of arg.split(';')) {
28
+ if (!segment) continue
29
+ if (segment.includes('=')) {
30
+ const eq = segment.indexOf('=')
31
+ const key = segment.slice(0, eq)
32
+ const value = segment.slice(eq + 1)
33
+ opts[key] = coerce(value)
34
+ } else {
35
+ opts[segment] = true
36
+ }
37
+ }
38
+ }
39
+ return opts
40
+ }
41
+
42
+ function coerce(v) {
43
+ if (v === 'true') return true
44
+ if (v === 'false') return false
45
+ return v
46
+ }
47
+
48
+ /**
49
+ * Compose CLI args > config > defaults into a normalized trigger spec, then
50
+ * validate it. Returns `{ on, path, line, pattern, ...rest }` with `line`
51
+ * coerced to a number, or `null` if validation failed (an error is printed).
52
+ *
53
+ * @param {object} cliArgs — output of parsePluginArgs(config._args)
54
+ * @param {object} config — full plugin config object
55
+ * @param {object} defaults — fallback values, e.g. `{ on: 'fail' }`
56
+ * @param {object} options
57
+ * @param {string} options.name — plugin name, used in error messages
58
+ * @param {string[]} [options.validModes] — accepted values for `on`
59
+ * (default: fail, test, step, file, url)
60
+ */
61
+ export function resolveTrigger(cliArgs = {}, config = {}, defaults = {}, options = {}) {
62
+ const { name = 'plugin', validModes = ALL_MODES } = options
63
+ const merged = { ...defaults, ...pickKnown(config), ...cliArgs }
64
+ if (merged.line != null) merged.line = parseInt(merged.line, 10)
65
+
66
+ const valid = new Set(validModes)
67
+ if (!valid.has(merged.on)) {
68
+ output.error(`${name}: unknown on="${merged.on}". Valid: ${validModes.join(', ')}`)
69
+ return null
70
+ }
71
+ if (merged.on === 'file' && !merged.path) {
72
+ output.error(`${name}:on=file requires path=. Example: -p ${name}:on=file:path=tests/foo.js`)
73
+ return null
74
+ }
75
+ if (merged.on === 'url' && !merged.pattern) {
76
+ output.error(`${name}:on=url requires pattern=. Example: -p ${name}:on=url:pattern=/users/*`)
77
+ return null
78
+ }
79
+
80
+ return merged
81
+ }
82
+
83
+ function pickKnown(config) {
84
+ const out = {}
85
+ for (const key of Object.keys(config || {})) {
86
+ if (RESERVED_KEYS.has(key)) out[key] = config[key]
87
+ }
88
+ return out
89
+ }
90
+
91
+ /**
92
+ * Match a step's source location against a `path` (substring/suffix) and optional `line`.
93
+ * Reads the step's stack via `step.line()` to get `file:row:col`.
94
+ */
95
+ export function matchStepFile(step, targetPath, targetLine) {
96
+ if (!targetPath) return false
97
+ const stepLine = step.line && step.line()
98
+ if (!stepLine) return false
99
+
100
+ const parsed = parseStepLine(stepLine)
101
+ if (!parsed) return false
102
+
103
+ const fileMatches = parsed.file.includes(targetPath) || parsed.file.endsWith(targetPath)
104
+ if (!fileMatches) return false
105
+
106
+ if (targetLine != null && !Number.isNaN(targetLine) && parsed.line !== targetLine) return false
107
+ return true
108
+ }
109
+
110
+ function parseStepLine(stepLine) {
111
+ let line = stepLine.trim()
112
+ if (line.startsWith('at ')) line = line.substring(3).trim()
113
+
114
+ const lastColon = line.lastIndexOf(':')
115
+ if (lastColon < 0) return null
116
+ const secondLastColon = line.lastIndexOf(':', lastColon - 1)
117
+ if (secondLastColon < 0) return null
118
+
119
+ const file = line.substring(0, secondLastColon)
120
+ const lineNum = parseInt(line.substring(secondLastColon + 1, lastColon), 10)
121
+
122
+ if (Number.isNaN(lineNum)) return null
123
+ return { file, line: lineNum }
124
+ }
125
+
126
+ /**
127
+ * Match a URL string against a glob-style pattern (supports `*` wildcards).
128
+ */
129
+ export function matchUrl(currentUrl, pattern) {
130
+ if (!pattern || !currentUrl) return false
131
+ return patternToRegex(pattern).test(currentUrl)
132
+ }
133
+
134
+ function patternToRegex(pattern) {
135
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
136
+ const regexStr = escaped.replace(/\*/g, '.*')
137
+ return new RegExp(regexStr)
138
+ }
139
+
140
+ /**
141
+ * Return the first available standard browser helper, or null.
142
+ */
143
+ export function getBrowserHelper() {
144
+ const helpers = Container.helpers()
145
+ for (const name of supportedHelpers) {
146
+ if (Object.keys(helpers).indexOf(name) > -1) {
147
+ return helpers[name]
148
+ }
149
+ }
150
+ return null
151
+ }