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,7 +1,5 @@
1
1
  import event from '../event.js'
2
-
3
2
  import recorder from '../recorder.js'
4
-
5
3
  import store from '../store.js'
6
4
 
7
5
  const defaultConfig = {
@@ -9,6 +7,15 @@ const defaultConfig = {
9
7
  defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
10
8
  factor: 1.5,
11
9
  ignoredSteps: [],
10
+ deferToScenarioRetries: true,
11
+ }
12
+
13
+ const RETRY_PRIORITIES = {
14
+ MANUAL_STEP: 100,
15
+ STEP_PLUGIN: 50,
16
+ SCENARIO_CONFIG: 30,
17
+ FEATURE_CONFIG: 20,
18
+ HOOK_CONFIG: 10,
12
19
  }
13
20
 
14
21
  /**
@@ -49,6 +56,7 @@ const defaultConfig = {
49
56
  * * `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list.
50
57
  * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
51
58
  * To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.
59
+ * * `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries.
52
60
  *
53
61
  * #### Example
54
62
  *
@@ -88,73 +96,74 @@ export default function (config) {
88
96
  if (!enableRetry) return
89
97
  if (store.debugMode) return false
90
98
  if (!store.autoRetries) return false
91
- // Don't retry terminal errors (e.g., frame detachment errors)
92
99
  if (err && err.isTerminal) return false
93
- // Don't retry navigation errors that are known to be terminal
94
100
  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
101
  if (customWhen) return customWhen(err)
96
102
  return true
97
103
  }
98
104
  config.when = when
99
105
 
100
- // Ensure retry options are available before any steps run
101
106
  if (!recorder.retries.find(r => r === config)) {
102
107
  recorder.retries.push(config)
103
108
  }
104
109
 
105
110
  event.dispatcher.on(event.step.started, step => {
106
- // if a step is ignored - return
107
111
  for (const ignored of config.ignoredSteps) {
108
112
  if (step.name === ignored) return
109
113
  if (ignored instanceof RegExp) {
110
114
  if (step.name.match(ignored)) return
111
115
  } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
112
116
  }
113
- enableRetry = true // enable retry for a step
117
+ enableRetry = true
114
118
  })
115
119
 
116
- // Disable retry only after a successful step; keep it enabled for failure so retry logic can act
117
120
  event.dispatcher.on(event.step.passed, () => {
118
121
  enableRetry = false
119
122
  })
120
123
 
121
124
  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
125
  if (!test.opts) test.opts = {}
125
126
  if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
126
127
  store.autoRetries = false
127
- return // disable retry when a test is not active
128
+ return
129
+ }
130
+
131
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
132
+ const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
133
+ const scenarioPriority = test.opts.retryPriority || 0
134
+
135
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
136
+ store.autoRetries = false
137
+ return
128
138
  }
129
139
 
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
140
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
133
141
  if (hasManualRetries) {
134
142
  store.autoRetries = false
135
143
  return
136
144
  }
137
145
 
138
- // this option is used to set the retries inside _before() block of helpers
139
146
  store.autoRetries = true
140
147
  test.opts.conditionalRetries = config.retries
141
- // debug: record applied retries value for tests
148
+ test.opts.stepRetryPriority = stepRetryPriority
149
+
142
150
  if (process.env.DEBUG_RETRY_PLUGIN) {
143
- // eslint-disable-next-line no-console
144
151
  console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
145
152
  }
146
153
  recorder.retry(config)
147
154
  })
148
155
 
149
- // Fallback for environments where event.test.before wasn't emitted (runner scenarios)
150
156
  event.dispatcher.on(event.test.started, test => {
151
157
  if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
152
158
 
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
159
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
156
160
  if (hasManualRetries) return
157
161
 
162
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
163
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
164
+ return
165
+ }
166
+
158
167
  if (!store.autoRetries) {
159
168
  store.autoRetries = true
160
169
  test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
@@ -0,0 +1,287 @@
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.step.started, step => {
110
+ if (state.steps) {
111
+ const at = Date.now()
112
+ step.id = step.id || uuidv4()
113
+ state.steps[step.id] = {
114
+ start: formatTimestamp(at - state.startedAt),
115
+ startedAt: at,
116
+ title: stepTitle(step),
117
+ }
118
+ }
119
+ if (!options.video || state.startQueued || !state.test) return
120
+ state.startQueued = true
121
+ const test = state.test
122
+ recorder.add('screencast:start', async () => startScreencast(test, options, state), true)
123
+ })
124
+
125
+ if (options.subtitles) {
126
+ event.dispatcher.on(event.step.finished, step => {
127
+ if (!state.steps || !step?.id || !state.steps[step.id]) return
128
+ state.steps[step.id].end = formatTimestamp(Date.now() - state.startedAt)
129
+ })
130
+ }
131
+
132
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
133
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
134
+ state.failed = true
135
+ })
136
+
137
+ event.dispatcher.on(event.test.after, () => {
138
+ if (!state.test) return
139
+ recorder.add('screencast:stop', async () => finalizeScreencast({
140
+ test: state.test,
141
+ webmPath: state.webmPath,
142
+ srtPath: state.srtPath,
143
+ steps: state.steps,
144
+ failed: state.failed,
145
+ started: state.started,
146
+ options,
147
+ mode,
148
+ }), true)
149
+ })
150
+ }
151
+
152
+ async function startScreencast(test, options, state) {
153
+ const helper = getBrowserHelper()
154
+ if (!helper?.page?.screencast) {
155
+ if (!state.warnedNoApi) {
156
+ output.plugin('screencast', 'page.screencast not available — requires Playwright >= 1.59. Skipping.')
157
+ state.warnedNoApi = true
158
+ }
159
+ return
160
+ }
161
+
162
+ const baseDir = path.join(store.outputDir || '_output', 'screencast')
163
+ mkdirp.sync(baseDir)
164
+ const baseName = testToFileName(test, { suffix: '', unique: true })
165
+ state.webmPath = path.join(baseDir, `${baseName}.webm`)
166
+ state.srtPath = path.join(baseDir, `${baseName}.srt`)
167
+
168
+ const startOpts = { path: state.webmPath }
169
+ if (options.size) startOpts.size = options.size
170
+ if (options.quality != null) startOpts.quality = options.quality
171
+
172
+ try {
173
+ await helper.page.screencast.start(startOpts)
174
+ state.started = true
175
+ } catch (err) {
176
+ output.plugin('screencast', `Failed to start: ${err.message}`)
177
+ state.webmPath = null
178
+ state.srtPath = null
179
+ state.started = false
180
+ return
181
+ }
182
+
183
+ if (options.captions && typeof helper.page.screencast.showActions === 'function') {
184
+ try { await helper.page.screencast.showActions() }
185
+ catch (err) { output.plugin('screencast', `showActions failed: ${err.message}`) }
186
+ }
187
+ if (typeof helper.page.screencast.showChapter === 'function') {
188
+ try { await helper.page.screencast.showChapter(String(test.title || '')) }
189
+ catch (err) { output.plugin('screencast', `showChapter failed: ${err.message}`) }
190
+ }
191
+ }
192
+
193
+ async function finalizeScreencast(snapshot) {
194
+ const { test, options, mode, steps } = snapshot
195
+ let { webmPath, srtPath } = snapshot
196
+
197
+ const helper = getBrowserHelper()
198
+ if (snapshot.started && helper?.page?.screencast) {
199
+ try {
200
+ await helper.page.screencast.stop()
201
+ } catch (err) {
202
+ output.plugin('screencast', `stop failed: ${err.message}`)
203
+ }
204
+ }
205
+
206
+ const shouldKeep = mode === 'test' || (mode === 'fail' && snapshot.failed)
207
+
208
+ if (options.video && webmPath) {
209
+ if (!shouldKeep) {
210
+ try { fs.unlinkSync(webmPath) } catch { /* file may not exist yet */ }
211
+ webmPath = null
212
+ } else {
213
+ ensureArtifactsObject(test)
214
+ test.artifacts.screencast = webmPath
215
+ attachJUnitArtifact(test, webmPath)
216
+ }
217
+ }
218
+
219
+ if (options.subtitles && steps) {
220
+ if (options.video && !shouldKeep) {
221
+ try { srtPath && fs.unlinkSync(srtPath) } catch { /* nothing to delete */ }
222
+ return
223
+ }
224
+
225
+ let target = srtPath
226
+ if (!options.video) {
227
+ if (test.artifacts && test.artifacts.video) {
228
+ const { dir, name } = path.parse(test.artifacts.video)
229
+ target = path.join(dir, `${name}.srt`)
230
+ } else {
231
+ const baseDir = path.join(store.outputDir || '_output', 'screencast')
232
+ mkdirp.sync(baseDir)
233
+ const baseName = testToFileName(test, { suffix: '', unique: true })
234
+ target = path.join(baseDir, `${baseName}.srt`)
235
+ }
236
+ }
237
+
238
+ if (!target) return
239
+ try {
240
+ await fs.promises.writeFile(target, buildSrt(steps))
241
+ ensureArtifactsObject(test)
242
+ test.artifacts.subtitle = target
243
+ } catch (err) {
244
+ output.plugin('screencast', `failed to write SRT: ${err.message}`)
245
+ }
246
+ }
247
+ }
248
+
249
+ function formatTimestamp(timestampInMs) {
250
+ const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs)
251
+ const hours = date.getHours()
252
+ const minutes = date.getMinutes()
253
+ const seconds = date.getSeconds()
254
+ const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000)
255
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`
256
+ }
257
+
258
+ function stepTitle(step) {
259
+ let title = `${step.actor}.${step.name}(${step.args ? step.args.join(',') : ''})`
260
+ if (title.length > 100) title = `${title.substring(0, 100)}...`
261
+ return title
262
+ }
263
+
264
+ function buildSrt(steps) {
265
+ const sorted = Object.values(steps).sort((a, b) => a.startedAt - b.startedAt)
266
+ let out = ''
267
+ let index = 1
268
+ for (const step of sorted) {
269
+ if (!step.end) continue
270
+ out += `${index}\n${step.start} --> ${step.end}\n${step.title}\n\n`
271
+ index++
272
+ }
273
+ return out
274
+ }
275
+
276
+ function ensureArtifactsObject(test) {
277
+ if (!test.artifacts || Array.isArray(test.artifacts)) test.artifacts = {}
278
+ }
279
+
280
+ function attachJUnitArtifact(test, filePath) {
281
+ const mocha = Container.mocha?.()
282
+ const junit = mocha?.options?.reporterOptions?.['mocha-junit-reporter']
283
+ if (junit?.options?.attachments) {
284
+ test.attachments = test.attachments || []
285
+ test.attachments.push(filePath)
286
+ }
287
+ }