codeceptjs 4.0.2-beta.8 → 4.0.2

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 (326) hide show
  1. package/README.md +39 -28
  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 +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +91 -37
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. package/lib/retryCoordinator.js +0 -207
@@ -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.title}(${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
+ }