codeceptjs 4.0.0-rc.17 → 4.0.0-rc.19

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 (240) hide show
  1. package/bin/codecept.js +15 -2
  2. package/bin/codeceptq.js +49 -0
  3. package/bin/mcp-server.js +733 -196
  4. package/docs/advanced.md +201 -0
  5. package/docs/agents.md +159 -0
  6. package/docs/ai.md +537 -0
  7. package/docs/aitrace.md +266 -0
  8. package/docs/api.md +332 -0
  9. package/docs/assertions.md +415 -0
  10. package/docs/auth.md +318 -0
  11. package/docs/basics.md +424 -0
  12. package/docs/bdd.md +539 -0
  13. package/docs/best.md +240 -0
  14. package/docs/bootstrap.md +132 -0
  15. package/docs/commands.md +352 -0
  16. package/docs/community-helpers.md +63 -0
  17. package/docs/configuration.md +230 -0
  18. package/docs/continuous-integration.md +497 -0
  19. package/docs/custom-helpers.md +297 -0
  20. package/docs/data.md +448 -0
  21. package/docs/debugging.md +332 -0
  22. package/docs/detox.md +235 -0
  23. package/docs/docker.md +136 -0
  24. package/docs/effects.md +179 -0
  25. package/docs/element-based-testing.md +295 -0
  26. package/docs/element-selection.md +125 -0
  27. package/docs/els.md +328 -0
  28. package/docs/examples.md +161 -0
  29. package/docs/heal.md +213 -0
  30. package/docs/helpers/ApiDataFactory.md +267 -0
  31. package/docs/helpers/Appium.md +1405 -0
  32. package/docs/helpers/Detox.md +665 -0
  33. package/docs/helpers/ExpectHelper.md +275 -0
  34. package/docs/helpers/FileSystem.md +152 -0
  35. package/docs/helpers/GraphQL.md +152 -0
  36. package/docs/helpers/GraphQLDataFactory.md +226 -0
  37. package/docs/helpers/JSONResponse.md +255 -0
  38. package/docs/helpers/Mochawesome.md +8 -0
  39. package/docs/helpers/MockRequest.md +377 -0
  40. package/docs/helpers/MockServer.md +212 -0
  41. package/docs/helpers/Playwright.md +2969 -0
  42. package/docs/helpers/Polly.md +44 -0
  43. package/docs/helpers/Protractor.md +1769 -0
  44. package/docs/helpers/Puppeteer-firefox.md +86 -0
  45. package/docs/helpers/Puppeteer.md +2690 -0
  46. package/docs/helpers/REST.md +289 -0
  47. package/docs/helpers/SoftExpectHelper.md +352 -0
  48. package/docs/helpers/WebDriver.md +2682 -0
  49. package/docs/hooks.md +339 -0
  50. package/docs/index.md +111 -0
  51. package/docs/installation.md +83 -0
  52. package/docs/internal-api.md +265 -0
  53. package/docs/internal-test-server.md +89 -0
  54. package/docs/locators.md +355 -0
  55. package/docs/mcp.md +485 -0
  56. package/docs/migration-4.md +556 -0
  57. package/docs/mobile.md +338 -0
  58. package/docs/pageobjects.md +399 -0
  59. package/docs/parallel.md +585 -0
  60. package/docs/playwright.md +714 -0
  61. package/docs/plugins.md +866 -0
  62. package/docs/puppeteer.md +314 -0
  63. package/docs/quickstart.md +120 -0
  64. package/docs/react.md +70 -0
  65. package/docs/reports.md +483 -0
  66. package/docs/retry.md +274 -0
  67. package/docs/secrets.md +150 -0
  68. package/docs/sessions.md +80 -0
  69. package/docs/shadow.md +68 -0
  70. package/docs/test-structure.md +275 -0
  71. package/docs/timeouts.md +183 -0
  72. package/docs/translation.md +247 -0
  73. package/docs/tutorial.md +271 -0
  74. package/docs/typescript.md +374 -0
  75. package/docs/web-element.md +251 -0
  76. package/docs/webdriver.md +708 -0
  77. package/docs/within.md +55 -0
  78. package/lib/aria.js +260 -0
  79. package/lib/command/dryRun.js +23 -3
  80. package/lib/command/init.js +247 -266
  81. package/lib/command/list.js +150 -10
  82. package/lib/command/query.js +218 -0
  83. package/lib/config.js +77 -4
  84. package/lib/container.js +34 -2
  85. package/lib/element/WebElement.js +37 -0
  86. package/lib/globals.js +11 -10
  87. package/lib/helper/Playwright.js +5 -6
  88. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  89. package/lib/html.js +90 -16
  90. package/lib/index.js +9 -1
  91. package/lib/locator.js +2 -2
  92. package/lib/mocha/factory.js +5 -1
  93. package/lib/mocha/inject.js +1 -1
  94. package/lib/parser.js +2 -2
  95. package/lib/pause.js +38 -4
  96. package/lib/plugin/aiTrace.js +72 -84
  97. package/lib/plugin/browser.js +77 -0
  98. package/lib/plugin/expose.js +159 -0
  99. package/lib/plugin/heal.js +44 -1
  100. package/lib/plugin/pageInfo.js +51 -48
  101. package/lib/plugin/pause.js +131 -0
  102. package/lib/plugin/pauseOnFail.js +10 -34
  103. package/lib/plugin/screencast.js +287 -0
  104. package/lib/plugin/screenshot.js +563 -0
  105. package/lib/plugin/screenshotOnFail.js +8 -170
  106. package/lib/utils/pluginParser.js +151 -0
  107. package/lib/utils/trace.js +297 -0
  108. package/lib/utils.js +25 -0
  109. package/lib/workers.js +1 -15
  110. package/package.json +12 -10
  111. package/typings/index.d.ts +0 -5
  112. package/docs/webapi/amOnPage.mustache +0 -11
  113. package/docs/webapi/appendField.mustache +0 -16
  114. package/docs/webapi/attachFile.mustache +0 -24
  115. package/docs/webapi/blur.mustache +0 -18
  116. package/docs/webapi/checkOption.mustache +0 -13
  117. package/docs/webapi/clearCookie.mustache +0 -9
  118. package/docs/webapi/clearField.mustache +0 -14
  119. package/docs/webapi/click.mustache +0 -29
  120. package/docs/webapi/clickLink.mustache +0 -8
  121. package/docs/webapi/closeCurrentTab.mustache +0 -7
  122. package/docs/webapi/closeOtherTabs.mustache +0 -8
  123. package/docs/webapi/dontSee.mustache +0 -11
  124. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  125. package/docs/webapi/dontSeeCookie.mustache +0 -8
  126. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  127. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  128. package/docs/webapi/dontSeeElement.mustache +0 -12
  129. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  130. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  131. package/docs/webapi/dontSeeInField.mustache +0 -16
  132. package/docs/webapi/dontSeeInSource.mustache +0 -8
  133. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  134. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  135. package/docs/webapi/doubleClick.mustache +0 -13
  136. package/docs/webapi/downloadFile.mustache +0 -12
  137. package/docs/webapi/dragAndDrop.mustache +0 -9
  138. package/docs/webapi/dragSlider.mustache +0 -11
  139. package/docs/webapi/executeAsyncScript.mustache +0 -24
  140. package/docs/webapi/executeScript.mustache +0 -26
  141. package/docs/webapi/fillField.mustache +0 -21
  142. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  143. package/docs/webapi/focus.mustache +0 -13
  144. package/docs/webapi/forceClick.mustache +0 -28
  145. package/docs/webapi/forceRightClick.mustache +0 -18
  146. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  147. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  148. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  149. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  150. package/docs/webapi/grabCookie.mustache +0 -11
  151. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  152. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  153. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  154. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  155. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  156. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  157. package/docs/webapi/grabGeoLocation.mustache +0 -8
  158. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  159. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  160. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  161. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  162. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  163. package/docs/webapi/grabPopupText.mustache +0 -5
  164. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  165. package/docs/webapi/grabSource.mustache +0 -8
  166. package/docs/webapi/grabTextFrom.mustache +0 -10
  167. package/docs/webapi/grabTextFromAll.mustache +0 -9
  168. package/docs/webapi/grabTitle.mustache +0 -8
  169. package/docs/webapi/grabValueFrom.mustache +0 -9
  170. package/docs/webapi/grabValueFromAll.mustache +0 -8
  171. package/docs/webapi/grabWebElement.mustache +0 -9
  172. package/docs/webapi/grabWebElements.mustache +0 -9
  173. package/docs/webapi/moveCursorTo.mustache +0 -16
  174. package/docs/webapi/openNewTab.mustache +0 -7
  175. package/docs/webapi/pressKey.mustache +0 -12
  176. package/docs/webapi/pressKeyDown.mustache +0 -12
  177. package/docs/webapi/pressKeyUp.mustache +0 -12
  178. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  179. package/docs/webapi/refreshPage.mustache +0 -6
  180. package/docs/webapi/resizeWindow.mustache +0 -6
  181. package/docs/webapi/rightClick.mustache +0 -14
  182. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  183. package/docs/webapi/saveScreenshot.mustache +0 -12
  184. package/docs/webapi/say.mustache +0 -10
  185. package/docs/webapi/scrollIntoView.mustache +0 -11
  186. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  187. package/docs/webapi/scrollPageToTop.mustache +0 -6
  188. package/docs/webapi/scrollTo.mustache +0 -12
  189. package/docs/webapi/see.mustache +0 -11
  190. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  191. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  192. package/docs/webapi/seeCookie.mustache +0 -8
  193. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  194. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  195. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  196. package/docs/webapi/seeElement.mustache +0 -12
  197. package/docs/webapi/seeElementInDOM.mustache +0 -8
  198. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  199. package/docs/webapi/seeInField.mustache +0 -17
  200. package/docs/webapi/seeInPopup.mustache +0 -8
  201. package/docs/webapi/seeInSource.mustache +0 -7
  202. package/docs/webapi/seeInTitle.mustache +0 -8
  203. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  204. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  205. package/docs/webapi/seeTextEquals.mustache +0 -9
  206. package/docs/webapi/seeTitleEquals.mustache +0 -8
  207. package/docs/webapi/seeTraffic.mustache +0 -36
  208. package/docs/webapi/selectOption.mustache +0 -26
  209. package/docs/webapi/setCookie.mustache +0 -16
  210. package/docs/webapi/setGeoLocation.mustache +0 -12
  211. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  212. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  213. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  214. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  215. package/docs/webapi/switchTo.mustache +0 -9
  216. package/docs/webapi/switchToNextTab.mustache +0 -10
  217. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  218. package/docs/webapi/type.mustache +0 -21
  219. package/docs/webapi/uncheckOption.mustache +0 -13
  220. package/docs/webapi/wait.mustache +0 -8
  221. package/docs/webapi/waitForClickable.mustache +0 -11
  222. package/docs/webapi/waitForCookie.mustache +0 -9
  223. package/docs/webapi/waitForDetached.mustache +0 -10
  224. package/docs/webapi/waitForDisabled.mustache +0 -6
  225. package/docs/webapi/waitForElement.mustache +0 -11
  226. package/docs/webapi/waitForEnabled.mustache +0 -6
  227. package/docs/webapi/waitForFunction.mustache +0 -17
  228. package/docs/webapi/waitForInvisible.mustache +0 -10
  229. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  230. package/docs/webapi/waitForText.mustache +0 -13
  231. package/docs/webapi/waitForValue.mustache +0 -10
  232. package/docs/webapi/waitForVisible.mustache +0 -10
  233. package/docs/webapi/waitInUrl.mustache +0 -9
  234. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  235. package/docs/webapi/waitToHide.mustache +0 -10
  236. package/docs/webapi/waitUrlEquals.mustache +0 -10
  237. package/lib/helper/AI.js +0 -214
  238. package/lib/plugin/pauseOn.js +0 -167
  239. package/lib/plugin/stepByStepReport.js +0 -432
  240. package/lib/plugin/subtitles.js +0 -89
@@ -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
+ }