codeceptjs 4.0.2-beta.9 → 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 +84 -41
  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,558 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { mkdirp } from 'mkdirp'
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 { fileExists, deleteDir, template } from '../utils.js'
13
+ import Codeceptjs from '../index.js'
14
+ import { testToFileName } from '../mocha/test.js'
15
+ import {
16
+ parsePluginArgs,
17
+ resolveTrigger,
18
+ matchStepFile,
19
+ matchUrl,
20
+ getBrowserHelper,
21
+ } from '../utils/pluginParser.js'
22
+
23
+ const defaultConfig = {
24
+ on: 'fail',
25
+ slides: false,
26
+ uniqueScreenshotNames: false,
27
+ disableScreenshots: false,
28
+ fullPageScreenshots: false,
29
+ animateSlides: true,
30
+ deleteSuccessful: true,
31
+ ignoreSteps: [],
32
+ }
33
+
34
+ /**
35
+ * Saves screenshots from the browser at points triggered by `on=`.
36
+ *
37
+ * Replaces the legacy `screenshotOnFail` plugin. Default `on=fail` preserves the
38
+ * old behavior (screenshot when a test fails). Pass `slides=true` (with `on=step`)
39
+ * to generate a step-by-step slideshow report — replaces the legacy
40
+ * `stepByStepReport` plugin.
41
+ *
42
+ * #### Configuration
43
+ *
44
+ * ```js
45
+ * plugins: {
46
+ * screenshot: {
47
+ * enabled: true,
48
+ * on: 'fail',
49
+ * }
50
+ * }
51
+ * ```
52
+ *
53
+ * #### `on=` modes
54
+ *
55
+ * * **fail** — screenshot when a test fails (default)
56
+ * * **test** — screenshot at the end of every test
57
+ * * **step** — screenshot after every step
58
+ * * **file** — screenshot for steps in `path=...[;line=...]`
59
+ * * **url** — screenshot when the current browser URL matches `pattern=...`
60
+ *
61
+ * Other config options:
62
+ *
63
+ * * `uniqueScreenshotNames`: use unique names for screenshot. Default: false.
64
+ * * `fullPageScreenshots`: make full page screenshots. Default: false.
65
+ * * `disableScreenshots`: legacy switch to skip the plugin entirely.
66
+ * * `slides`: generate a step-by-step slideshow report (requires `on=step`). Default: false.
67
+ * * `deleteSuccessful`: when `slides=true`, drop slideshow directories of passing tests. Default: true.
68
+ * * `animateSlides`: when `slides=true`, animate transitions between slides. Default: true.
69
+ * * `ignoreSteps`: when `slides=true`, RegExps of step names to skip in the slideshow.
70
+ *
71
+ * CLI examples:
72
+ *
73
+ * ```
74
+ * npx codeceptjs run -p screenshot
75
+ * npx codeceptjs run -p screenshot:on=step
76
+ * npx codeceptjs run -p screenshot:on=step;slides=true
77
+ * npx codeceptjs run -p screenshot:on=file:path=tests/login_test.js
78
+ * npx codeceptjs run -p screenshot:on=url:pattern=/users/*
79
+ * ```
80
+ */
81
+ export default function (config = {}) {
82
+ const helper = getBrowserHelper()
83
+ if (!helper) return
84
+
85
+ const cliArgs = parsePluginArgs(config._args)
86
+ const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'screenshot' })
87
+ if (!trigger) return
88
+
89
+ const options = Object.assign({}, defaultConfig, helper.options, config)
90
+ options.slides = cliArgs.slides ?? config.slides ?? defaultConfig.slides
91
+
92
+ if (Codeceptjs.container.mocha()) {
93
+ options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions
94
+ && Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
95
+ }
96
+
97
+ if (options.disableScreenshots) return
98
+
99
+ if (options.slides) {
100
+ return wireSlides(options, trigger)
101
+ }
102
+
103
+ switch (trigger.on) {
104
+ case 'fail':
105
+ return wireOnFail(options)
106
+ case 'test':
107
+ return wireOnTest(options)
108
+ case 'step':
109
+ return wireOnStep(options, () => true)
110
+ case 'file':
111
+ return wireOnStep(options, step => matchStepFile(step, trigger.path, trigger.line))
112
+ case 'url':
113
+ return wireOnUrl(options, trigger.pattern)
114
+ }
115
+ }
116
+
117
+ function wireOnFail(options) {
118
+ let currentTest = null
119
+ event.dispatcher.on(event.test.before, test => {
120
+ currentTest = test
121
+ })
122
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
123
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
124
+ const t = test || currentTest
125
+ if (!t) return
126
+ scheduleScreenshot(t, suffix(t, options, 'failed'), options)
127
+ })
128
+ }
129
+
130
+ function wireOnTest(options) {
131
+ event.dispatcher.on(event.test.after, test => {
132
+ if (!test) return
133
+ scheduleScreenshot(test, suffix(test, options, 'test'), options)
134
+ })
135
+ }
136
+
137
+ function wireOnStep(options, filter) {
138
+ let currentTest = null
139
+ let stepCount = 0
140
+ event.dispatcher.on(event.test.before, test => {
141
+ currentTest = test
142
+ stepCount = 0
143
+ })
144
+ event.dispatcher.on(event.step.after, step => {
145
+ if (!currentTest) return
146
+ if (!filter(step)) return
147
+ stepCount++
148
+ const name = `${testToFileName(currentTest, { suffix: '', unique: options.uniqueScreenshotNames })}.step_${stepCount}.png`
149
+ scheduleScreenshot(currentTest, name, options)
150
+ })
151
+ }
152
+
153
+ function wireOnUrl(options, pattern) {
154
+ let currentTest = null
155
+ let stepCount = 0
156
+ event.dispatcher.on(event.test.before, test => {
157
+ currentTest = test
158
+ stepCount = 0
159
+ })
160
+ event.dispatcher.on(event.step.after, () => {
161
+ if (!currentTest) return
162
+ const helper = getBrowserHelper()
163
+ if (!helper) return
164
+ recorder.add('screenshot:url check', async () => {
165
+ try {
166
+ const url = await helper.grabCurrentUrl()
167
+ if (!matchUrl(url, pattern)) return
168
+ stepCount++
169
+ const name = `${testToFileName(currentTest, { suffix: '', unique: options.uniqueScreenshotNames })}.url_${stepCount}.png`
170
+ await takeScreenshot(currentTest, name, options)
171
+ } catch (err) {
172
+ // page may not be ready
173
+ }
174
+ })
175
+ })
176
+ }
177
+
178
+ function suffix(test, options, kind) {
179
+ const base = testToFileName(test, { suffix: '', unique: options.uniqueScreenshotNames })
180
+ return `${base}.${kind}.png`
181
+ }
182
+
183
+ function scheduleScreenshot(test, fileName, options) {
184
+ recorder.add(
185
+ 'screenshot capture',
186
+ async () => takeScreenshot(test, fileName, options),
187
+ true,
188
+ )
189
+ }
190
+
191
+ async function takeScreenshot(test, fileName, options) {
192
+ const quietMode = !store.outputDir
193
+ if (!quietMode) {
194
+ output.plugin('screenshot', `Saving screenshot ${fileName}`)
195
+ }
196
+
197
+ const helper = getBrowserHelper()
198
+ if (!helper || typeof helper.saveScreenshot !== 'function') return
199
+
200
+ try {
201
+ if (options.reportDir) {
202
+ fileName = path.join(options.reportDir, fileName)
203
+ const mochaReportDir = path.resolve(process.cwd(), options.reportDir)
204
+ if (!fileExists(mochaReportDir)) fs.mkdirSync(mochaReportDir)
205
+ }
206
+
207
+ if (helper.page && helper.page.isClosed && helper.page.isClosed()) {
208
+ throw new Error('Browser page has been closed')
209
+ }
210
+ if (helper.browser && helper.browser.isConnected && !helper.browser.isConnected()) {
211
+ throw new Error('Browser has been disconnected')
212
+ }
213
+
214
+ const screenshotPromise = helper.saveScreenshot(fileName, options.fullPageScreenshots)
215
+ const timeoutPromise = new Promise((_, reject) => {
216
+ setTimeout(() => reject(new Error('Screenshot timeout after 5 seconds')), 5000)
217
+ })
218
+
219
+ await Promise.race([screenshotPromise, timeoutPromise])
220
+
221
+ if (!test.artifacts) test.artifacts = {}
222
+ const baseOutputDir = store.outputDir || null
223
+ if (baseOutputDir) {
224
+ test.artifacts.screenshot = path.join(baseOutputDir, fileName)
225
+ const mocha = Container.mocha()
226
+ const junit = mocha?.options?.reporterOptions?.['mocha-junit-reporter']
227
+ if (junit?.options?.attachments) {
228
+ test.attachments = [path.join(baseOutputDir, fileName)]
229
+ }
230
+ } else {
231
+ test.artifacts.screenshot = fileName
232
+ }
233
+ } catch (err) {
234
+ if (!quietMode) {
235
+ output.plugin('screenshot', `Failed to save screenshot: ${err.message}`)
236
+ }
237
+ if (
238
+ err
239
+ && ((err.message
240
+ && (err.message.includes('Target page, context or browser has been closed')
241
+ || err.message.includes('Browser page has been closed')
242
+ || err.message.includes('Browser has been disconnected')
243
+ || err.message.includes('was terminated due to')
244
+ || err.message.includes('no such window: target window already closed')
245
+ || err.message.includes('Screenshot timeout after')))
246
+ || (err.type && err.type === 'RuntimeError'))
247
+ ) {
248
+ output.log(`Can't make screenshot, ${err.message}`)
249
+ helper.isRunning = false
250
+ }
251
+ }
252
+ }
253
+
254
+ function wireSlides(options, trigger) {
255
+ const reportDir = options.output
256
+ ? path.resolve(store.codeceptDir, options.output)
257
+ : (store.outputDir || './_output')
258
+
259
+ const stepFilter = makeStepFilter(trigger, options)
260
+ const recordedTests = {}
261
+
262
+ let dir
263
+ let stepNum
264
+ let slides = {}
265
+ let savedStep = null
266
+ let currentTest = null
267
+ let scenarioFailed = false
268
+
269
+ event.dispatcher.on(event.suite.before, () => {
270
+ stepNum = -1
271
+ })
272
+
273
+ event.dispatcher.on(event.test.before, test => {
274
+ const hash = crypto.createHash('sha256').update(test.file + test.title).digest('hex')
275
+ dir = path.join(reportDir, `record_${hash}`)
276
+ mkdirp.sync(dir)
277
+ stepNum = 0
278
+ slides = {}
279
+ savedStep = null
280
+ currentTest = test
281
+ scenarioFailed = false
282
+ })
283
+
284
+ event.dispatcher.on(event.step.failed, step => {
285
+ recorder.add('slides: failed step', async () => persistStep(step), true)
286
+ })
287
+
288
+ event.dispatcher.on(event.step.after, step => {
289
+ recorder.add('slides: step', async () => persistStep(step), true)
290
+ })
291
+
292
+ event.dispatcher.on(event.test.passed, test => {
293
+ if (options.deleteSuccessful) {
294
+ deleteDir(dir)
295
+ return
296
+ }
297
+ persist(test)
298
+ })
299
+
300
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
301
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
302
+ persist(test)
303
+ })
304
+
305
+ event.dispatcher.on(event.all.result, () => {
306
+ if (Object.keys(recordedTests).length === 0) return
307
+ writeIndex(reportDir, recordedTests)
308
+ })
309
+
310
+ if (event.workers && event.workers.result) {
311
+ event.dispatcher.on(event.workers.result, async () => {
312
+ await recorder.add(() => {
313
+ const tests = scanRecordDirs(reportDir)
314
+ if (Object.keys(tests).length) writeIndex(reportDir, tests)
315
+ })
316
+ })
317
+ }
318
+
319
+ async function persistStep(step) {
320
+ if (stepNum === -1) return
321
+ if (savedStep === step) return
322
+ if (scenarioFailed) return
323
+ if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
324
+ if (!currentTest) return
325
+ if (!stepFilter(step)) return
326
+ if (isStepIgnored(step, options.ignoreSteps)) return
327
+
328
+ const fileName = `${String(stepNum).padStart(4, '0')}.png`
329
+ if (step.status === 'failed') scenarioFailed = true
330
+ stepNum++
331
+ slides[fileName] = step
332
+
333
+ const helper = getBrowserHelper()
334
+ if (!helper || typeof helper.saveScreenshot !== 'function') return
335
+
336
+ try {
337
+ const screenshotPath = path.join(dir, fileName)
338
+ await helper.saveScreenshot(screenshotPath, options.fullPageScreenshots)
339
+ step.artifacts = step.artifacts || {}
340
+ step.artifacts.screenshot = screenshotPath
341
+
342
+ currentTest.artifacts = currentTest.artifacts || {}
343
+ currentTest.artifacts.screenshots = currentTest.artifacts.screenshots || []
344
+ currentTest.artifacts.screenshots.push(screenshotPath)
345
+ } catch (err) {
346
+ output.plugin('screenshot', `Can't save step screenshot: ${err.message}`)
347
+ } finally {
348
+ savedStep = step
349
+ }
350
+ }
351
+
352
+ function persist(test) {
353
+ if (!Object.keys(slides).length) return
354
+
355
+ const slideHtml = Object.keys(slides)
356
+ .sort()
357
+ .map((fileName, idx) => {
358
+ const step = slides[fileName]
359
+ const caption = step.toString().replace(/\[\d{2}m/g, '')
360
+ const failed = step.status === 'failed' ? ' is-failed' : ''
361
+ return template(SLIDE_TEMPLATE, {
362
+ image: fileName,
363
+ caption,
364
+ index: idx + 1,
365
+ activeClass: idx === 0 ? ' is-active' : '',
366
+ failed,
367
+ })
368
+ })
369
+ .join('')
370
+
371
+ const dotHtml = Object.keys(slides)
372
+ .map((_, idx) => `<button type="button" class="slides__dot${idx === 0 ? ' is-active' : ''}" data-slide="${idx}" aria-label="Step ${idx + 1}"></button>`)
373
+ .join('')
374
+
375
+ const html = template(SLIDESHOW_TEMPLATE, {
376
+ title: test.title,
377
+ feature: (test.parent && test.parent.title) || '',
378
+ slides: slideHtml,
379
+ dots: dotHtml,
380
+ animate: options.animateSlides ? 'true' : 'false',
381
+ })
382
+
383
+ const indexFile = path.join(dir, 'index.html')
384
+ fs.writeFileSync(indexFile, html)
385
+ recordedTests[`${(test.parent && test.parent.title) || ''}: ${test.title}`] = path.relative(reportDir, indexFile)
386
+ }
387
+ }
388
+
389
+ function makeStepFilter(trigger, options) {
390
+ if (trigger.on === 'file' && trigger.path) {
391
+ return step => matchStepFile(step, trigger.path, trigger.line)
392
+ }
393
+ if (trigger.on === 'fail') {
394
+ return step => step.status === 'failed'
395
+ }
396
+ return () => true
397
+ }
398
+
399
+ function isStepIgnored(step, patterns) {
400
+ if (!patterns || !patterns.length) return false
401
+ for (const pattern of patterns) {
402
+ if (step.title && step.title.match(pattern)) return true
403
+ }
404
+ return false
405
+ }
406
+
407
+ function scanRecordDirs(reportDir) {
408
+ const out = {}
409
+ try {
410
+ for (const item of fs.readdirSync(reportDir, { withFileTypes: true })) {
411
+ if (!item.isDirectory() || !item.name.startsWith('record_')) continue
412
+ const indexFile = path.join(reportDir, item.name, 'index.html')
413
+ if (!fs.existsSync(indexFile)) continue
414
+ const html = fs.readFileSync(indexFile, 'utf-8')
415
+ const titleMatch = html.match(/<title>([^<]*)<\/title>/)
416
+ const label = titleMatch ? titleMatch[1].replace(/^Slides — /, '') : item.name
417
+ out[label] = `${item.name}/index.html`
418
+ }
419
+ } catch (err) {
420
+ // ignore
421
+ }
422
+ return out
423
+ }
424
+
425
+ function writeIndex(reportDir, recordedTests) {
426
+ const items = Object.entries(recordedTests)
427
+ .map(([name, href]) => `<li><a href="${href}">${escapeHtml(name)}</a></li>`)
428
+ .join('\n')
429
+
430
+ const html = template(INDEX_TEMPLATE, {
431
+ time: new Date().toString(),
432
+ records: items,
433
+ })
434
+
435
+ const indexPath = path.join(reportDir, 'records.html')
436
+ fs.writeFileSync(indexPath, html)
437
+ output.print(`Step-by-step preview: file://${indexPath}`)
438
+ }
439
+
440
+ function escapeHtml(s) {
441
+ return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
442
+ }
443
+
444
+ const SLIDE_TEMPLATE = `
445
+ <figure class="slides__slide{{activeClass}}{{failed}}" data-index="{{index}}">
446
+ <img class="slides__image" src="{{image}}" alt="">
447
+ <figcaption class="slides__caption">
448
+ <span class="slides__step">{{index}}</span>
449
+ <span class="slides__text">{{caption}}</span>
450
+ </figcaption>
451
+ </figure>
452
+ `
453
+
454
+ const SLIDESHOW_TEMPLATE = `<!DOCTYPE html>
455
+ <html lang="en">
456
+ <head>
457
+ <meta charset="utf-8">
458
+ <meta name="viewport" content="width=device-width, initial-scale=1">
459
+ <title>Slides — {{feature}}: {{title}}</title>
460
+ <style>
461
+ :root { color-scheme: dark; --bg: #0b0d10; --panel: #14181d; --fg: #e7ecef; --muted: #8a96a0; --accent: #ff5b00; --error: #c0392b; }
462
+ * { box-sizing: border-box; }
463
+ html, body { height: 100%; margin: 0; }
464
+ body { background: var(--bg); color: var(--fg); font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Inter, sans-serif; display: flex; flex-direction: column; }
465
+ header { padding: 14px 20px; background: var(--panel); border-bottom: 1px solid #1f262d; display: flex; align-items: baseline; gap: 16px; }
466
+ header a { color: var(--muted); text-decoration: none; font-weight: 500; }
467
+ header a:hover { color: var(--fg); }
468
+ header .feature { color: var(--muted); }
469
+ header .test { font-weight: 600; }
470
+ .slides { flex: 1; position: relative; overflow: hidden; }
471
+ .slides__slide { position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .25s ease; }
472
+ .slides[data-animate="false"] .slides__slide { transition: none; }
473
+ .slides__slide.is-active { opacity: 1; pointer-events: auto; }
474
+ .slides__image { max-width: 100%; max-height: 100%; object-fit: contain; box-shadow: 0 10px 40px rgba(0,0,0,.4); }
475
+ .slides__caption { position: absolute; left: 20px; right: 20px; bottom: 24px; padding: 12px 16px; background: rgba(20,24,29,.92); border: 1px solid #1f262d; border-radius: 6px; display: flex; gap: 12px; align-items: baseline; }
476
+ .slides__slide.is-failed .slides__caption { background: var(--error); border-color: var(--error); }
477
+ .slides__step { font-variant-numeric: tabular-nums; color: var(--muted); font-weight: 600; min-width: 2ch; }
478
+ .slides__slide.is-failed .slides__step { color: #ffd9d4; }
479
+ .slides__text { word-break: break-word; }
480
+ .nav { position: absolute; top: 0; bottom: 0; width: 25%; background: transparent; border: 0; cursor: pointer; color: transparent; }
481
+ .nav--prev { left: 0; }
482
+ .nav--next { right: 0; }
483
+ .dots { display: flex; gap: 6px; justify-content: center; padding: 12px; background: var(--panel); border-top: 1px solid #1f262d; flex-wrap: wrap; }
484
+ .slides__dot { width: 10px; height: 10px; border-radius: 50%; border: 0; background: #2a323a; cursor: pointer; padding: 0; }
485
+ .slides__dot.is-active { background: var(--accent); }
486
+ .slides__dot:hover { background: #3d4751; }
487
+ .slides__dot.is-active:hover { background: var(--accent); }
488
+ .hint { color: var(--muted); font-size: 12px; padding: 8px 20px; text-align: center; background: var(--panel); border-top: 1px solid #1f262d; }
489
+ </style>
490
+ </head>
491
+ <body>
492
+ <header>
493
+ <a href="../records.html">&laquo; back</a>
494
+ <span class="feature">{{feature}}</span>
495
+ <span class="test">{{title}}</span>
496
+ </header>
497
+ <div class="slides" data-animate="{{animate}}">
498
+ {{slides}}
499
+ <button class="nav nav--prev" type="button" aria-label="Previous">&larr;</button>
500
+ <button class="nav nav--next" type="button" aria-label="Next">&rarr;</button>
501
+ </div>
502
+ <nav class="dots">{{dots}}</nav>
503
+ <p class="hint">Use &larr; / &rarr; to navigate, click sides of image, or use the dots below.</p>
504
+ <script>
505
+ (function () {
506
+ var slidesEl = document.querySelector('.slides');
507
+ var slides = Array.prototype.slice.call(slidesEl.querySelectorAll('.slides__slide'));
508
+ var dots = Array.prototype.slice.call(document.querySelectorAll('.slides__dot'));
509
+ var idx = 0;
510
+ function show(i) {
511
+ if (i < 0) i = slides.length - 1;
512
+ if (i >= slides.length) i = 0;
513
+ slides[idx].classList.remove('is-active');
514
+ dots[idx] && dots[idx].classList.remove('is-active');
515
+ idx = i;
516
+ slides[idx].classList.add('is-active');
517
+ dots[idx] && dots[idx].classList.add('is-active');
518
+ }
519
+ document.querySelector('.nav--prev').addEventListener('click', function () { show(idx - 1); });
520
+ document.querySelector('.nav--next').addEventListener('click', function () { show(idx + 1); });
521
+ dots.forEach(function (d, i) { d.addEventListener('click', function () { show(i); }); });
522
+ document.addEventListener('keydown', function (e) {
523
+ if (e.key === 'ArrowLeft') show(idx - 1);
524
+ if (e.key === 'ArrowRight') show(idx + 1);
525
+ });
526
+ })();
527
+ </script>
528
+ </body>
529
+ </html>
530
+ `
531
+
532
+ const INDEX_TEMPLATE = `<!DOCTYPE html>
533
+ <html lang="en">
534
+ <head>
535
+ <meta charset="utf-8">
536
+ <meta name="viewport" content="width=device-width, initial-scale=1">
537
+ <title>Step-by-step Reports</title>
538
+ <style>
539
+ :root { color-scheme: dark; --bg: #0b0d10; --panel: #14181d; --fg: #e7ecef; --muted: #8a96a0; --accent: #ff5b00; }
540
+ * { box-sizing: border-box; }
541
+ body { background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Inter, sans-serif; max-width: 880px; margin: 0 auto; padding: 32px 24px; }
542
+ h1 { margin: 0 0 4px; font-size: 22px; font-weight: 600; }
543
+ .meta { color: var(--muted); margin-bottom: 24px; font-size: 13px; }
544
+ ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 4px; }
545
+ li { background: var(--panel); border: 1px solid #1f262d; border-radius: 6px; }
546
+ li a { display: block; padding: 12px 16px; color: var(--fg); text-decoration: none; }
547
+ li a:hover { background: #1c2229; border-color: var(--accent); }
548
+ </style>
549
+ </head>
550
+ <body>
551
+ <h1>Step-by-step Reports</h1>
552
+ <div class="meta">{{time}}</div>
553
+ <ul>
554
+ {{records}}
555
+ </ul>
556
+ </body>
557
+ </html>
558
+ `