codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10

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 (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. package/typings/types.d.ts +0 -13073
@@ -0,0 +1,52 @@
1
+ import Locator from '../../locator.js'
2
+
3
+ class MultipleElementsFound extends Error {
4
+ constructor(locator, webElements) {
5
+ const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
6
+ ? new Locator(locator).toString()
7
+ : String(locator)
8
+ super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
9
+ this.name = 'MultipleElementsFound'
10
+ this.locator = locator
11
+ this.webElements = webElements
12
+ this.count = webElements.length
13
+ this._detailsFetched = false
14
+ }
15
+
16
+ async fetchDetails() {
17
+ if (this._detailsFetched) return
18
+
19
+ try {
20
+ const items = []
21
+ const maxToShow = Math.min(this.count, 10)
22
+
23
+ for (let i = 0; i < maxToShow; i++) {
24
+ const webEl = this.webElements[i]
25
+ try {
26
+ const xpath = await webEl.toAbsoluteXPath()
27
+ const html = await webEl.toSimplifiedHTML()
28
+ items.push(` ${i + 1}. > ${xpath}\n ${html}`)
29
+ } catch (err) {
30
+ items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
31
+ }
32
+ }
33
+
34
+ if (this.count > 10) {
35
+ items.push(` ... and ${this.count - 10} more`)
36
+ }
37
+
38
+ const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
39
+ ? new Locator(this.locator).toString()
40
+ : String(this.locator)
41
+ this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
42
+ items.join('\n') +
43
+ `\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
44
+ } catch (err) {
45
+ this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
46
+ }
47
+
48
+ this._detailsFetched = true
49
+ }
50
+ }
51
+
52
+ export default MultipleElementsFound
@@ -0,0 +1,58 @@
1
+ import store from '../../store.js'
2
+ import output from '../../output.js'
3
+ import WebElement from '../../element/WebElement.js'
4
+ import MultipleElementsFound from '../errors/MultipleElementsFound.js'
5
+
6
+ function resolveElementIndex(value) {
7
+ if (value === 'first') return 1
8
+ if (value === 'last') return -1
9
+ return value
10
+ }
11
+
12
+ function isStrictStep(opts, helper) {
13
+ if (opts?.exact === true || opts?.strictMode === true) return true
14
+ if (opts?.exact === false || opts?.strictMode === false) return false
15
+ return helper.options.strict
16
+ }
17
+
18
+ function selectElement(els, locator, helper) {
19
+ const opts = store.currentStep?.opts
20
+ const rawIndex = opts?.elementIndex
21
+ const elementIndex = resolveElementIndex(rawIndex)
22
+
23
+ if (elementIndex != null) {
24
+ if (els.length === 1) return els[0]
25
+
26
+ if (!Number.isInteger(elementIndex) || elementIndex === 0) {
27
+ throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
28
+ }
29
+
30
+ let idx
31
+ if (elementIndex > 0) {
32
+ idx = elementIndex - 1
33
+ if (idx >= els.length) {
34
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
35
+ }
36
+ } else {
37
+ idx = els.length + elementIndex
38
+ if (idx < 0) {
39
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
40
+ }
41
+ }
42
+
43
+ output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
44
+ return els[idx]
45
+ }
46
+
47
+ if (isStrictStep(opts, helper)) {
48
+ if (els.length > 1) {
49
+ const webElements = Array.from(els).map(el => new WebElement(el, helper))
50
+ throw new MultipleElementsFound(locator, webElements)
51
+ }
52
+ }
53
+
54
+ if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
55
+ return els[0]
56
+ }
57
+
58
+ export { selectElement }
@@ -0,0 +1,11 @@
1
+ export const dropFile = (el, { base64Content, fileName, mimeType }) => {
2
+ const binaryStr = atob(base64Content)
3
+ const bytes = new Uint8Array(binaryStr.length)
4
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
5
+ const fileObj = new File([bytes], fileName, { type: mimeType })
6
+ const dataTransfer = new DataTransfer()
7
+ dataTransfer.items.add(fileObj)
8
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
9
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
10
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
11
+ }
package/lib/html.js CHANGED
@@ -245,4 +245,17 @@ function splitByChunks(text, chunkSize) {
245
245
  return chunks.map(chunk => chunk.trim())
246
246
  }
247
247
 
248
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml }
248
+ function simplifyHtmlElement(html, maxLength = 300) {
249
+ try {
250
+ html = removeNonInteractiveElements(html)
251
+ html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
252
+ } catch (e) {
253
+ // keep raw html if minification fails
254
+ }
255
+ if (html.length > maxLength) {
256
+ html = html.slice(0, maxLength) + '...'
257
+ }
258
+ return html
259
+ }
260
+
261
+ export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement }
@@ -12,15 +12,23 @@ export default function () {
12
12
  return
13
13
  }
14
14
  global.__codeceptConfigListenerInitialized = true
15
-
16
- const helpers = global.container.helpers()
17
15
 
18
16
  enableDynamicConfigFor('suite')
19
17
  enableDynamicConfigFor('test')
20
18
 
21
19
  function enableDynamicConfigFor(type) {
22
20
  event.dispatcher.on(event[type].before, (context = {}) => {
21
+ // Get helpers dynamically at runtime, not at initialization time
22
+ // This ensures we get the actual helper instances, not placeholders
23
+ const helpers = global.container.helpers()
24
+
23
25
  function updateHelperConfig(helper, config) {
26
+ // Guard against undefined or invalid helpers
27
+ if (!helper || !helper.constructor) {
28
+ output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`)
29
+ return
30
+ }
31
+
24
32
  const oldConfig = deepClone(helper.options)
25
33
  try {
26
34
  helper._setConfig(deepMerge(deepClone(oldConfig), config))
@@ -41,7 +49,7 @@ export default function () {
41
49
  for (let name in context.config) {
42
50
  const config = context.config[name]
43
51
  if (name === '0') {
44
- // first helper
52
+ // first helper - get dynamically
45
53
  name = Object.keys(helpers)[0]
46
54
  }
47
55
  const helper = helpers[name]
@@ -5,16 +5,27 @@ import { isNotSet } from '../utils.js'
5
5
 
6
6
  const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
7
 
8
+ const RETRY_PRIORITIES = {
9
+ MANUAL_STEP: 100,
10
+ STEP_PLUGIN: 50,
11
+ SCENARIO_CONFIG: 30,
12
+ FEATURE_CONFIG: 20,
13
+ HOOK_CONFIG: 10,
14
+ }
15
+
8
16
  export default function () {
9
17
  event.dispatcher.on(event.suite.before, suite => {
10
18
  let retryConfig = Config.get('retry')
11
19
  if (!retryConfig) return
12
20
 
13
21
  if (Number.isInteger(+retryConfig)) {
14
- // is number
15
22
  const retryNum = +retryConfig
16
23
  output.log(`Retries: ${retryNum}`)
17
- suite.retries(retryNum)
24
+
25
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
26
+ suite.retries(retryNum)
27
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
28
+ }
18
29
  return
19
30
  }
20
31
 
@@ -30,11 +41,18 @@ export default function () {
30
41
  hooks
31
42
  .filter(hook => !!config[hook])
32
43
  .forEach(hook => {
33
- if (isNotSet(suite.opts[`retry${hook}`])) suite.opts[`retry${hook}`] = config[hook]
44
+ const retryKey = `retry${hook}`
45
+ if (isNotSet(suite.opts[retryKey])) {
46
+ suite.opts[retryKey] = config[hook]
47
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
48
+ }
34
49
  })
35
50
 
36
51
  if (config.Feature) {
37
- if (isNotSet(suite.retries())) suite.retries(config.Feature)
52
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
53
+ suite.retries(config.Feature)
54
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
55
+ }
38
56
  }
39
57
 
40
58
  output.log(`Retries: ${JSON.stringify(config)}`)
@@ -46,7 +64,10 @@ export default function () {
46
64
  if (!retryConfig) return
47
65
 
48
66
  if (Number.isInteger(+retryConfig)) {
49
- if (test.retries() === -1) test.retries(retryConfig)
67
+ if (test.retries() === -1) {
68
+ test.retries(retryConfig)
69
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
70
+ }
50
71
  return
51
72
  }
52
73
 
@@ -62,9 +83,14 @@ export default function () {
62
83
  }
63
84
 
64
85
  if (config.Scenario) {
65
- if (test.retries() === -1) test.retries(config.Scenario)
86
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
87
+ test.retries(config.Scenario)
88
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
89
+ }
66
90
  output.log(`Retries: ${config.Scenario}`)
67
91
  }
68
92
  }
69
93
  })
70
94
  }
95
+
96
+ export { RETRY_PRIORITIES }
@@ -73,30 +73,18 @@ export default function () {
73
73
  })
74
74
 
75
75
  event.dispatcher.on(event.all.result, () => {
76
- // Skip _finishTest for all helpers if any browser helper restarts to avoid double cleanup
77
- const hasBrowserRestart = Object.values(helpers).some(helper =>
78
- (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
79
- (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
80
- )
81
-
82
76
  Object.keys(helpers).forEach(key => {
83
77
  const helper = helpers[key]
84
- if (helper._finishTest && !hasBrowserRestart) {
78
+ if (helper._finishTest) {
85
79
  recorder.add(`hook ${key}._finishTest()`, () => helper._finishTest(), true, false)
86
80
  }
87
81
  })
88
82
  })
89
83
 
90
84
  event.dispatcher.on(event.all.after, () => {
91
- // Skip _cleanup for all helpers if any browser helper restarts to avoid double cleanup
92
- const hasBrowserRestart = Object.values(helpers).some(helper =>
93
- (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
94
- (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
95
- )
96
-
97
85
  Object.keys(helpers).forEach(key => {
98
86
  const helper = helpers[key]
99
- if (helper._cleanup && !hasBrowserRestart) {
87
+ if (helper._cleanup) {
100
88
  recorder.add(`hook ${key}._cleanup()`, () => helper._cleanup(), true, false)
101
89
  }
102
90
  })
package/lib/locator.js CHANGED
@@ -40,6 +40,11 @@ class Locator {
40
40
  return
41
41
  }
42
42
 
43
+ // Try to parse JSON strings that look like objects
44
+ if (this.parsedJsonAsString(locator)) {
45
+ return
46
+ }
47
+
43
48
  this.type = defaultType || 'fuzzy'
44
49
  this.output = locator
45
50
  this.value = locator
@@ -89,6 +94,33 @@ class Locator {
89
94
  return { [this.type]: this.value }
90
95
  }
91
96
 
97
+ parsedJsonAsString(locator) {
98
+ if (typeof locator !== 'string') {
99
+ return false
100
+ }
101
+
102
+ const trimmed = locator.trim()
103
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
104
+ return false
105
+ }
106
+
107
+ try {
108
+ const parsed = JSON.parse(trimmed)
109
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
110
+ this.locator = parsed
111
+ this.type = Object.keys(parsed)[0]
112
+ this.value = parsed[this.type]
113
+ this.strict = true
114
+
115
+ Locator.filters.forEach(f => f(parsed, this))
116
+ return true
117
+ }
118
+ } catch (e) {
119
+ // continue with normal string processing
120
+ }
121
+ return false
122
+ }
123
+
92
124
  /**
93
125
  * @returns {string}
94
126
  */
package/lib/mocha/cli.js CHANGED
@@ -9,6 +9,7 @@ import event from '../event.js'
9
9
  import AssertionFailedError from '../assert/error.js'
10
10
  import output from '../output.js'
11
11
  import test, { cloneTest } from './test.js'
12
+ import { fixErrorStack } from '../utils/typescript.js'
12
13
 
13
14
  // Get version from package.json to avoid circular dependency
14
15
  const __filename = fileURLToPath(import.meta.url)
@@ -201,6 +202,16 @@ class Cli extends Base {
201
202
 
202
203
  // failures
203
204
  if (stats.failures) {
205
+ for (const test of this.failures) {
206
+ if (test.err && typeof test.err.fetchDetails === 'function') {
207
+ try {
208
+ await test.err.fetchDetails()
209
+ } catch (e) {
210
+ // ignore fetch errors
211
+ }
212
+ }
213
+ }
214
+
204
215
  // append step traces
205
216
  this.failures = this.failures.map(test => {
206
217
  // we will change the stack trace, so we need to clone the test
@@ -264,6 +275,11 @@ class Cli extends Base {
264
275
  }
265
276
 
266
277
  try {
278
+ const fileMapping = global.container?.tsFileMapping?.()
279
+ if (fileMapping) {
280
+ fixErrorStack(err, fileMapping)
281
+ }
282
+
267
283
  let stack = err.stack
268
284
  stack = (stack || '').replace(originalMessage, '')
269
285
  stack = stack ? stack.split('\n') : []
@@ -7,6 +7,7 @@ import gherkinParser, { loadTranslations } from './gherkin.js'
7
7
  import output from '../output.js'
8
8
  import scenarioUiFunction from './ui.js'
9
9
  import { initMochaGlobals } from '../globals.js'
10
+ import { fixErrorStack } from '../utils/typescript.js'
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url)
12
13
  const __dirname = fsPath.dirname(__filename)
@@ -34,6 +35,10 @@ class MochaFactory {
34
35
  // Handle ECONNREFUSED without dynamic import for now
35
36
  err = new Error('Connection refused: ' + err.toString())
36
37
  }
38
+ const fileMapping = global.container?.tsFileMapping?.()
39
+ if (fileMapping) {
40
+ fixErrorStack(err, fileMapping)
41
+ }
37
42
  output.error(err)
38
43
  output.print(err.stack)
39
44
  process.exit(1)
@@ -62,34 +67,9 @@ class MochaFactory {
62
67
  const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
63
68
  this.files = this.files.filter(file => !file.match(/\.feature$/))
64
69
 
65
- // Load JavaScript test files using ESM imports
70
+ // Load JavaScript test files using original loadFiles
66
71
  if (jsFiles.length > 0) {
67
- try {
68
- // Try original loadFiles first for compatibility
69
- originalLoadFiles.call(this, fn)
70
- } catch (e) {
71
- // If original loadFiles fails, load ESM files manually
72
- if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
73
- // Load ESM files by importing them synchronously using top-level await workaround
74
- for (const file of jsFiles) {
75
- try {
76
- // Convert file path to file:// URL for dynamic import
77
- const fileUrl = `file://${file}`
78
- // Use import() but don't await it - let it load in the background
79
- import(fileUrl).catch(importErr => {
80
- // If dynamic import fails, the file may have syntax errors or other issues
81
- console.error(`Failed to load test file ${file}:`, importErr.message)
82
- })
83
- if (fn) fn()
84
- } catch (fileErr) {
85
- console.error(`Error processing test file ${file}:`, fileErr.message)
86
- if (fn) fn(fileErr)
87
- }
88
- }
89
- } else {
90
- throw e
91
- }
92
- }
72
+ originalLoadFiles.call(this, fn)
93
73
  }
94
74
 
95
75
  // add ids for each test and check uniqueness
@@ -1,4 +1,4 @@
1
- import Gherkin from '@cucumber/gherkin'
1
+ import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
2
2
  import { IdGenerator } from '@cucumber/messages'
3
3
  import { Context, Suite } from 'mocha'
4
4
  import debug from 'debug'
@@ -15,9 +15,9 @@ import { createTest } from './test.js'
15
15
  import { matchStep } from './bdd.js'
16
16
 
17
17
  const uuidFn = IdGenerator.uuid()
18
- const builder = new Gherkin.AstBuilder(uuidFn)
19
- const matcher = new Gherkin.GherkinClassicTokenMatcher()
20
- const parser = new Gherkin.Parser(builder, matcher)
18
+ const builder = new AstBuilder(uuidFn)
19
+ const matcher = new GherkinClassicTokenMatcher()
20
+ const parser = new Parser(builder, matcher)
21
21
  parser.stopAtFirstError = false
22
22
 
23
23
  const gherkinParser = (text, file) => {
package/lib/mocha/test.js CHANGED
@@ -154,14 +154,16 @@ function cloneTest(test) {
154
154
  function testToFileName(test, { suffix = '', unique = false } = {}) {
155
155
  let fileName = test.title
156
156
 
157
- if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
158
- if (suffix) fileName = `${fileName}_${suffix}`
159
157
  // remove tags with empty string (disable for now)
160
158
  // fileName = fileName.replace(/\@\w+/g, '')
161
159
  fileName = fileName.slice(0, 100)
162
160
  if (fileName.indexOf('{') !== -1) {
163
161
  fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
164
162
  }
163
+
164
+ // Apply unique suffix AFTER removing data part to ensure uniqueness
165
+ if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime())}`
166
+ if (suffix) fileName = `${fileName}_${suffix}`
165
167
  if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
166
168
  // TODO: add suite title to file name
167
169
  // if (test.parent && test.parent.title) {
package/lib/output.js CHANGED
@@ -222,12 +222,10 @@ const output = {
222
222
  /**
223
223
  * @param {Mocha.Test} test
224
224
  */
225
-
226
225
  started(test) {
227
226
  if (outputLevel < 1) return
228
227
  print(` ${colors.dim.bold('Scenario()')}`)
229
228
  },
230
-
231
229
  /**
232
230
  * @param {Mocha.Test} test
233
231
  */
@@ -273,10 +271,12 @@ const output = {
273
271
  },
274
272
 
275
273
  /**
274
+ * Prints the stats of a test run to the console.
276
275
  * @param {number} passed
277
276
  * @param {number} failed
278
277
  * @param {number} skipped
279
278
  * @param {number|string} duration
279
+ * @param {number} [failedHooks]
280
280
  */
281
281
  result(passed, failed, skipped, duration, failedHooks = 0) {
282
282
  let style = colors.bgGreen