codeceptjs 4.0.0-rc.9 → 4.0.0

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 (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  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 +743 -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 +198 -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 +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +194 -2
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +63 -70
  126. package/lib/helper/Puppeteer.js +20 -109
  127. package/lib/helper/WebDriver.js +13 -30
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +10 -3
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +7 -0
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +0 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -1,39 +1,17 @@
1
- import event from '../event.js'
1
+ import output from '../output.js'
2
+ import pause from './pause.js'
2
3
 
3
- import pause from '../pause.js'
4
+ let warned = false
4
5
 
5
6
  /**
6
- * Automatically launches [interactive pause](/basics/#pause) when a test fails.
7
- *
8
- * Useful for debugging flaky tests on local environment.
9
- * Add this plugin to config file:
10
- *
11
- * ```js
12
- * plugins: {
13
- * pauseOnFail: {},
14
- * }
15
- * ```
16
- *
17
- * Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default.
18
- * Enable it manually on each run via `-p` option:
19
- *
20
- * ```
21
- * npx codeceptjs run -p pauseOnFail
22
- * ```
7
+ * Starts an interactive pause when a test fails.
23
8
  *
9
+ * **Deprecated:** use the `pause` plugin with `on: 'fail'`, which is the default behavior.
24
10
  */
25
- export default function() {
26
- let failed = false
27
-
28
- event.dispatcher.on(event.test.started, () => {
29
- failed = false
30
- })
31
-
32
- event.dispatcher.on(event.step.failed, () => {
33
- failed = true
34
- })
35
-
36
- event.dispatcher.on(event.test.after, () => {
37
- if (failed) pause()
38
- })
11
+ export default function (config = {}) {
12
+ if (!warned) {
13
+ output.error('pauseOnFail is deprecated; use the `pause` plugin (default on=fail).')
14
+ warned = true
15
+ }
16
+ return pause({ ...config, on: 'fail' })
39
17
  }
@@ -1,11 +1,17 @@
1
+ import debugModule from 'debug'
1
2
  import event from '../event.js'
2
3
  import recorder from '../recorder.js'
3
4
  import store from '../store.js'
4
5
 
6
+ const debug = debugModule('codeceptjs:retryFailedStep')
7
+
5
8
  const defaultConfig = {
6
9
  retries: 3,
7
10
  defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
11
+ minTimeout: 150,
12
+ maxTimeout: 10000,
8
13
  factor: 1.5,
14
+ randomize: false,
9
15
  ignoredSteps: [],
10
16
  deferToScenarioRetries: true,
11
17
  }
@@ -41,10 +47,9 @@ const RETRY_PRIORITIES = {
41
47
  * #### Configuration:
42
48
  *
43
49
  * * `retries` - number of retries (by default 3),
44
- * * `when` - function, when to perform a retry (accepts error as parameter)
45
50
  * * `factor` - The exponential factor to use. Default is 1.5.
46
- * * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
47
- * * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
51
+ * * `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
52
+ * * `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
48
53
  * * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
49
54
  * * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
50
55
  * * `amOnPage`
@@ -74,7 +79,7 @@ const RETRY_PRIORITIES = {
74
79
  *
75
80
  * #### Disable Per Test
76
81
  *
77
- * This plugin can be disabled per test. In this case you will need to stet `I.retry()` to all flaky steps:
82
+ * This plugin can be disabled per test. In this case you will need to add `step.retry()` to all flaky steps:
78
83
  *
79
84
  * Use scenario configuration to disable plugin for a test
80
85
  *
@@ -86,9 +91,8 @@ const RETRY_PRIORITIES = {
86
91
  *
87
92
  */
88
93
  export default function (config) {
89
- config = Object.assign(defaultConfig, config)
94
+ config = Object.assign({}, defaultConfig, config)
90
95
  config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
91
- const customWhen = config.when
92
96
 
93
97
  let enableRetry = false
94
98
 
@@ -98,7 +102,6 @@ export default function (config) {
98
102
  if (!store.autoRetries) return false
99
103
  if (err && err.isTerminal) return false
100
104
  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
101
- if (customWhen) return customWhen(err)
102
105
  return true
103
106
  }
104
107
  config.when = when
@@ -108,11 +111,12 @@ export default function (config) {
108
111
  }
109
112
 
110
113
  event.dispatcher.on(event.step.started, step => {
114
+ if (!step.title) return
111
115
  for (const ignored of config.ignoredSteps) {
112
- if (step.name === ignored) return
116
+ if (step.title === ignored) return
113
117
  if (ignored instanceof RegExp) {
114
- if (step.name.match(ignored)) return
115
- } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
118
+ if (step.title.match(ignored)) return
119
+ } else if (ignored.indexOf('*') && step.title.startsWith(ignored.slice(0, -1))) return
116
120
  }
117
121
  enableRetry = true
118
122
  })
@@ -147,9 +151,7 @@ export default function (config) {
147
151
  test.opts.conditionalRetries = config.retries
148
152
  test.opts.stepRetryPriority = stepRetryPriority
149
153
 
150
- if (process.env.DEBUG_RETRY_PLUGIN) {
151
- console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
152
- }
154
+ debug('applying retries = %d for test %s', config.retries, test.title)
153
155
  recorder.retry(config)
154
156
  })
155
157
 
@@ -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
+ }