codeceptjs 4.0.0-beta.4 → 4.0.0-beta.6.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 (188) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +53 -54
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +70 -102
  5. package/lib/ai.js +131 -121
  6. package/lib/assert/empty.js +11 -12
  7. package/lib/assert/equal.js +16 -21
  8. package/lib/assert/error.js +2 -2
  9. package/lib/assert/include.js +11 -15
  10. package/lib/assert/throws.js +3 -5
  11. package/lib/assert/truth.js +10 -7
  12. package/lib/assert.js +18 -18
  13. package/lib/codecept.js +112 -101
  14. package/lib/colorUtils.js +48 -50
  15. package/lib/command/check.js +206 -0
  16. package/lib/command/configMigrate.js +13 -14
  17. package/lib/command/definitions.js +24 -36
  18. package/lib/command/dryRun.js +16 -16
  19. package/lib/command/generate.js +38 -39
  20. package/lib/command/gherkin/init.js +36 -38
  21. package/lib/command/gherkin/snippets.js +76 -74
  22. package/lib/command/gherkin/steps.js +21 -18
  23. package/lib/command/info.js +49 -15
  24. package/lib/command/init.js +41 -37
  25. package/lib/command/interactive.js +22 -13
  26. package/lib/command/list.js +11 -10
  27. package/lib/command/run-multiple/chunk.js +50 -47
  28. package/lib/command/run-multiple/collection.js +5 -5
  29. package/lib/command/run-multiple/run.js +3 -3
  30. package/lib/command/run-multiple.js +27 -47
  31. package/lib/command/run-rerun.js +6 -7
  32. package/lib/command/run-workers.js +15 -66
  33. package/lib/command/run.js +8 -8
  34. package/lib/command/utils.js +22 -21
  35. package/lib/command/workers/runTests.js +131 -241
  36. package/lib/config.js +111 -49
  37. package/lib/container.js +589 -244
  38. package/lib/data/context.js +16 -18
  39. package/lib/data/dataScenarioConfig.js +9 -9
  40. package/lib/data/dataTableArgument.js +7 -7
  41. package/lib/data/table.js +6 -12
  42. package/lib/effects.js +307 -0
  43. package/lib/els.js +160 -0
  44. package/lib/event.js +24 -19
  45. package/lib/globals.js +141 -0
  46. package/lib/heal.js +89 -81
  47. package/lib/helper/AI.js +3 -2
  48. package/lib/helper/ApiDataFactory.js +19 -19
  49. package/lib/helper/Appium.js +47 -51
  50. package/lib/helper/FileSystem.js +35 -15
  51. package/lib/helper/GraphQL.js +1 -1
  52. package/lib/helper/GraphQLDataFactory.js +4 -4
  53. package/lib/helper/JSONResponse.js +72 -45
  54. package/lib/helper/Mochawesome.js +14 -11
  55. package/lib/helper/Playwright.js +832 -434
  56. package/lib/helper/Puppeteer.js +393 -292
  57. package/lib/helper/REST.js +32 -27
  58. package/lib/helper/WebDriver.js +320 -219
  59. package/lib/helper/errors/ConnectionRefused.js +6 -6
  60. package/lib/helper/errors/ElementAssertion.js +11 -16
  61. package/lib/helper/errors/ElementNotFound.js +5 -9
  62. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  63. package/lib/helper/extras/Console.js +11 -11
  64. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  65. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  66. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  67. package/lib/helper/extras/Popup.js +22 -22
  68. package/lib/helper/extras/React.js +29 -30
  69. package/lib/helper/network/actions.js +33 -48
  70. package/lib/helper/network/utils.js +76 -83
  71. package/lib/helper/scripts/blurElement.js +6 -6
  72. package/lib/helper/scripts/focusElement.js +6 -6
  73. package/lib/helper/scripts/highlightElement.js +9 -9
  74. package/lib/helper/scripts/isElementClickable.js +34 -34
  75. package/lib/helper.js +2 -1
  76. package/lib/history.js +23 -20
  77. package/lib/hooks.js +10 -10
  78. package/lib/html.js +90 -100
  79. package/lib/index.js +48 -21
  80. package/lib/listener/config.js +8 -9
  81. package/lib/listener/emptyRun.js +54 -0
  82. package/lib/listener/exit.js +10 -12
  83. package/lib/listener/{retry.js → globalRetry.js} +10 -10
  84. package/lib/listener/globalTimeout.js +166 -0
  85. package/lib/listener/helpers.js +43 -24
  86. package/lib/listener/mocha.js +4 -5
  87. package/lib/listener/result.js +11 -0
  88. package/lib/listener/steps.js +26 -23
  89. package/lib/listener/store.js +20 -0
  90. package/lib/locator.js +213 -192
  91. package/lib/mocha/asyncWrapper.js +264 -0
  92. package/lib/mocha/bdd.js +167 -0
  93. package/lib/mocha/cli.js +341 -0
  94. package/lib/mocha/factory.js +160 -0
  95. package/lib/{interfaces → mocha}/featureConfig.js +33 -13
  96. package/lib/{interfaces → mocha}/gherkin.js +75 -45
  97. package/lib/mocha/hooks.js +121 -0
  98. package/lib/mocha/index.js +21 -0
  99. package/lib/mocha/inject.js +46 -0
  100. package/lib/{interfaces → mocha}/scenarioConfig.js +32 -8
  101. package/lib/mocha/suite.js +89 -0
  102. package/lib/mocha/test.js +178 -0
  103. package/lib/mocha/types.d.ts +42 -0
  104. package/lib/mocha/ui.js +229 -0
  105. package/lib/output.js +86 -64
  106. package/lib/parser.js +44 -44
  107. package/lib/pause.js +160 -139
  108. package/lib/plugin/analyze.js +403 -0
  109. package/lib/plugin/{autoLogin.js → auth.js} +137 -43
  110. package/lib/plugin/autoDelay.js +19 -15
  111. package/lib/plugin/coverage.js +22 -27
  112. package/lib/plugin/customLocator.js +5 -5
  113. package/lib/plugin/customReporter.js +53 -0
  114. package/lib/plugin/heal.js +49 -17
  115. package/lib/plugin/pageInfo.js +140 -0
  116. package/lib/plugin/pauseOnFail.js +4 -3
  117. package/lib/plugin/retryFailedStep.js +60 -19
  118. package/lib/plugin/screenshotOnFail.js +80 -83
  119. package/lib/plugin/stepByStepReport.js +70 -31
  120. package/lib/plugin/stepTimeout.js +7 -13
  121. package/lib/plugin/subtitles.js +10 -9
  122. package/lib/recorder.js +167 -126
  123. package/lib/rerun.js +94 -50
  124. package/lib/result.js +161 -0
  125. package/lib/secret.js +18 -17
  126. package/lib/session.js +95 -89
  127. package/lib/step/base.js +239 -0
  128. package/lib/step/comment.js +10 -0
  129. package/lib/step/config.js +50 -0
  130. package/lib/step/func.js +46 -0
  131. package/lib/step/helper.js +50 -0
  132. package/lib/step/meta.js +99 -0
  133. package/lib/step/record.js +74 -0
  134. package/lib/step/retry.js +11 -0
  135. package/lib/step/section.js +55 -0
  136. package/lib/step.js +18 -332
  137. package/lib/steps.js +54 -0
  138. package/lib/store.js +37 -5
  139. package/lib/template/heal.js +2 -11
  140. package/lib/timeout.js +60 -0
  141. package/lib/transform.js +8 -8
  142. package/lib/translation.js +32 -18
  143. package/lib/utils.js +354 -250
  144. package/lib/workerStorage.js +16 -16
  145. package/lib/workers.js +366 -282
  146. package/package.json +107 -95
  147. package/translations/de-DE.js +5 -4
  148. package/translations/fr-FR.js +5 -4
  149. package/translations/index.js +23 -9
  150. package/translations/it-IT.js +5 -4
  151. package/translations/ja-JP.js +5 -4
  152. package/translations/nl-NL.js +76 -0
  153. package/translations/pl-PL.js +5 -4
  154. package/translations/pt-BR.js +5 -4
  155. package/translations/ru-RU.js +5 -4
  156. package/translations/utils.js +18 -0
  157. package/translations/zh-CN.js +5 -4
  158. package/translations/zh-TW.js +5 -4
  159. package/typings/index.d.ts +177 -186
  160. package/typings/promiseBasedTypes.d.ts +3573 -5941
  161. package/typings/types.d.ts +4042 -6370
  162. package/lib/cli.js +0 -256
  163. package/lib/helper/ExpectHelper.js +0 -391
  164. package/lib/helper/Nightmare.js +0 -1504
  165. package/lib/helper/Protractor.js +0 -1863
  166. package/lib/helper/SoftExpectHelper.js +0 -381
  167. package/lib/helper/TestCafe.js +0 -1414
  168. package/lib/helper/clientscripts/nightmare.js +0 -213
  169. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  170. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  171. package/lib/helper/testcafe/testcafe-utils.js +0 -62
  172. package/lib/interfaces/bdd.js +0 -81
  173. package/lib/listener/artifacts.js +0 -19
  174. package/lib/listener/timeout.js +0 -109
  175. package/lib/mochaFactory.js +0 -113
  176. package/lib/plugin/allure.js +0 -15
  177. package/lib/plugin/commentStep.js +0 -136
  178. package/lib/plugin/debugErrors.js +0 -67
  179. package/lib/plugin/eachElement.js +0 -127
  180. package/lib/plugin/fakerTransform.js +0 -49
  181. package/lib/plugin/retryTo.js +0 -127
  182. package/lib/plugin/selenoid.js +0 -384
  183. package/lib/plugin/standardActingHelpers.js +0 -3
  184. package/lib/plugin/tryTo.js +0 -115
  185. package/lib/plugin/wdio.js +0 -249
  186. package/lib/scenario.js +0 -224
  187. package/lib/ui.js +0 -236
  188. package/lib/within.js +0 -70
@@ -1,9 +1,13 @@
1
- const Container = require('../container')
2
- const store = require('../store')
3
- const recorder = require('../recorder')
4
- const event = require('../event')
5
- const log = require('../output').log
6
- const supportedHelpers = require('./standardActingHelpers').slice()
1
+ import Container from '../container.js'
2
+
3
+ import store from '../store.js'
4
+
5
+ import recorder from '../recorder.js'
6
+
7
+ import event from '../event.js'
8
+
9
+ import output from '../output.js'
10
+ const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
7
11
 
8
12
  const methodsToDelay = ['click', 'fillField', 'checkOption', 'pressKey', 'doubleClick', 'rightClick']
9
13
 
@@ -51,14 +55,14 @@ const defaultConfig = {
51
55
  * * `delayAfter`: put a delay after a command. 200ms by default
52
56
  *
53
57
  */
54
- module.exports = function (config) {
55
- supportedHelpers.push('REST')
58
+ export default function (config) {
59
+ const affectedHelpers = [...standardActingHelpers, 'REST']
56
60
  const helpers = Container.helpers()
57
61
  let helper
58
62
 
59
63
  config = Object.assign(defaultConfig, config)
60
64
 
61
- for (const helperName of supportedHelpers) {
65
+ for (const helperName of affectedHelpers) {
62
66
  if (Object.keys(helpers).indexOf(helperName) > -1) {
63
67
  helper = helpers[helperName]
64
68
  }
@@ -66,25 +70,25 @@ module.exports = function (config) {
66
70
 
67
71
  if (!helper) return // no helpers for auto-delay
68
72
 
69
- event.dispatcher.on(event.step.before, (step) => {
73
+ event.dispatcher.on(event.step.before, step => {
70
74
  if (config.methods.indexOf(step.helperMethod) < 0) return // skip non-actions
71
75
 
72
76
  recorder.add('auto-delay', async () => {
73
77
  if (store.debugMode) return // no need to delay in debug
74
- log(`Delaying for ${config.delayBefore}ms`)
75
- return new Promise((resolve) => {
78
+ output.log(`Delaying for ${config.delayBefore}ms`)
79
+ return new Promise(resolve => {
76
80
  setTimeout(resolve, config.delayBefore)
77
81
  })
78
82
  })
79
83
  })
80
84
 
81
- event.dispatcher.on(event.step.after, (step) => {
85
+ event.dispatcher.on(event.step.after, step => {
82
86
  if (config.methods.indexOf(step.helperMethod) < 0) return // skip non-actions
83
87
 
84
88
  recorder.add('auto-delay', async () => {
85
89
  if (store.debugMode) return // no need to delay in debug
86
- log(`Delaying for ${config.delayAfter}ms`)
87
- return new Promise((resolve) => {
90
+ output.log(`Delaying for ${config.delayAfter}ms`)
91
+ return new Promise(resolve => {
88
92
  setTimeout(resolve, config.delayAfter)
89
93
  })
90
94
  })
@@ -1,10 +1,14 @@
1
- const debugModule = require('debug')
2
- const { CoverageReport } = require('monocart-coverage-reports')
3
- const Container = require('../container')
4
- const recorder = require('../recorder')
5
- const event = require('../event')
6
- const output = require('../output')
7
- const { deepMerge } = require('../utils')
1
+ import debugModule from 'debug'
2
+ import { CoverageReport } from 'monocart-coverage-reports'
3
+ import Container from '../container.js'
4
+
5
+ import recorder from '../recorder.js'
6
+
7
+ import event from '../event.js'
8
+
9
+ import output from '../output.js'
10
+
11
+ import { deepMerge } from '../utils.js'
8
12
 
9
13
  const defaultConfig = {
10
14
  name: 'CodeceptJS Coverage Report',
@@ -15,7 +19,7 @@ const supportedHelpers = ['Puppeteer', 'Playwright', 'WebDriver']
15
19
 
16
20
  const v8CoverageHelpers = {
17
21
  Playwright: {
18
- startCoverage: async (page) => {
22
+ startCoverage: async page => {
19
23
  await Promise.all([
20
24
  page.coverage.startJSCoverage({
21
25
  resetOnNavigation: false,
@@ -26,16 +30,13 @@ const v8CoverageHelpers = {
26
30
  ])
27
31
  },
28
32
  takeCoverage: async (page, coverageReport) => {
29
- const [jsCoverage, cssCoverage] = await Promise.all([
30
- page.coverage.stopJSCoverage(),
31
- page.coverage.stopCSSCoverage(),
32
- ])
33
+ const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage()])
33
34
  const coverageList = [...jsCoverage, ...cssCoverage]
34
35
  await coverageReport.add(coverageList)
35
36
  },
36
37
  },
37
38
  Puppeteer: {
38
- startCoverage: async (page) => {
39
+ startCoverage: async page => {
39
40
  await Promise.all([
40
41
  page.coverage.startJSCoverage({
41
42
  resetOnNavigation: false,
@@ -47,13 +48,10 @@ const v8CoverageHelpers = {
47
48
  ])
48
49
  },
49
50
  takeCoverage: async (page, coverageReport) => {
50
- const [jsCoverage, cssCoverage] = await Promise.all([
51
- page.coverage.stopJSCoverage(),
52
- page.coverage.stopCSSCoverage(),
53
- ])
51
+ const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage()])
54
52
  // to raw V8 script coverage
55
53
  const coverageList = [
56
- ...jsCoverage.map((it) => {
54
+ ...jsCoverage.map(it => {
57
55
  return {
58
56
  source: it.text,
59
57
  ...it.rawScriptCoverage,
@@ -65,7 +63,7 @@ const v8CoverageHelpers = {
65
63
  },
66
64
  },
67
65
  WebDriver: {
68
- startCoverage: async (page) => {
66
+ startCoverage: async page => {
69
67
  await Promise.all([
70
68
  page.coverage.startJSCoverage({
71
69
  resetOnNavigation: false,
@@ -77,13 +75,10 @@ const v8CoverageHelpers = {
77
75
  ])
78
76
  },
79
77
  takeCoverage: async (page, coverageReport) => {
80
- const [jsCoverage, cssCoverage] = await Promise.all([
81
- page.coverage.stopJSCoverage(),
82
- page.coverage.stopCSSCoverage(),
83
- ])
78
+ const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage()])
84
79
  // to raw V8 script coverage
85
80
  const coverageList = [
86
- ...jsCoverage.map((it) => {
81
+ ...jsCoverage.map(it => {
87
82
  return {
88
83
  source: it.text,
89
84
  ...it.rawScriptCoverage,
@@ -122,7 +117,7 @@ const v8CoverageHelpers = {
122
117
  * * `sourcePath`: option to resolve a custom path.
123
118
  *
124
119
  */
125
- module.exports = function (config) {
120
+ export default function (config) {
126
121
  config = deepMerge(defaultConfig, config)
127
122
 
128
123
  if (config.debug) config.logging = 'debug'
@@ -131,7 +126,7 @@ module.exports = function (config) {
131
126
  let coverageRunning = false
132
127
 
133
128
  const v8Names = Object.keys(v8CoverageHelpers)
134
- const helperName = Object.keys(helpers).find((it) => v8Names.includes(it))
129
+ const helperName = Object.keys(helpers).find(it => v8Names.includes(it))
135
130
  if (!helperName) {
136
131
  console.error(`Coverage is only supported in ${supportedHelpers.join(' or ')}`)
137
132
  // no helpers for screenshot
@@ -180,7 +175,7 @@ module.exports = function (config) {
180
175
  })
181
176
 
182
177
  // Save coverage data after every test run
183
- event.dispatcher.on(event.test.after, (test) => {
178
+ event.dispatcher.on(event.test.after, test => {
184
179
  recorder.add(
185
180
  'take coverage',
186
181
  async () => {
@@ -1,5 +1,5 @@
1
- const Locator = require('../locator')
2
- const { xpathLocator } = require('../utils')
1
+ import Locator from '../locator.js'
2
+ import { xpathLocator } from '../utils.js'
3
3
 
4
4
  const defaultConfig = {
5
5
  prefix: '$',
@@ -111,7 +111,7 @@ const defaultConfig = {
111
111
  * I.click('=sign-up'); // matches => [data-qa=sign-up],[data-test=sign-up]
112
112
  * ```
113
113
  */
114
- module.exports = (config) => {
114
+ export default function (config) {
115
115
  config = { ...defaultConfig, ...config }
116
116
 
117
117
  Locator.addFilter((value, locatorObj) => {
@@ -125,7 +125,7 @@ module.exports = (config) => {
125
125
  if (config.strategy.toLowerCase() === 'xpath') {
126
126
  locatorObj.value = `.//*[${[]
127
127
  .concat(config.attribute)
128
- .map((attr) => `@${attr}=${xpathLocator.literal(val)}`)
128
+ .map(attr => `@${attr}=${xpathLocator.literal(val)}`)
129
129
  .join(' or ')}]`
130
130
  locatorObj.type = 'xpath'
131
131
  }
@@ -133,7 +133,7 @@ module.exports = (config) => {
133
133
  if (config.strategy.toLowerCase() === 'css') {
134
134
  locatorObj.value = []
135
135
  .concat(config.attribute)
136
- .map((attr) => `[${attr}=${val}]`)
136
+ .map(attr => `[${attr}=${val}]`)
137
137
  .join(',')
138
138
  locatorObj.type = 'css'
139
139
  }
@@ -0,0 +1,53 @@
1
+ import event from '../event.js'
2
+
3
+
4
+ /**
5
+ * Sample custom reporter for CodeceptJS.
6
+ */
7
+ export default function (config) {
8
+ event.dispatcher.on(event.hook.finished, hook => {
9
+ if (config.onHookFinished) {
10
+ config.onHookFinished(hook)
11
+ }
12
+ })
13
+
14
+ event.dispatcher.on(event.test.before, test => {
15
+ if (config.onTestBefore) {
16
+ config.onTestBefore(test)
17
+ }
18
+ })
19
+
20
+ event.dispatcher.on(event.test.failed, (test, err) => {
21
+ if (config.onTestFailed) {
22
+ config.onTestFailed(test, err)
23
+ }
24
+ })
25
+
26
+ event.dispatcher.on(event.test.passed, test => {
27
+ if (config.onTestPassed) {
28
+ config.onTestPassed(test)
29
+ }
30
+ })
31
+
32
+ event.dispatcher.on(event.test.skipped, test => {
33
+ if (config.onTestSkipped) {
34
+ config.onTestSkipped(test)
35
+ }
36
+ })
37
+
38
+ event.dispatcher.on(event.test.finished, test => {
39
+ if (config.onTestFinished) {
40
+ config.onTestFinished(test)
41
+ }
42
+ })
43
+
44
+ event.dispatcher.on(event.all.result, result => {
45
+ if (config.onResult) {
46
+ config.onResult(result)
47
+ }
48
+
49
+ if (config.save) {
50
+ result.save()
51
+ }
52
+ })
53
+ }
@@ -1,10 +1,16 @@
1
- const debug = require('debug')('codeceptjs:heal')
2
- const colors = require('chalk')
3
- const recorder = require('../recorder')
4
- const event = require('../event')
5
- const output = require('../output')
6
- const heal = require('../heal')
7
- const store = require('../store')
1
+ import debugFactory from 'debug'
2
+ const debug = debugFactory('codeceptjs:heal')
3
+ import colors from 'chalk'
4
+ import recorder from '../recorder.js'
5
+
6
+ import event from '../event.js'
7
+
8
+ import output from '../output.js'
9
+
10
+ import healModule from '../heal.js'
11
+ const heal = healModule.default || healModule
12
+ import store from '../store.js'
13
+
8
14
 
9
15
  const defaultConfig = {
10
16
  healLimit: 2,
@@ -28,13 +34,10 @@ const defaultConfig = {
28
34
  * * `healLimit` - how many steps can be healed in a single test (default: 2)
29
35
  *
30
36
  */
31
- module.exports = function (config = {}) {
37
+ export default function (config = {}) {
32
38
  if (store.debugMode && !process.env.DEBUG) {
33
39
  event.dispatcher.on(event.test.failed, () => {
34
- output.plugin(
35
- 'heal',
36
- 'Healing is disabled in --debug mode, use DEBUG="codeceptjs:heal" to enable it in debug mode',
37
- )
40
+ output.plugin('heal', 'Healing is disabled in --debug mode, use DEBUG="codeceptjs:heal" to enable it in debug mode')
38
41
  })
39
42
  return
40
43
  }
@@ -48,21 +51,21 @@ module.exports = function (config = {}) {
48
51
 
49
52
  config = Object.assign(defaultConfig, config)
50
53
 
51
- event.dispatcher.on(event.test.before, (test) => {
54
+ event.dispatcher.on(event.test.before, test => {
52
55
  currentTest = test
53
56
  healedSteps = 0
54
57
  caughtError = null
55
58
  })
56
59
 
57
- event.dispatcher.on(event.step.started, (step) => (currentStep = step))
60
+ event.dispatcher.on(event.step.started, step => (currentStep = step))
58
61
 
59
- event.dispatcher.on(event.step.after, (step) => {
62
+ event.dispatcher.on(event.step.after, step => {
60
63
  if (isHealing) return
61
64
  if (healTries >= config.healLimit) return // out of limit
62
65
 
63
66
  if (!heal.hasCorrespondingRecipes(step)) return
64
67
 
65
- recorder.catchWithoutStop(async (err) => {
68
+ recorder.catchWithoutStop(async err => {
66
69
  isHealing = true
67
70
  if (caughtError === err) throw err // avoid double handling
68
71
  caughtError = err
@@ -99,7 +102,7 @@ module.exports = function (config = {}) {
99
102
 
100
103
  print(`${colors.bold(heal.fixes.length)} ${heal.fixes.length === 1 ? 'step was' : 'steps were'} healed`)
101
104
 
102
- const suggestions = heal.fixes.filter((fix) => fix.recipe && heal.recipes[fix.recipe].suggest)
105
+ const suggestions = heal.fixes.filter(fix => fix.recipe && heal.recipes[fix.recipe].suggest)
103
106
 
104
107
  if (!suggestions.length) return
105
108
 
@@ -118,4 +121,33 @@ module.exports = function (config = {}) {
118
121
  i++
119
122
  }
120
123
  })
124
+
125
+ event.dispatcher.on(event.workers.result, result => {
126
+ const { print } = output
127
+
128
+ const healedTests = Object.values(result.tests)
129
+ .flat()
130
+ .filter(test => test.notes.some(note => note.type === 'heal'))
131
+ if (!healedTests.length) return
132
+
133
+ setTimeout(() => {
134
+ print('')
135
+ print('===================')
136
+ print(colors.bold.green('Self-Healing Report:'))
137
+
138
+ print('')
139
+ print('Suggested changes:')
140
+ print('')
141
+
142
+ healedTests.forEach(test => {
143
+ print(`${colors.bold.magenta(test.title)}`)
144
+ test.notes
145
+ .filter(note => note.type === 'heal')
146
+ .forEach(note => {
147
+ print(note.text)
148
+ print('')
149
+ })
150
+ })
151
+ }, 0)
152
+ })
121
153
  }
@@ -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
- const event = require('../event')
2
- const pause = require('../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,7 +22,7 @@ const pause = require('../pause')
21
22
  * ```
22
23
  *
23
24
  */
24
- module.exports = () => {
25
+ export default function() {
25
26
  let failed = false
26
27
 
27
28
  event.dispatcher.on(event.test.started, () => {
@@ -1,7 +1,8 @@
1
- const event = require('../event')
2
- const recorder = require('../recorder')
3
- const container = require('../container')
4
- const { log } = require('../output')
1
+ import event from '../event.js'
2
+
3
+ import recorder from '../recorder.js'
4
+
5
+ import store from '../store.js'
5
6
 
6
7
  const defaultConfig = {
7
8
  retries: 3,
@@ -70,34 +71,38 @@ const defaultConfig = {
70
71
  * Use scenario configuration to disable plugin for a test
71
72
  *
72
73
  * ```js
73
- * Scenario('scenario tite', () => {
74
+ * Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
74
75
  * // test goes here
75
- * }).config(test => test.disableRetryFailedStep = true)
76
+ * })
76
77
  * ```
77
78
  *
78
79
  */
79
- module.exports = (config) => {
80
+ export default function (config) {
80
81
  config = Object.assign(defaultConfig, config)
81
82
  config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
82
83
  const customWhen = config.when
83
84
 
84
85
  let enableRetry = false
85
86
 
86
- const when = (err) => {
87
+ const when = err => {
87
88
  if (!enableRetry) return
88
- const store = require('../store')
89
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
90
95
  if (customWhen) return customWhen(err)
91
96
  return true
92
97
  }
93
98
  config.when = when
94
99
 
95
- event.dispatcher.on(event.step.started, (step) => {
96
- if (process.env.TRY_TO === 'true') {
97
- log('Info: RetryFailedStep plugin is disabled inside tryTo block')
98
- return
99
- }
100
+ // Ensure retry options are available before any steps run
101
+ if (!recorder.retries.find(r => r === config)) {
102
+ recorder.retries.push(config)
103
+ }
100
104
 
105
+ event.dispatcher.on(event.step.started, step => {
101
106
  // if a step is ignored - return
102
107
  for (const ignored of config.ignoredSteps) {
103
108
  if (step.name === ignored) return
@@ -108,14 +113,50 @@ module.exports = (config) => {
108
113
  enableRetry = true // enable retry for a step
109
114
  })
110
115
 
111
- event.dispatcher.on(event.step.finished, () => {
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, () => {
112
118
  enableRetry = false
113
119
  })
114
120
 
115
- event.dispatcher.on(event.test.before, (test) => {
116
- if (test && test.disableRetryFailedStep) return // disable retry when a test is not active
117
- // this env var is used to set the retries inside _before() block of helpers
118
- process.env.FAILED_STEP_RETRIES = config.retries
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.disableRetryFailedStep || test.disableRetryFailedStep) {
125
+ store.autoRetries = false
126
+ return // disable retry when a test is not active
127
+ }
128
+
129
+ // Don't apply plugin retry logic if there are already manual retries configured
130
+ // Check if any retry configs exist that aren't from this plugin
131
+ const hasManualRetries = recorder.retries.some(retry => retry !== config)
132
+ if (hasManualRetries) {
133
+ store.autoRetries = false
134
+ return
135
+ }
136
+
137
+ // this option is used to set the retries inside _before() block of helpers
138
+ store.autoRetries = true
139
+ test.opts.conditionalRetries = config.retries
140
+ // debug: record applied retries value for tests
141
+ if (process.env.DEBUG_RETRY_PLUGIN) {
142
+ // eslint-disable-next-line no-console
143
+ console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
144
+ }
119
145
  recorder.retry(config)
120
146
  })
147
+
148
+ // Fallback for environments where event.test.before wasn't emitted (runner scenarios)
149
+ event.dispatcher.on(event.test.started, test => {
150
+ if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
151
+
152
+ // Don't apply plugin retry logic if there are already manual retries configured
153
+ // Check if any retry configs exist that aren't from this plugin
154
+ const hasManualRetries = recorder.retries.some(retry => retry !== config)
155
+ if (hasManualRetries) return
156
+
157
+ if (!store.autoRetries) {
158
+ store.autoRetries = true
159
+ test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
160
+ }
161
+ })
121
162
  }