codeceptjs 4.0.0-beta.1 → 4.0.0-beta.10.esm-aria

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 (207) hide show
  1. package/README.md +133 -120
  2. package/bin/codecept.js +107 -96
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/docs/webapi/click.mustache +5 -1
  6. package/lib/actor.js +71 -103
  7. package/lib/ai.js +159 -188
  8. package/lib/assert/empty.js +22 -24
  9. package/lib/assert/equal.js +30 -37
  10. package/lib/assert/error.js +14 -14
  11. package/lib/assert/include.js +43 -48
  12. package/lib/assert/throws.js +11 -11
  13. package/lib/assert/truth.js +22 -22
  14. package/lib/assert.js +20 -18
  15. package/lib/codecept.js +238 -162
  16. package/lib/colorUtils.js +50 -52
  17. package/lib/command/check.js +206 -0
  18. package/lib/command/configMigrate.js +56 -51
  19. package/lib/command/definitions.js +96 -109
  20. package/lib/command/dryRun.js +77 -79
  21. package/lib/command/generate.js +234 -194
  22. package/lib/command/gherkin/init.js +42 -33
  23. package/lib/command/gherkin/snippets.js +76 -74
  24. package/lib/command/gherkin/steps.js +20 -17
  25. package/lib/command/info.js +74 -38
  26. package/lib/command/init.js +300 -290
  27. package/lib/command/interactive.js +41 -32
  28. package/lib/command/list.js +28 -27
  29. package/lib/command/run-multiple/chunk.js +51 -48
  30. package/lib/command/run-multiple/collection.js +5 -5
  31. package/lib/command/run-multiple/run.js +5 -1
  32. package/lib/command/run-multiple.js +97 -97
  33. package/lib/command/run-rerun.js +19 -25
  34. package/lib/command/run-workers.js +68 -92
  35. package/lib/command/run.js +39 -27
  36. package/lib/command/utils.js +80 -64
  37. package/lib/command/workers/runTests.js +388 -226
  38. package/lib/config.js +124 -50
  39. package/lib/container.js +751 -260
  40. package/lib/data/context.js +60 -61
  41. package/lib/data/dataScenarioConfig.js +47 -47
  42. package/lib/data/dataTableArgument.js +32 -32
  43. package/lib/data/table.js +22 -22
  44. package/lib/effects.js +307 -0
  45. package/lib/element/WebElement.js +327 -0
  46. package/lib/els.js +160 -0
  47. package/lib/event.js +173 -163
  48. package/lib/globals.js +141 -0
  49. package/lib/heal.js +89 -85
  50. package/lib/helper/AI.js +131 -41
  51. package/lib/helper/ApiDataFactory.js +107 -75
  52. package/lib/helper/Appium.js +542 -404
  53. package/lib/helper/FileSystem.js +100 -79
  54. package/lib/helper/GraphQL.js +44 -43
  55. package/lib/helper/GraphQLDataFactory.js +52 -52
  56. package/lib/helper/JSONResponse.js +126 -88
  57. package/lib/helper/Mochawesome.js +54 -29
  58. package/lib/helper/Playwright.js +2547 -1316
  59. package/lib/helper/Puppeteer.js +1578 -1181
  60. package/lib/helper/REST.js +209 -68
  61. package/lib/helper/WebDriver.js +1482 -1342
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
  71. package/lib/helper/extras/Popup.js +22 -22
  72. package/lib/helper/extras/React.js +27 -28
  73. package/lib/helper/network/actions.js +36 -42
  74. package/lib/helper/network/utils.js +78 -84
  75. package/lib/helper/scripts/blurElement.js +5 -5
  76. package/lib/helper/scripts/focusElement.js +5 -5
  77. package/lib/helper/scripts/highlightElement.js +8 -8
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -3
  80. package/lib/history.js +23 -19
  81. package/lib/hooks.js +8 -8
  82. package/lib/html.js +94 -104
  83. package/lib/index.js +38 -27
  84. package/lib/listener/config.js +30 -23
  85. package/lib/listener/emptyRun.js +54 -0
  86. package/lib/listener/enhancedGlobalRetry.js +110 -0
  87. package/lib/listener/exit.js +16 -18
  88. package/lib/listener/globalRetry.js +70 -0
  89. package/lib/listener/globalTimeout.js +181 -0
  90. package/lib/listener/helpers.js +76 -51
  91. package/lib/listener/mocha.js +10 -11
  92. package/lib/listener/result.js +11 -0
  93. package/lib/listener/retryEnhancer.js +85 -0
  94. package/lib/listener/steps.js +71 -59
  95. package/lib/listener/store.js +20 -0
  96. package/lib/locator.js +214 -197
  97. package/lib/mocha/asyncWrapper.js +274 -0
  98. package/lib/mocha/bdd.js +167 -0
  99. package/lib/mocha/cli.js +341 -0
  100. package/lib/mocha/factory.js +163 -0
  101. package/lib/mocha/featureConfig.js +89 -0
  102. package/lib/mocha/gherkin.js +231 -0
  103. package/lib/mocha/hooks.js +121 -0
  104. package/lib/mocha/index.js +21 -0
  105. package/lib/mocha/inject.js +46 -0
  106. package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
  107. package/lib/mocha/suite.js +89 -0
  108. package/lib/mocha/test.js +184 -0
  109. package/lib/mocha/types.d.ts +42 -0
  110. package/lib/mocha/ui.js +242 -0
  111. package/lib/output.js +141 -71
  112. package/lib/parser.js +47 -44
  113. package/lib/pause.js +173 -145
  114. package/lib/plugin/analyze.js +403 -0
  115. package/lib/plugin/{autoLogin.js → auth.js} +178 -79
  116. package/lib/plugin/autoDelay.js +36 -40
  117. package/lib/plugin/coverage.js +131 -78
  118. package/lib/plugin/customLocator.js +22 -21
  119. package/lib/plugin/customReporter.js +53 -0
  120. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  121. package/lib/plugin/heal.js +101 -110
  122. package/lib/plugin/htmlReporter.js +3648 -0
  123. package/lib/plugin/pageInfo.js +140 -0
  124. package/lib/plugin/pauseOnFail.js +12 -11
  125. package/lib/plugin/retryFailedStep.js +82 -47
  126. package/lib/plugin/screenshotOnFail.js +111 -92
  127. package/lib/plugin/stepByStepReport.js +159 -101
  128. package/lib/plugin/stepTimeout.js +20 -25
  129. package/lib/plugin/subtitles.js +38 -38
  130. package/lib/recorder.js +193 -130
  131. package/lib/rerun.js +94 -49
  132. package/lib/result.js +238 -0
  133. package/lib/retryCoordinator.js +207 -0
  134. package/lib/secret.js +20 -18
  135. package/lib/session.js +95 -89
  136. package/lib/step/base.js +239 -0
  137. package/lib/step/comment.js +10 -0
  138. package/lib/step/config.js +50 -0
  139. package/lib/step/func.js +46 -0
  140. package/lib/step/helper.js +50 -0
  141. package/lib/step/meta.js +99 -0
  142. package/lib/step/record.js +74 -0
  143. package/lib/step/retry.js +11 -0
  144. package/lib/step/section.js +55 -0
  145. package/lib/step.js +18 -329
  146. package/lib/steps.js +54 -0
  147. package/lib/store.js +38 -7
  148. package/lib/template/heal.js +3 -12
  149. package/lib/template/prompts/generatePageObject.js +31 -0
  150. package/lib/template/prompts/healStep.js +13 -0
  151. package/lib/template/prompts/writeStep.js +9 -0
  152. package/lib/test-server.js +334 -0
  153. package/lib/timeout.js +60 -0
  154. package/lib/transform.js +8 -8
  155. package/lib/translation.js +34 -21
  156. package/lib/utils/mask_data.js +47 -0
  157. package/lib/utils.js +411 -228
  158. package/lib/workerStorage.js +37 -34
  159. package/lib/workers.js +532 -296
  160. package/package.json +115 -95
  161. package/translations/de-DE.js +5 -3
  162. package/translations/fr-FR.js +5 -4
  163. package/translations/index.js +22 -12
  164. package/translations/it-IT.js +4 -3
  165. package/translations/ja-JP.js +4 -3
  166. package/translations/nl-NL.js +76 -0
  167. package/translations/pl-PL.js +4 -3
  168. package/translations/pt-BR.js +4 -3
  169. package/translations/ru-RU.js +4 -3
  170. package/translations/utils.js +10 -0
  171. package/translations/zh-CN.js +4 -3
  172. package/translations/zh-TW.js +4 -3
  173. package/typings/index.d.ts +546 -185
  174. package/typings/promiseBasedTypes.d.ts +150 -879
  175. package/typings/types.d.ts +547 -996
  176. package/lib/cli.js +0 -249
  177. package/lib/dirname.js +0 -5
  178. package/lib/helper/Expect.js +0 -425
  179. package/lib/helper/ExpectHelper.js +0 -399
  180. package/lib/helper/MockServer.js +0 -223
  181. package/lib/helper/Nightmare.js +0 -1411
  182. package/lib/helper/Protractor.js +0 -1835
  183. package/lib/helper/SoftExpectHelper.js +0 -381
  184. package/lib/helper/TestCafe.js +0 -1410
  185. package/lib/helper/clientscripts/nightmare.js +0 -213
  186. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  187. package/lib/helper/testcafe/testcafe-utils.js +0 -63
  188. package/lib/interfaces/bdd.js +0 -98
  189. package/lib/interfaces/featureConfig.js +0 -69
  190. package/lib/interfaces/gherkin.js +0 -195
  191. package/lib/listener/artifacts.js +0 -19
  192. package/lib/listener/retry.js +0 -68
  193. package/lib/listener/timeout.js +0 -109
  194. package/lib/mochaFactory.js +0 -110
  195. package/lib/plugin/allure.js +0 -15
  196. package/lib/plugin/commentStep.js +0 -136
  197. package/lib/plugin/debugErrors.js +0 -67
  198. package/lib/plugin/eachElement.js +0 -127
  199. package/lib/plugin/fakerTransform.js +0 -49
  200. package/lib/plugin/retryTo.js +0 -121
  201. package/lib/plugin/selenoid.js +0 -371
  202. package/lib/plugin/standardActingHelpers.js +0 -9
  203. package/lib/plugin/tryTo.js +0 -105
  204. package/lib/plugin/wdio.js +0 -246
  205. package/lib/scenario.js +0 -222
  206. package/lib/ui.js +0 -238
  207. package/lib/within.js +0 -70
@@ -0,0 +1,140 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import Container from '../container.js'
4
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
5
+ import recorder from '../recorder.js'
6
+ import event from '../event.js'
7
+ import { scanForErrorMessages } from '../html.js'
8
+ import { output } from '../index.js'
9
+ import { humanizeString, ucfirst } from '../utils.js'
10
+ import { testToFileName } from '../mocha/test.js'
11
+ const defaultConfig = {
12
+ errorClasses: ['error', 'warning', 'alert', 'danger'],
13
+ browserLogs: ['error'],
14
+ }
15
+
16
+ /**
17
+ * Collects information from web page after each failed test and adds it to the test as an artifact.
18
+ * It is suggested to enable this plugin if you run tests on CI and you need to debug failed tests.
19
+ * This plugin can be paired with `analyze` plugin to provide more context.
20
+ *
21
+ * It collects URL, HTML errors (by classes), and browser logs.
22
+ *
23
+ * Enable this plugin in config:
24
+ *
25
+ * ```js
26
+ * plugins: {
27
+ * pageInfo: {
28
+ * enabled: true,
29
+ * }
30
+ * ```
31
+ *
32
+ * Additional config options:
33
+ *
34
+ * * `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`)
35
+ * * `browserLogs` - list of types of errors to search for in browser logs (default: `['error']`)
36
+ *
37
+ */
38
+ export default function (config = {}) {
39
+ const helpers = Container.helpers()
40
+ let helper
41
+
42
+ config = Object.assign(defaultConfig, config)
43
+
44
+ for (const helperName of supportedHelpers) {
45
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
46
+ helper = helpers[helperName]
47
+ }
48
+ }
49
+
50
+ if (!helper) return // no helpers for screenshot
51
+
52
+ event.dispatcher.on(event.test.failed, test => {
53
+ const pageState = {}
54
+
55
+ recorder.add('URL of failed test', async () => {
56
+ try {
57
+ const url = await helper.grabCurrentUrl()
58
+ pageState.url = url
59
+ } catch (err) {
60
+ // not really needed
61
+ }
62
+ })
63
+ recorder.add('HTML snapshot failed test', async () => {
64
+ try {
65
+ const html = await helper.grabHTMLFrom('body')
66
+
67
+ if (!html) return
68
+
69
+ const errors = scanForErrorMessages(html, config.errorClasses)
70
+ if (errors.length) {
71
+ output.debug('Detected errors in HTML code')
72
+ errors.forEach(error => output.debug(error))
73
+ pageState.htmlErrors = errors
74
+ }
75
+ } catch (err) {
76
+ // not really needed
77
+ }
78
+ })
79
+
80
+ recorder.add('Browser logs for failed test', async () => {
81
+ try {
82
+ const logs = await helper.grabBrowserLogs()
83
+
84
+ if (!logs) return
85
+
86
+ pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
87
+ } catch (err) {
88
+ // not really needed
89
+ }
90
+ })
91
+
92
+ recorder.add('Save page info', () => {
93
+ test.addNote('pageInfo', pageStateToMarkdown(pageState))
94
+
95
+ const pageStateFileName = path.join(global.output_dir, `${testToFileName(test)}.pageInfo.md`)
96
+ fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
97
+ test.artifacts.pageInfo = pageStateFileName
98
+ return pageState
99
+ })
100
+ })
101
+ }
102
+
103
+ function pageStateToMarkdown(pageState) {
104
+ let markdown = ''
105
+
106
+ for (const [key, value] of Object.entries(pageState)) {
107
+ if (!value) continue
108
+ let result = ''
109
+
110
+ if (Array.isArray(value)) {
111
+ result = value.map(v => `- ${JSON.stringify(v, null, 2)}`).join('\n')
112
+ } else if (typeof value === 'string') {
113
+ result = `${value}`
114
+ } else {
115
+ result = JSON.stringify(value, null, 2)
116
+ }
117
+
118
+ if (!result.trim()) continue
119
+
120
+ markdown += `### ${ucfirst(humanizeString(key))}\n\n`
121
+ markdown += result
122
+ markdown += '\n\n'
123
+ }
124
+
125
+ return markdown
126
+ }
127
+
128
+ function getBrowserErrors(logs, type = ['error']) {
129
+ // Playwright & WebDriver console messages
130
+ let errors = logs
131
+ .map(log => {
132
+ if (typeof log === 'string') return log
133
+ if (!log.type) return null
134
+ return { type: log.type(), text: log.text() }
135
+ })
136
+ .filter(l => l && (typeof l === 'string' || type.includes(l.type)))
137
+ .map(l => (typeof l === 'string' ? l : l.text))
138
+
139
+ return errors
140
+ }
@@ -1,5 +1,6 @@
1
- import * as event from '../event.js';
2
- import pause from '../pause';
1
+ import event from '../event.js'
2
+
3
+ import pause from '../pause.js'
3
4
 
4
5
  /**
5
6
  * Automatically launches [interactive pause](/basics/#pause) when a test fails.
@@ -21,18 +22,18 @@ import pause from '../pause';
21
22
  * ```
22
23
  *
23
24
  */
24
- export default () => {
25
- let failed = false;
25
+ export default function() {
26
+ let failed = false
26
27
 
27
28
  event.dispatcher.on(event.test.started, () => {
28
- failed = false;
29
- });
29
+ failed = false
30
+ })
30
31
 
31
32
  event.dispatcher.on(event.step.failed, () => {
32
- failed = true;
33
- });
33
+ failed = true
34
+ })
34
35
 
35
36
  event.dispatcher.on(event.test.after, () => {
36
- if (failed) pause();
37
- });
38
- };
37
+ if (failed) pause()
38
+ })
39
+ }
@@ -1,22 +1,15 @@
1
- import * as event from '../event.js';
2
- import recorder from '../recorder.js';
3
- import * as output from '../output.js';
1
+ import event from '../event.js'
4
2
 
5
- import { store } from '../store.js';
3
+ import recorder from '../recorder.js'
4
+
5
+ import store from '../store.js'
6
6
 
7
7
  const defaultConfig = {
8
8
  retries: 3,
9
- defaultIgnoredSteps: [
10
- 'amOnPage',
11
- 'wait*',
12
- 'send*',
13
- 'execute*',
14
- 'run*',
15
- 'have*',
16
- ],
9
+ defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
17
10
  factor: 1.5,
18
11
  ignoredSteps: [],
19
- };
12
+ }
20
13
 
21
14
  /**
22
15
  * Retries each failed step in a test.
@@ -78,51 +71,93 @@ const defaultConfig = {
78
71
  * Use scenario configuration to disable plugin for a test
79
72
  *
80
73
  * ```js
81
- * Scenario('scenario tite', () => {
74
+ * Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
82
75
  * // test goes here
83
- * }).config(test => test.disableRetryFailedStep = true)
76
+ * })
84
77
  * ```
85
78
  *
86
79
  */
87
- export default (config) => {
88
- config = Object.assign(defaultConfig, config);
89
- config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps);
90
- const customWhen = config.when;
80
+ export default function (config) {
81
+ config = Object.assign(defaultConfig, config)
82
+ config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
83
+ const customWhen = config.when
91
84
 
92
- let enableRetry = false;
85
+ let enableRetry = false
93
86
 
94
- const when = (err) => {
95
- if (!enableRetry) return;
96
- if (store.debugMode) return false;
97
- if (customWhen) return customWhen(err);
98
- return true;
99
- };
100
- config.when = when;
87
+ const when = err => {
88
+ if (!enableRetry) return
89
+ if (store.debugMode) return false
90
+ if (!store.autoRetries) return false
91
+ // Don't retry terminal errors (e.g., frame detachment errors)
92
+ if (err && err.isTerminal) return false
93
+ // Don't retry navigation errors that are known to be terminal
94
+ if (err && err.message && (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed'))) return false
95
+ if (customWhen) return customWhen(err)
96
+ return true
97
+ }
98
+ config.when = when
101
99
 
102
- event.dispatcher.on(event.step.started, (step) => {
103
- if (process.env.TRY_TO === 'true') {
104
- output.output.log('Info: RetryFailedStep plugin is disabled inside tryTo block');
105
- return;
106
- }
100
+ // Ensure retry options are available before any steps run
101
+ if (!recorder.retries.find(r => r === config)) {
102
+ recorder.retries.push(config)
103
+ }
107
104
 
105
+ event.dispatcher.on(event.step.started, step => {
108
106
  // if a step is ignored - return
109
107
  for (const ignored of config.ignoredSteps) {
110
- if (step.name === ignored) return;
108
+ if (step.name === ignored) return
111
109
  if (ignored instanceof RegExp) {
112
- if (step.name.match(ignored)) return;
113
- } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return;
110
+ if (step.name.match(ignored)) return
111
+ } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
112
+ }
113
+ enableRetry = true // enable retry for a step
114
+ })
115
+
116
+ // Disable retry only after a successful step; keep it enabled for failure so retry logic can act
117
+ event.dispatcher.on(event.step.passed, () => {
118
+ enableRetry = false
119
+ })
120
+
121
+ event.dispatcher.on(event.test.before, test => {
122
+ // pass disableRetryFailedStep is a preferred way to disable retries
123
+ // test.disableRetryFailedStep is used for backward compatibility
124
+ if (!test.opts) test.opts = {}
125
+ if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
126
+ store.autoRetries = false
127
+ return // disable retry when a test is not active
114
128
  }
115
- enableRetry = true; // enable retry for a step
116
- });
117
129
 
118
- event.dispatcher.on(event.step.finished, () => {
119
- enableRetry = false;
120
- });
130
+ // Don't apply plugin retry logic if there are already manual retries configured
131
+ // Check if any retry configs exist that aren't from this plugin
132
+ const hasManualRetries = recorder.retries.some(retry => retry !== config)
133
+ if (hasManualRetries) {
134
+ store.autoRetries = false
135
+ return
136
+ }
121
137
 
122
- event.dispatcher.on(event.test.before, (test) => {
123
- if (test && test.disableRetryFailedStep) return; // disable retry when a test is not active
124
- // this env var is used to set the retries inside _before() block of helpers
125
- process.env.FAILED_STEP_RETRIES = config.retries;
126
- recorder.retry(config);
127
- });
128
- };
138
+ // this option is used to set the retries inside _before() block of helpers
139
+ store.autoRetries = true
140
+ test.opts.conditionalRetries = config.retries
141
+ // debug: record applied retries value for tests
142
+ if (process.env.DEBUG_RETRY_PLUGIN) {
143
+ // eslint-disable-next-line no-console
144
+ console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
145
+ }
146
+ recorder.retry(config)
147
+ })
148
+
149
+ // Fallback for environments where event.test.before wasn't emitted (runner scenarios)
150
+ event.dispatcher.on(event.test.started, test => {
151
+ if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
152
+
153
+ // Don't apply plugin retry logic if there are already manual retries configured
154
+ // Check if any retry configs exist that aren't from this plugin
155
+ const hasManualRetries = recorder.retries.some(retry => retry !== config)
156
+ if (hasManualRetries) return
157
+
158
+ if (!store.autoRetries) {
159
+ store.autoRetries = true
160
+ test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
161
+ }
162
+ })
163
+ }
@@ -1,19 +1,25 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import Container from '../container.js';
4
- import recorder from '../recorder.js';
5
- import * as event from '../event.js';
6
- import * as output from '../output.js';
7
- import { fileExists, clearString } from '../utils.js';
8
- import * as Codeceptjs from '../index.js';
1
+ import fs from 'fs'
2
+ import path from 'path'
9
3
 
10
- import supportedHelpers from './standardActingHelpers.js';
4
+ import Container from '../container.js'
5
+
6
+ import recorder from '../recorder.js'
7
+
8
+ import event from '../event.js'
9
+
10
+ import output from '../output.js'
11
+
12
+ import { fileExists } from '../utils.js'
13
+ import Codeceptjs from '../index.js'
14
+ import { testToFileName } from '../mocha/test.js'
11
15
 
12
16
  const defaultConfig = {
13
17
  uniqueScreenshotNames: false,
14
18
  disableScreenshots: false,
15
19
  fullPageScreenshots: false,
16
- };
20
+ }
21
+
22
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
17
23
 
18
24
  /**
19
25
  * Creates screenshot on failure. Screenshot is saved into `output` directory.
@@ -42,118 +48,131 @@ const defaultConfig = {
42
48
  *
43
49
  */
44
50
  export default function (config) {
45
- const helpers = Container.helpers();
46
- let helper;
51
+ const helpers = Container.helpers()
52
+ let helper
47
53
 
48
54
  for (const helperName of supportedHelpers) {
49
55
  if (Object.keys(helpers).indexOf(helperName) > -1) {
50
- helper = helpers[helperName];
56
+ helper = helpers[helperName]
51
57
  }
52
58
  }
53
59
 
54
- if (!helper) return; // no helpers for screenshot
60
+ if (!helper) return // no helpers for screenshot
55
61
 
56
- const options = Object.assign(defaultConfig, helper.options, config);
62
+ const options = Object.assign(defaultConfig, helper.options, config)
57
63
 
58
64
  if (helpers.Mochawesome) {
59
65
  if (helpers.Mochawesome.config) {
60
- options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames;
66
+ options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames
61
67
  }
62
68
  }
63
69
 
64
- if (Codeceptjs.container.mocha) {
65
- options.reportDir = Codeceptjs.container.mocha.options.reporterOptions
66
- && Codeceptjs.container.mocha.options.reporterOptions.reportDir;
70
+ if (Codeceptjs.container.mocha()) {
71
+ options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions && Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
67
72
  }
68
73
 
69
74
  if (options.disableScreenshots) {
70
75
  // old version of disabling screenshots
71
- return;
76
+ return
72
77
  }
73
78
 
74
- event.dispatcher.on(event.test.failed, (test) => {
75
- if (test.ctx?._runnable.title.includes('hook: ')) {
76
- output.output.plugin('screenshotOnFail', 'BeforeSuite/AfterSuite do not have any access to the browser, hence it could not take screenshot.');
79
+ event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
80
+ if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
81
+ // no browser here
77
82
  return
78
83
  }
79
84
 
80
- recorder.add('screenshot of failed test', async () => {
81
- let fileName = clearString(test.title);
82
- const dataType = 'image/png';
83
- // This prevents data driven to be included in the failed screenshot file name
84
- if (fileName.indexOf('{') !== -1) {
85
- fileName = fileName.substr(0, (fileName.indexOf('{') - 3)).trim();
86
- }
87
- if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`);
88
- if (options.uniqueScreenshotNames && test) {
89
- const uuid = _getUUID(test);
90
- fileName = `${fileName.substring(0, 10)}_${uuid}.failed.png`;
91
- } else {
92
- fileName += '.failed.png';
93
- }
94
- output.output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot');
95
-
96
- try {
97
- if (options.reportDir) {
98
- fileName = path.join(options.reportDir, fileName);
99
- const mochaReportDir = path.resolve(process.cwd(), options.reportDir);
100
- if (!fileExists(mochaReportDir)) {
101
- fs.mkdirSync(mochaReportDir);
102
- }
85
+ recorder.add(
86
+ 'screenshot of failed test',
87
+ async () => {
88
+ const dataType = 'image/png'
89
+ // This prevents data driven to be included in the failed screenshot file name
90
+ let fileName
91
+
92
+ if (options.uniqueScreenshotNames && test) {
93
+ fileName = `${testToFileName(test, { suffix: '', unique: true })}.failed.png`
94
+ } else {
95
+ fileName = `${testToFileName(test, { suffix: '', unique: false })}.failed.png`
103
96
  }
104
- await helper.saveScreenshot(fileName, options.fullPageScreenshots);
105
-
106
- if (!test.artifacts) test.artifacts = {};
107
- test.artifacts.screenshot = path.join(global.output_dir, fileName);
108
- if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
109
- test.attachments = [path.join(global.output_dir, fileName)];
97
+ const quietMode = !('output_dir' in global) || !global.output_dir
98
+ if (!quietMode) {
99
+ output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot')
110
100
  }
111
101
 
112
- const allureReporter = Container.plugins('allure');
113
- if (allureReporter) {
114
- allureReporter.addAttachment('Main session - Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), dataType);
115
-
116
- if (helper.activeSessionName) {
117
- for (const sessionName in helper.sessionPages) {
118
- const screenshotFileName = `${sessionName}_${fileName}`;
119
- test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName);
120
- allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType);
121
- }
102
+ // Re-check helpers at runtime in case they weren't ready during plugin init
103
+ const runtimeHelpers = Container.helpers()
104
+ let runtimeHelper = null
105
+ for (const helperName of supportedHelpers) {
106
+ if (Object.keys(runtimeHelpers).indexOf(helperName) > -1) {
107
+ runtimeHelper = runtimeHelpers[helperName]
108
+ break
122
109
  }
123
110
  }
124
111
 
125
- const cucumberReporter = Container.plugins('cucumberJsonReporter');
126
- if (cucumberReporter) {
127
- cucumberReporter.addScreenshot(test.artifacts.screenshot);
128
- }
129
- } catch (err) {
130
- output.output.plugin(err);
131
- if (
132
- err
133
- && err.type
134
- && err.type === 'RuntimeError'
135
- && err.message
136
- && (
137
- err.message.indexOf('was terminated due to') > -1
138
- || err.message.indexOf('no such window: target window already closed') > -1
139
- )
140
- ) {
141
- output.output.log(`Can't make screenshot, ${err}`);
142
- helper.isRunning = false;
112
+ if (runtimeHelper && typeof runtimeHelper.saveScreenshot === 'function') {
113
+ helper = runtimeHelper
143
114
  }
144
- }
145
- }, true);
146
- });
147
115
 
148
- function _getUUID(test) {
149
- if (test.uuid) {
150
- return test.uuid;
151
- }
116
+ try {
117
+ if (options.reportDir) {
118
+ fileName = path.join(options.reportDir, fileName)
119
+ const mochaReportDir = path.resolve(process.cwd(), options.reportDir)
120
+ if (!fileExists(mochaReportDir)) {
121
+ fs.mkdirSync(mochaReportDir)
122
+ }
123
+ }
152
124
 
153
- if (test.ctx && test.ctx.test?.uuid) {
154
- return test.ctx.test.uuid;
155
- }
125
+ // Check if browser/page is still available before attempting screenshot
126
+ if (helper.page && helper.page.isClosed && helper.page.isClosed()) {
127
+ throw new Error('Browser page has been closed')
128
+ }
129
+ if (helper.browser && helper.browser.isConnected && !helper.browser.isConnected()) {
130
+ throw new Error('Browser has been disconnected')
131
+ }
156
132
 
157
- return Math.floor(new Date().getTime() / 1000);
158
- }
133
+ // Add timeout wrapper to prevent hanging with shorter timeout for ESM
134
+ const screenshotPromise = helper.saveScreenshot(fileName, options.fullPageScreenshots)
135
+ const timeoutPromise = new Promise((_, reject) => {
136
+ setTimeout(() => reject(new Error('Screenshot timeout after 5 seconds')), 5000)
137
+ })
138
+
139
+ await Promise.race([screenshotPromise, timeoutPromise])
140
+
141
+ if (!test.artifacts) test.artifacts = {}
142
+ // Some unit tests may not define global.output_dir; avoid throwing when it is undefined
143
+ // Detect output directory safely (may not be initialized in narrow unit tests)
144
+ const baseOutputDir = 'output_dir' in global && typeof global.output_dir === 'string' && global.output_dir ? global.output_dir : null
145
+ if (baseOutputDir) {
146
+ test.artifacts.screenshot = path.join(baseOutputDir, fileName)
147
+ if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
148
+ test.attachments = [path.join(baseOutputDir, fileName)]
149
+ }
150
+ } else {
151
+ // Fallback: just store the file name to keep tests stable without triggering path errors
152
+ test.artifacts.screenshot = fileName
153
+ }
154
+ } catch (err) {
155
+ if (!quietMode) {
156
+ output.plugin('screenshotOnFail', `Failed to save screenshot: ${err.message}`)
157
+ }
158
+ // Enhanced error handling for browser closed scenarios
159
+ if (
160
+ err &&
161
+ ((err.message &&
162
+ (err.message.includes('Target page, context or browser has been closed') ||
163
+ err.message.includes('Browser page has been closed') ||
164
+ err.message.includes('Browser has been disconnected') ||
165
+ err.message.includes('was terminated due to') ||
166
+ err.message.includes('no such window: target window already closed') ||
167
+ err.message.includes('Screenshot timeout after'))) ||
168
+ (err.type && err.type === 'RuntimeError'))
169
+ ) {
170
+ output.log(`Can't make screenshot, ${err.message}`)
171
+ helper.isRunning = false
172
+ }
173
+ }
174
+ },
175
+ true,
176
+ )
177
+ })
159
178
  }