codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21

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 (296) 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 +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -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/environment-variables.md +131 -0
  30. package/docs/examples.md +161 -0
  31. package/docs/heal.md +213 -0
  32. package/docs/helpers/ApiDataFactory.md +267 -0
  33. package/docs/helpers/Appium.md +1405 -0
  34. package/docs/helpers/Detox.md +665 -0
  35. package/docs/helpers/ExpectHelper.md +275 -0
  36. package/docs/helpers/FileSystem.md +152 -0
  37. package/docs/helpers/GraphQL.md +152 -0
  38. package/docs/helpers/GraphQLDataFactory.md +226 -0
  39. package/docs/helpers/JSONResponse.md +255 -0
  40. package/docs/helpers/Mochawesome.md +8 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/MockServer.md +212 -0
  43. package/docs/helpers/Playwright.md +2969 -0
  44. package/docs/helpers/Polly.md +44 -0
  45. package/docs/helpers/Protractor.md +1769 -0
  46. package/docs/helpers/Puppeteer-firefox.md +86 -0
  47. package/docs/helpers/Puppeteer.md +2690 -0
  48. package/docs/helpers/REST.md +289 -0
  49. package/docs/helpers/SoftExpectHelper.md +352 -0
  50. package/docs/helpers/WebDriver.md +2682 -0
  51. package/docs/hooks.md +339 -0
  52. package/docs/index.md +111 -0
  53. package/docs/installation.md +83 -0
  54. package/docs/internal-api.md +265 -0
  55. package/docs/internal-test-server.md +89 -0
  56. package/docs/locators.md +355 -0
  57. package/docs/mcp.md +485 -0
  58. package/docs/migration-4.md +556 -0
  59. package/docs/mobile.md +338 -0
  60. package/docs/pageobjects.md +399 -0
  61. package/docs/parallel.md +585 -0
  62. package/docs/playwright.md +714 -0
  63. package/docs/plugins.md +866 -0
  64. package/docs/puppeteer.md +314 -0
  65. package/docs/quickstart.md +120 -0
  66. package/docs/react.md +70 -0
  67. package/docs/reports.md +483 -0
  68. package/docs/retry.md +274 -0
  69. package/docs/secrets.md +150 -0
  70. package/docs/sessions.md +80 -0
  71. package/docs/shadow.md +68 -0
  72. package/docs/test-structure.md +275 -0
  73. package/docs/timeouts.md +183 -0
  74. package/docs/translation.md +247 -0
  75. package/docs/tutorial.md +271 -0
  76. package/docs/typescript.md +374 -0
  77. package/docs/web-element.md +251 -0
  78. package/docs/webdriver.md +708 -0
  79. package/docs/within.md +55 -0
  80. package/lib/ai.js +3 -2
  81. package/lib/aria.js +260 -0
  82. package/lib/assertions.js +18 -0
  83. package/lib/codecept.js +27 -24
  84. package/lib/command/check.js +2 -1
  85. package/lib/command/dryRun.js +24 -5
  86. package/lib/command/generate.js +2 -0
  87. package/lib/command/gherkin/snippets.js +5 -4
  88. package/lib/command/init.js +248 -269
  89. package/lib/command/list.js +150 -10
  90. package/lib/command/query.js +218 -0
  91. package/lib/command/run-multiple.js +2 -0
  92. package/lib/command/run-workers.js +2 -14
  93. package/lib/command/run.js +3 -17
  94. package/lib/command/utils.js +14 -0
  95. package/lib/command/workers/runTests.js +10 -10
  96. package/lib/config.js +77 -4
  97. package/lib/container.js +114 -17
  98. package/lib/effects.js +17 -0
  99. package/lib/element/WebElement.js +246 -2
  100. package/lib/els.js +12 -6
  101. package/lib/globals.js +32 -19
  102. package/lib/heal.js +6 -3
  103. package/lib/helper/ApiDataFactory.js +2 -1
  104. package/lib/helper/Appium.js +8 -8
  105. package/lib/helper/FileSystem.js +3 -2
  106. package/lib/helper/GraphQLDataFactory.js +2 -1
  107. package/lib/helper/Playwright.js +233 -162
  108. package/lib/helper/Puppeteer.js +208 -76
  109. package/lib/helper/WebDriver.js +173 -68
  110. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  111. package/lib/helper/errors/NonFocusedType.js +8 -0
  112. package/lib/helper/extras/Download.js +45 -0
  113. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  114. package/lib/helper/extras/elementSelection.js +58 -0
  115. package/lib/helper/extras/focusCheck.js +43 -0
  116. package/lib/helper/extras/richTextEditor.js +178 -0
  117. package/lib/helper/scripts/dropFile.js +11 -0
  118. package/lib/history.js +3 -2
  119. package/lib/html.js +103 -16
  120. package/lib/index.js +9 -1
  121. package/lib/listener/config.js +6 -4
  122. package/lib/listener/emptyRun.js +2 -1
  123. package/lib/listener/globalRetry.js +32 -6
  124. package/lib/listener/helpers.js +4 -1
  125. package/lib/listener/mocha.js +2 -1
  126. package/lib/listener/pageobjects.js +43 -0
  127. package/lib/listener/result.js +3 -2
  128. package/lib/locator.js +126 -3
  129. package/lib/mocha/cli.js +14 -2
  130. package/lib/mocha/factory.js +7 -2
  131. package/lib/mocha/inject.js +1 -1
  132. package/lib/mocha/scenarioConfig.js +2 -1
  133. package/lib/mocha/ui.js +5 -6
  134. package/lib/parser.js +2 -2
  135. package/lib/pause.js +38 -4
  136. package/lib/plugin/aiTrace.js +456 -0
  137. package/lib/plugin/analyze.js +6 -5
  138. package/lib/plugin/auth.js +3 -3
  139. package/lib/plugin/browser.js +77 -0
  140. package/lib/plugin/expose.js +159 -0
  141. package/lib/plugin/heal.js +47 -3
  142. package/lib/plugin/pageInfo.js +54 -52
  143. package/lib/plugin/pause.js +131 -0
  144. package/lib/plugin/pauseOnFail.js +10 -34
  145. package/lib/plugin/retryFailedStep.js +32 -22
  146. package/lib/plugin/screencast.js +289 -0
  147. package/lib/plugin/screenshot.js +563 -0
  148. package/lib/plugin/screenshotOnFail.js +8 -171
  149. package/lib/rerun.js +2 -1
  150. package/lib/result.js +2 -1
  151. package/lib/step/base.js +3 -2
  152. package/lib/step/config.js +15 -2
  153. package/lib/step/record.js +2 -2
  154. package/lib/store.js +72 -3
  155. package/lib/translation.js +2 -1
  156. package/lib/utils/mask_data.js +2 -1
  157. package/lib/utils/pluginParser.js +151 -0
  158. package/lib/utils/trace.js +297 -0
  159. package/lib/utils.js +77 -3
  160. package/lib/workers.js +63 -25
  161. package/package.json +19 -13
  162. package/typings/index.d.ts +19 -5
  163. package/docs/webapi/amOnPage.mustache +0 -11
  164. package/docs/webapi/appendField.mustache +0 -11
  165. package/docs/webapi/attachFile.mustache +0 -12
  166. package/docs/webapi/blur.mustache +0 -18
  167. package/docs/webapi/checkOption.mustache +0 -13
  168. package/docs/webapi/clearCookie.mustache +0 -9
  169. package/docs/webapi/clearField.mustache +0 -9
  170. package/docs/webapi/click.mustache +0 -29
  171. package/docs/webapi/clickLink.mustache +0 -8
  172. package/docs/webapi/closeCurrentTab.mustache +0 -7
  173. package/docs/webapi/closeOtherTabs.mustache +0 -8
  174. package/docs/webapi/dontSee.mustache +0 -11
  175. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  176. package/docs/webapi/dontSeeCookie.mustache +0 -8
  177. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  178. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  179. package/docs/webapi/dontSeeElement.mustache +0 -8
  180. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  181. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  182. package/docs/webapi/dontSeeInField.mustache +0 -11
  183. package/docs/webapi/dontSeeInSource.mustache +0 -8
  184. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  185. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  186. package/docs/webapi/doubleClick.mustache +0 -13
  187. package/docs/webapi/downloadFile.mustache +0 -12
  188. package/docs/webapi/dragAndDrop.mustache +0 -9
  189. package/docs/webapi/dragSlider.mustache +0 -11
  190. package/docs/webapi/executeAsyncScript.mustache +0 -24
  191. package/docs/webapi/executeScript.mustache +0 -26
  192. package/docs/webapi/fillField.mustache +0 -16
  193. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  194. package/docs/webapi/focus.mustache +0 -13
  195. package/docs/webapi/forceClick.mustache +0 -28
  196. package/docs/webapi/forceRightClick.mustache +0 -18
  197. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  198. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  199. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  200. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  201. package/docs/webapi/grabCookie.mustache +0 -11
  202. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  203. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  204. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  205. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  206. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  207. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  208. package/docs/webapi/grabGeoLocation.mustache +0 -8
  209. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  210. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  211. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  212. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  213. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  214. package/docs/webapi/grabPopupText.mustache +0 -5
  215. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  216. package/docs/webapi/grabSource.mustache +0 -8
  217. package/docs/webapi/grabTextFrom.mustache +0 -10
  218. package/docs/webapi/grabTextFromAll.mustache +0 -9
  219. package/docs/webapi/grabTitle.mustache +0 -8
  220. package/docs/webapi/grabValueFrom.mustache +0 -9
  221. package/docs/webapi/grabValueFromAll.mustache +0 -8
  222. package/docs/webapi/grabWebElement.mustache +0 -9
  223. package/docs/webapi/grabWebElements.mustache +0 -9
  224. package/docs/webapi/moveCursorTo.mustache +0 -12
  225. package/docs/webapi/openNewTab.mustache +0 -7
  226. package/docs/webapi/pressKey.mustache +0 -12
  227. package/docs/webapi/pressKeyDown.mustache +0 -12
  228. package/docs/webapi/pressKeyUp.mustache +0 -12
  229. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  230. package/docs/webapi/refreshPage.mustache +0 -6
  231. package/docs/webapi/resizeWindow.mustache +0 -6
  232. package/docs/webapi/rightClick.mustache +0 -14
  233. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  234. package/docs/webapi/saveScreenshot.mustache +0 -12
  235. package/docs/webapi/say.mustache +0 -10
  236. package/docs/webapi/scrollIntoView.mustache +0 -11
  237. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  238. package/docs/webapi/scrollPageToTop.mustache +0 -6
  239. package/docs/webapi/scrollTo.mustache +0 -12
  240. package/docs/webapi/see.mustache +0 -11
  241. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  242. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  243. package/docs/webapi/seeCookie.mustache +0 -8
  244. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  245. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  246. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  247. package/docs/webapi/seeElement.mustache +0 -8
  248. package/docs/webapi/seeElementInDOM.mustache +0 -8
  249. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  250. package/docs/webapi/seeInField.mustache +0 -12
  251. package/docs/webapi/seeInPopup.mustache +0 -8
  252. package/docs/webapi/seeInSource.mustache +0 -7
  253. package/docs/webapi/seeInTitle.mustache +0 -8
  254. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  255. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  256. package/docs/webapi/seeTextEquals.mustache +0 -9
  257. package/docs/webapi/seeTitleEquals.mustache +0 -8
  258. package/docs/webapi/seeTraffic.mustache +0 -36
  259. package/docs/webapi/selectOption.mustache +0 -21
  260. package/docs/webapi/setCookie.mustache +0 -16
  261. package/docs/webapi/setGeoLocation.mustache +0 -12
  262. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  263. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  264. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  265. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  266. package/docs/webapi/switchTo.mustache +0 -9
  267. package/docs/webapi/switchToNextTab.mustache +0 -10
  268. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  269. package/docs/webapi/type.mustache +0 -21
  270. package/docs/webapi/uncheckOption.mustache +0 -13
  271. package/docs/webapi/wait.mustache +0 -8
  272. package/docs/webapi/waitForClickable.mustache +0 -11
  273. package/docs/webapi/waitForCookie.mustache +0 -9
  274. package/docs/webapi/waitForDetached.mustache +0 -10
  275. package/docs/webapi/waitForDisabled.mustache +0 -6
  276. package/docs/webapi/waitForElement.mustache +0 -11
  277. package/docs/webapi/waitForEnabled.mustache +0 -6
  278. package/docs/webapi/waitForFunction.mustache +0 -17
  279. package/docs/webapi/waitForInvisible.mustache +0 -10
  280. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  281. package/docs/webapi/waitForText.mustache +0 -13
  282. package/docs/webapi/waitForValue.mustache +0 -10
  283. package/docs/webapi/waitForVisible.mustache +0 -10
  284. package/docs/webapi/waitInUrl.mustache +0 -9
  285. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  286. package/docs/webapi/waitToHide.mustache +0 -10
  287. package/docs/webapi/waitUrlEquals.mustache +0 -10
  288. package/lib/helper/AI.js +0 -214
  289. package/lib/listener/enhancedGlobalRetry.js +0 -110
  290. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  291. package/lib/plugin/htmlReporter.js +0 -3648
  292. package/lib/plugin/stepByStepReport.js +0 -427
  293. package/lib/plugin/subtitles.js +0 -89
  294. package/lib/retryCoordinator.js +0 -207
  295. package/typings/promiseBasedTypes.d.ts +0 -9469
  296. package/typings/types.d.ts +0 -11402
@@ -1,14 +1,24 @@
1
+ import debugModule from 'debug'
1
2
  import event from '../event.js'
2
-
3
3
  import recorder from '../recorder.js'
4
-
5
4
  import store from '../store.js'
6
5
 
6
+ const debug = debugModule('codeceptjs:retryFailedStep')
7
+
7
8
  const defaultConfig = {
8
9
  retries: 3,
9
10
  defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
10
11
  factor: 1.5,
11
12
  ignoredSteps: [],
13
+ deferToScenarioRetries: true,
14
+ }
15
+
16
+ const RETRY_PRIORITIES = {
17
+ MANUAL_STEP: 100,
18
+ STEP_PLUGIN: 50,
19
+ SCENARIO_CONFIG: 30,
20
+ FEATURE_CONFIG: 20,
21
+ HOOK_CONFIG: 10,
12
22
  }
13
23
 
14
24
  /**
@@ -49,6 +59,7 @@ const defaultConfig = {
49
59
  * * `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list.
50
60
  * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
51
61
  * To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.
62
+ * * `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries.
52
63
  *
53
64
  * #### Example
54
65
  *
@@ -88,73 +99,72 @@ export default function (config) {
88
99
  if (!enableRetry) return
89
100
  if (store.debugMode) return false
90
101
  if (!store.autoRetries) return false
91
- // Don't retry terminal errors (e.g., frame detachment errors)
92
102
  if (err && err.isTerminal) return false
93
- // Don't retry navigation errors that are known to be terminal
94
103
  if (err && err.message && (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed'))) return false
95
104
  if (customWhen) return customWhen(err)
96
105
  return true
97
106
  }
98
107
  config.when = when
99
108
 
100
- // Ensure retry options are available before any steps run
101
109
  if (!recorder.retries.find(r => r === config)) {
102
110
  recorder.retries.push(config)
103
111
  }
104
112
 
105
113
  event.dispatcher.on(event.step.started, step => {
106
- // if a step is ignored - return
107
114
  for (const ignored of config.ignoredSteps) {
108
115
  if (step.name === ignored) return
109
116
  if (ignored instanceof RegExp) {
110
117
  if (step.name.match(ignored)) return
111
118
  } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
112
119
  }
113
- enableRetry = true // enable retry for a step
120
+ enableRetry = true
114
121
  })
115
122
 
116
- // Disable retry only after a successful step; keep it enabled for failure so retry logic can act
117
123
  event.dispatcher.on(event.step.passed, () => {
118
124
  enableRetry = false
119
125
  })
120
126
 
121
127
  event.dispatcher.on(event.test.before, test => {
122
- // pass disableRetryFailedStep is a preferred way to disable retries
123
- // test.disableRetryFailedStep is used for backward compatibility
124
128
  if (!test.opts) test.opts = {}
125
129
  if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
126
130
  store.autoRetries = false
127
- return // disable retry when a test is not active
131
+ return
132
+ }
133
+
134
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
135
+ const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
136
+ const scenarioPriority = test.opts.retryPriority || 0
137
+
138
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
139
+ store.autoRetries = false
140
+ return
128
141
  }
129
142
 
130
- // Don't apply plugin retry logic if there are already manual retries configured
131
- // Check if any retry configs exist that aren't from this plugin
132
143
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
133
144
  if (hasManualRetries) {
134
145
  store.autoRetries = false
135
146
  return
136
147
  }
137
148
 
138
- // this option is used to set the retries inside _before() block of helpers
139
149
  store.autoRetries = true
140
150
  test.opts.conditionalRetries = config.retries
141
- // debug: record applied retries value for tests
142
- if (process.env.DEBUG_RETRY_PLUGIN) {
143
- // eslint-disable-next-line no-console
144
- console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
145
- }
151
+ test.opts.stepRetryPriority = stepRetryPriority
152
+
153
+ debug('applying retries = %d for test %s', config.retries, test.title)
146
154
  recorder.retry(config)
147
155
  })
148
156
 
149
- // Fallback for environments where event.test.before wasn't emitted (runner scenarios)
150
157
  event.dispatcher.on(event.test.started, test => {
151
158
  if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
152
159
 
153
- // Don't apply plugin retry logic if there are already manual retries configured
154
- // Check if any retry configs exist that aren't from this plugin
155
160
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
156
161
  if (hasManualRetries) return
157
162
 
163
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
164
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
165
+ return
166
+ }
167
+
158
168
  if (!store.autoRetries) {
159
169
  store.autoRetries = true
160
170
  test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
@@ -0,0 +1,289 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { mkdirp } from 'mkdirp'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+
6
+ import Container from '../container.js'
7
+ import recorder from '../recorder.js'
8
+ import event from '../event.js'
9
+ import output from '../output.js'
10
+ import store from '../store.js'
11
+
12
+ import { testToFileName } from '../mocha/test.js'
13
+ import { parsePluginArgs, resolveTrigger, getBrowserHelper } from '../utils/pluginParser.js'
14
+
15
+ const defaultConfig = {
16
+ on: 'fail',
17
+ captions: true,
18
+ subtitles: false,
19
+ video: true,
20
+ }
21
+
22
+ /**
23
+ * Records WebM video of tests using Playwright's screencast API.
24
+ *
25
+ * When `captions` is enabled, action annotations are burned into the video;
26
+ * when `subtitles` is enabled, a standalone `.srt` is also produced. Default
27
+ * `on=fail` keeps videos for failed tests only; `on=test` keeps every test's
28
+ * video.
29
+ *
30
+ * Note: enabling Playwright's helper-level `video: true` together with this
31
+ * plugin produces two independent recordings (`output/videos/*.webm` from the
32
+ * helper, `output/screencast/*.webm` from this plugin).
33
+ *
34
+ * #### Configuration
35
+ *
36
+ * ```js
37
+ * plugins: {
38
+ * screencast: {
39
+ * enabled: true,
40
+ * on: 'fail',
41
+ * }
42
+ * }
43
+ * ```
44
+ *
45
+ * #### `on=` modes
46
+ *
47
+ * * **fail** — record while running; delete on pass, keep on fail (default)
48
+ * * **test** — record and keep every test's video
49
+ *
50
+ * Other config options:
51
+ *
52
+ * * `captions`: burn-in action overlays via `page.screencast.showActions()`. Default: true.
53
+ * * `subtitles`: also write a standalone `.srt` file alongside the video. Default: false.
54
+ * * `video`: record a video. With `video=false, subtitles=true`, only the `.srt` is produced. Default: true.
55
+ * * `size`: pass-through `{ width, height }` for `screencast.start`.
56
+ * * `quality`: pass-through 0–100 for `screencast.start`.
57
+ *
58
+ * CLI examples:
59
+ *
60
+ * ```
61
+ * npx codeceptjs run -p screencast
62
+ * npx codeceptjs run -p screencast:on=test
63
+ * npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true
64
+ * ```
65
+ */
66
+ export default function (config = {}) {
67
+ const helper = getBrowserHelper()
68
+ if (!helper) return
69
+
70
+ const cliArgs = parsePluginArgs(config._args)
71
+ const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, {
72
+ name: 'screencast',
73
+ validModes: ['fail', 'test'],
74
+ })
75
+ if (!trigger) return
76
+
77
+ const options = Object.assign({}, defaultConfig, config)
78
+ options.captions = cliArgs.captions ?? config.captions ?? defaultConfig.captions
79
+ options.subtitles = cliArgs.subtitles ?? config.subtitles ?? defaultConfig.subtitles
80
+ options.video = cliArgs.video ?? config.video ?? defaultConfig.video
81
+
82
+ return wireScreencast(trigger.on, options)
83
+ }
84
+
85
+ function wireScreencast(mode, options) {
86
+ const state = {
87
+ test: null,
88
+ webmPath: null,
89
+ srtPath: null,
90
+ steps: null,
91
+ startedAt: null,
92
+ failed: false,
93
+ startQueued: false,
94
+ started: false,
95
+ warnedNoApi: false,
96
+ }
97
+
98
+ event.dispatcher.on(event.test.before, test => {
99
+ state.test = test
100
+ state.failed = false
101
+ state.webmPath = null
102
+ state.srtPath = null
103
+ state.startQueued = false
104
+ state.started = false
105
+ state.steps = options.subtitles ? {} : null
106
+ state.startedAt = options.subtitles ? Date.now() : null
107
+ })
108
+
109
+ event.dispatcher.on(event.test.started, test => {
110
+ if (!options.video || state.startQueued) return
111
+ state.startQueued = true
112
+ recorder.add('screencast:start', async () => startScreencast(state.test, options, state), true)
113
+ })
114
+
115
+ event.dispatcher.on(event.step.started, step => {
116
+ if (state.steps) {
117
+ const at = Date.now()
118
+ step.id = step.id || uuidv4()
119
+ state.steps[step.id] = {
120
+ start: formatTimestamp(at - state.startedAt),
121
+ startedAt: at,
122
+ title: stepTitle(step),
123
+ }
124
+ }
125
+ })
126
+
127
+ if (options.subtitles) {
128
+ event.dispatcher.on(event.step.finished, step => {
129
+ if (!state.steps || !step?.id || !state.steps[step.id]) return
130
+ state.steps[step.id].end = formatTimestamp(Date.now() - state.startedAt)
131
+ })
132
+ }
133
+
134
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
135
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
136
+ state.failed = true
137
+ })
138
+
139
+ event.dispatcher.on(event.test.after, () => {
140
+ if (!state.test) return
141
+ recorder.add('screencast:stop', async () => finalizeScreencast({
142
+ test: state.test,
143
+ webmPath: state.webmPath,
144
+ srtPath: state.srtPath,
145
+ steps: state.steps,
146
+ failed: state.failed,
147
+ started: state.started,
148
+ options,
149
+ mode,
150
+ }), true)
151
+ })
152
+ }
153
+
154
+ async function startScreencast(test, options, state) {
155
+ const helper = getBrowserHelper()
156
+ if (!helper?.page?.screencast) {
157
+ if (!state.warnedNoApi) {
158
+ output.plugin('screencast', 'page.screencast not available — requires Playwright >= 1.59. Skipping.')
159
+ state.warnedNoApi = true
160
+ }
161
+ return
162
+ }
163
+
164
+ const baseDir = path.join(store.outputDir || '_output', 'screencast')
165
+ mkdirp.sync(baseDir)
166
+ const baseName = testToFileName(test, { suffix: '', unique: true })
167
+ state.webmPath = path.join(baseDir, `${baseName}.webm`)
168
+ state.srtPath = path.join(baseDir, `${baseName}.srt`)
169
+
170
+ const startOpts = { path: state.webmPath }
171
+ if (options.size) startOpts.size = options.size
172
+ if (options.quality != null) startOpts.quality = options.quality
173
+
174
+ try {
175
+ await helper.page.screencast.start(startOpts)
176
+ state.started = true
177
+ } catch (err) {
178
+ output.plugin('screencast', `Failed to start: ${err.message}`)
179
+ state.webmPath = null
180
+ state.srtPath = null
181
+ state.started = false
182
+ return
183
+ }
184
+
185
+ if (options.captions && typeof helper.page.screencast.showActions === 'function') {
186
+ try { await helper.page.screencast.showActions() }
187
+ catch (err) { output.plugin('screencast', `showActions failed: ${err.message}`) }
188
+ }
189
+ if (typeof helper.page.screencast.showChapter === 'function') {
190
+ try { await helper.page.screencast.showChapter(String(test.title || '')) }
191
+ catch (err) { output.plugin('screencast', `showChapter failed: ${err.message}`) }
192
+ }
193
+ }
194
+
195
+ async function finalizeScreencast(snapshot) {
196
+ const { test, options, mode, steps } = snapshot
197
+ let { webmPath, srtPath } = snapshot
198
+
199
+ const helper = getBrowserHelper()
200
+ if (snapshot.started && helper?.page?.screencast) {
201
+ try {
202
+ await helper.page.screencast.stop()
203
+ } catch (err) {
204
+ output.plugin('screencast', `stop failed: ${err.message}`)
205
+ }
206
+ }
207
+
208
+ const shouldKeep = mode === 'test' || (mode === 'fail' && snapshot.failed)
209
+
210
+ if (options.video && webmPath) {
211
+ if (!shouldKeep) {
212
+ try { fs.unlinkSync(webmPath) } catch { /* file may not exist yet */ }
213
+ webmPath = null
214
+ } else {
215
+ ensureArtifactsObject(test)
216
+ test.artifacts.screencast = webmPath
217
+ attachJUnitArtifact(test, webmPath)
218
+ }
219
+ }
220
+
221
+ if (options.subtitles && steps) {
222
+ if (options.video && !shouldKeep) {
223
+ try { srtPath && fs.unlinkSync(srtPath) } catch { /* nothing to delete */ }
224
+ return
225
+ }
226
+
227
+ let target = srtPath
228
+ if (!options.video) {
229
+ if (test.artifacts && test.artifacts.video) {
230
+ const { dir, name } = path.parse(test.artifacts.video)
231
+ target = path.join(dir, `${name}.srt`)
232
+ } else {
233
+ const baseDir = path.join(store.outputDir || '_output', 'screencast')
234
+ mkdirp.sync(baseDir)
235
+ const baseName = testToFileName(test, { suffix: '', unique: true })
236
+ target = path.join(baseDir, `${baseName}.srt`)
237
+ }
238
+ }
239
+
240
+ if (!target) return
241
+ try {
242
+ await fs.promises.writeFile(target, buildSrt(steps))
243
+ ensureArtifactsObject(test)
244
+ test.artifacts.subtitle = target
245
+ } catch (err) {
246
+ output.plugin('screencast', `failed to write SRT: ${err.message}`)
247
+ }
248
+ }
249
+ }
250
+
251
+ function formatTimestamp(timestampInMs) {
252
+ const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs)
253
+ const hours = date.getHours()
254
+ const minutes = date.getMinutes()
255
+ const seconds = date.getSeconds()
256
+ const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000)
257
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`
258
+ }
259
+
260
+ function stepTitle(step) {
261
+ let title = `${step.actor}.${step.name}(${step.args ? step.args.join(',') : ''})`
262
+ if (title.length > 100) title = `${title.substring(0, 100)}...`
263
+ return title
264
+ }
265
+
266
+ function buildSrt(steps) {
267
+ const sorted = Object.values(steps).sort((a, b) => a.startedAt - b.startedAt)
268
+ let out = ''
269
+ let index = 1
270
+ for (const step of sorted) {
271
+ if (!step.end) continue
272
+ out += `${index}\n${step.start} --> ${step.end}\n${step.title}\n\n`
273
+ index++
274
+ }
275
+ return out
276
+ }
277
+
278
+ function ensureArtifactsObject(test) {
279
+ if (!test.artifacts || Array.isArray(test.artifacts)) test.artifacts = {}
280
+ }
281
+
282
+ function attachJUnitArtifact(test, filePath) {
283
+ const mocha = Container.mocha?.()
284
+ const junit = mocha?.options?.reporterOptions?.['mocha-junit-reporter']
285
+ if (junit?.options?.attachments) {
286
+ test.attachments = test.attachments || []
287
+ test.attachments.push(filePath)
288
+ }
289
+ }