codeceptjs 3.7.0-beta.1 → 3.7.0-beta.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 (73) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +7 -0
  3. package/lib/actor.js +46 -92
  4. package/lib/ai.js +130 -121
  5. package/lib/codecept.js +2 -2
  6. package/lib/command/check.js +186 -0
  7. package/lib/command/definitions.js +3 -1
  8. package/lib/command/interactive.js +1 -1
  9. package/lib/command/run-workers.js +2 -54
  10. package/lib/command/workers/runTests.js +64 -225
  11. package/lib/container.js +27 -0
  12. package/lib/effects.js +218 -0
  13. package/lib/els.js +87 -106
  14. package/lib/event.js +18 -17
  15. package/lib/heal.js +10 -0
  16. package/lib/helper/AI.js +2 -1
  17. package/lib/helper/Appium.js +31 -22
  18. package/lib/helper/Playwright.js +22 -1
  19. package/lib/helper/Puppeteer.js +5 -0
  20. package/lib/helper/WebDriver.js +29 -8
  21. package/lib/listener/emptyRun.js +2 -5
  22. package/lib/listener/exit.js +5 -8
  23. package/lib/listener/globalTimeout.js +66 -10
  24. package/lib/listener/result.js +12 -0
  25. package/lib/listener/steps.js +3 -6
  26. package/lib/listener/store.js +9 -1
  27. package/lib/mocha/asyncWrapper.js +15 -3
  28. package/lib/mocha/cli.js +79 -28
  29. package/lib/mocha/featureConfig.js +13 -0
  30. package/lib/mocha/hooks.js +32 -3
  31. package/lib/mocha/inject.js +5 -0
  32. package/lib/mocha/scenarioConfig.js +11 -0
  33. package/lib/mocha/suite.js +27 -1
  34. package/lib/mocha/test.js +102 -3
  35. package/lib/mocha/types.d.ts +11 -0
  36. package/lib/output.js +75 -73
  37. package/lib/pause.js +3 -10
  38. package/lib/plugin/analyze.js +349 -0
  39. package/lib/plugin/autoDelay.js +2 -2
  40. package/lib/plugin/commentStep.js +5 -0
  41. package/lib/plugin/customReporter.js +52 -0
  42. package/lib/plugin/heal.js +30 -0
  43. package/lib/plugin/pageInfo.js +140 -0
  44. package/lib/plugin/retryTo.js +18 -118
  45. package/lib/plugin/screenshotOnFail.js +12 -17
  46. package/lib/plugin/standardActingHelpers.js +4 -1
  47. package/lib/plugin/stepByStepReport.js +6 -5
  48. package/lib/plugin/stepTimeout.js +1 -1
  49. package/lib/plugin/tryTo.js +17 -107
  50. package/lib/recorder.js +5 -5
  51. package/lib/rerun.js +43 -42
  52. package/lib/result.js +161 -0
  53. package/lib/step/base.js +228 -0
  54. package/lib/step/config.js +50 -0
  55. package/lib/step/func.js +46 -0
  56. package/lib/step/helper.js +50 -0
  57. package/lib/step/meta.js +99 -0
  58. package/lib/step/record.js +74 -0
  59. package/lib/step/retry.js +11 -0
  60. package/lib/step/section.js +55 -0
  61. package/lib/step.js +20 -347
  62. package/lib/steps.js +50 -0
  63. package/lib/store.js +4 -0
  64. package/lib/timeout.js +66 -0
  65. package/lib/utils.js +93 -0
  66. package/lib/within.js +2 -2
  67. package/lib/workers.js +29 -49
  68. package/package.json +23 -20
  69. package/typings/index.d.ts +5 -4
  70. package/typings/promiseBasedTypes.d.ts +617 -7
  71. package/typings/types.d.ts +663 -34
  72. package/lib/listener/artifacts.js +0 -19
  73. package/lib/plugin/debugErrors.js +0 -67
package/lib/helper/AI.js CHANGED
@@ -3,13 +3,14 @@ const ora = require('ora-classic')
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const ai = require('../ai')
6
- const standardActingHelpers = require('../plugin/standardActingHelpers')
7
6
  const Container = require('../container')
8
7
  const { splitByChunks, minifyHtml } = require('../html')
9
8
  const { beautify } = require('../utils')
10
9
  const output = require('../output')
11
10
  const { registerVariable } = require('../pause')
12
11
 
12
+ const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
13
+
13
14
  const gtpRole = {
14
15
  user: 'user',
15
16
  }
@@ -44,7 +44,7 @@ const vendorPrefix = {
44
44
  *
45
45
  * This helper should be configured in codecept.conf.ts or codecept.conf.js
46
46
  *
47
- * * `appiumV2`: set this to true if you want to run tests with AppiumV2. See more how to setup [here](https://codecept.io/mobile/#setting-up)
47
+ * * `appiumV2`: by default is true, set this to false if you want to run tests with AppiumV1. See more how to setup [here](https://codecept.io/mobile/#setting-up)
48
48
  * * `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage
49
49
  * * `host`: (default: 'localhost') Appium host
50
50
  * * `port`: (default: '4723') Appium port
@@ -124,7 +124,7 @@ const vendorPrefix = {
124
124
  * {
125
125
  * helpers: {
126
126
  * Appium: {
127
- * appiumV2: true,
127
+ * appiumV2: true, // By default is true, set to false if you want to run against Appium v1
128
128
  * host: "hub-cloud.browserstack.com",
129
129
  * port: 4444,
130
130
  * user: process.env.BROWSERSTACK_USER,
@@ -178,14 +178,12 @@ class Appium extends Webdriver {
178
178
  super(config)
179
179
 
180
180
  this.isRunning = false
181
- if (config.appiumV2 === true) {
182
- this.appiumV2 = true
183
- }
181
+ this.appiumV2 = config.appiumV2 || true
184
182
  this.axios = axios.create()
185
183
 
186
184
  webdriverio = require('webdriverio')
187
185
  if (!config.appiumV2) {
188
- console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.')
186
+ console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Appium 2.x is used by default.')
189
187
  console.log('More info: https://bit.ly/appium-v2-migration')
190
188
  console.log('This Appium 1.x support will be removed in next major release.')
191
189
  }
@@ -386,7 +384,7 @@ class Appium extends Webdriver {
386
384
  _buildAppiumEndpoint() {
387
385
  const { protocol, port, hostname, path } = this.browser.options
388
386
  // Build path to Appium REST API endpoint
389
- return `${protocol}://${hostname}:${port}${path}`
387
+ return `${protocol}://${hostname}:${port}${path}/session/${this.browser.sessionId}`
390
388
  }
391
389
 
392
390
  /**
@@ -602,7 +600,7 @@ class Appium extends Webdriver {
602
600
 
603
601
  return this.axios({
604
602
  method: 'post',
605
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`,
603
+ url: `${this._buildAppiumEndpoint()}/appium/device/remove_app`,
606
604
  data: { appId, bundleId },
607
605
  })
608
606
  }
@@ -619,7 +617,7 @@ class Appium extends Webdriver {
619
617
  onlyForApps.call(this)
620
618
  return this.axios({
621
619
  method: 'post',
622
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/app/reset`,
620
+ url: `${this._buildAppiumEndpoint()}/appium/app/reset`,
623
621
  })
624
622
  }
625
623
 
@@ -693,7 +691,7 @@ class Appium extends Webdriver {
693
691
 
694
692
  const res = await this.axios({
695
693
  method: 'get',
696
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
694
+ url: `${this._buildAppiumEndpoint()}/orientation`,
697
695
  })
698
696
 
699
697
  const currentOrientation = res.data.value
@@ -717,7 +715,7 @@ class Appium extends Webdriver {
717
715
 
718
716
  return this.axios({
719
717
  method: 'post',
720
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
718
+ url: `${this._buildAppiumEndpoint()}/orientation`,
721
719
  data: { orientation },
722
720
  })
723
721
  }
@@ -956,21 +954,19 @@ class Appium extends Webdriver {
956
954
  * ```js
957
955
  * // taps outside to hide keyboard per default
958
956
  * I.hideDeviceKeyboard();
959
- * I.hideDeviceKeyboard('tapOutside');
960
- *
961
- * // or by pressing key
962
- * I.hideDeviceKeyboard('pressKey', 'Done');
963
957
  * ```
964
958
  *
965
959
  * Appium: support Android and iOS
966
960
  *
967
- * @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
968
- * @param {string} [key] Optional key
969
961
  */
970
- async hideDeviceKeyboard(strategy, key) {
962
+ async hideDeviceKeyboard() {
971
963
  onlyForApps.call(this)
972
- strategy = strategy || 'tapOutside'
973
- return this.browser.hideKeyboard(strategy, key)
964
+
965
+ return this.axios({
966
+ method: 'post',
967
+ url: `${this._buildAppiumEndpoint()}/appium/device/hide_keyboard`,
968
+ data: {},
969
+ })
974
970
  }
975
971
 
976
972
  /**
@@ -1046,7 +1042,13 @@ class Appium extends Webdriver {
1046
1042
  * @param {*} locator
1047
1043
  */
1048
1044
  async tap(locator) {
1049
- return this.makeTouchAction(locator, 'tap')
1045
+ const { elementId } = await this.browser.$(parseLocator.call(this, locator))
1046
+
1047
+ return this.axios({
1048
+ method: 'post',
1049
+ url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
1050
+ data: {},
1051
+ })
1050
1052
  }
1051
1053
 
1052
1054
  /**
@@ -1493,7 +1495,14 @@ class Appium extends Webdriver {
1493
1495
  */
1494
1496
  async click(locator, context) {
1495
1497
  if (this.isWeb) return super.click(locator, context)
1496
- return super.click(parseLocator.call(this, locator), parseLocator.call(this, context))
1498
+
1499
+ const { elementId } = await this.browser.$(parseLocator.call(this, locator), parseLocator.call(this, context))
1500
+
1501
+ return this.axios({
1502
+ method: 'post',
1503
+ url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
1504
+ data: {},
1505
+ })
1497
1506
  }
1498
1507
 
1499
1508
  /**
@@ -482,6 +482,7 @@ class Playwright extends Helper {
482
482
 
483
483
  async _before(test) {
484
484
  this.currentRunningTest = test
485
+
485
486
  recorder.retry({
486
487
  retries: process.env.FAILED_STEP_RETRIES || 3,
487
488
  when: err => {
@@ -522,12 +523,17 @@ class Playwright extends Helper {
522
523
  this.currentRunningTest.artifacts.har = fileName
523
524
  contextOptions.recordHar = this.options.recordHar
524
525
  }
526
+
527
+ // load pre-saved cookies
528
+ if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
529
+
525
530
  if (this.storageState) contextOptions.storageState = this.storageState
526
531
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
527
532
  if (this.options.locale) contextOptions.locale = this.options.locale
528
533
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
529
534
  this.contextOptions = contextOptions
530
535
  if (!this.browserContext || !restartsSession()) {
536
+ this.debugSection('New Session', JSON.stringify(this.contextOptions))
531
537
  this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
532
538
  }
533
539
  }
@@ -552,6 +558,15 @@ class Playwright extends Helper {
552
558
 
553
559
  await this._setPage(mainPage)
554
560
 
561
+ try {
562
+ // set metadata for reporting
563
+ test.meta.browser = this.browser.browserType().name()
564
+ test.meta.browserVersion = this.browser.version()
565
+ test.meta.windowSize = `${this.page.viewportSize().width}x${this.page.viewportSize().height}`
566
+ } catch (e) {
567
+ this.debug('Failed to set metadata for reporting')
568
+ }
569
+
555
570
  if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true })
556
571
 
557
572
  return this.browser
@@ -928,7 +943,8 @@ class Playwright extends Helper {
928
943
  throw new Error('Cannot open pages inside an Electron container')
929
944
  }
930
945
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
931
- url = this.options.url + (url.startsWith('/') ? url : `/${url}`)
946
+ url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
947
+ this.debug(`Changed URL to base url + relative path: ${url}`)
932
948
  }
933
949
 
934
950
  if (this.options.basicAuth && this.isAuthenticated !== true) {
@@ -3499,6 +3515,11 @@ async function proceedSee(assertType, text, context, strict = false) {
3499
3515
  allText = await Promise.all(els.map(el => el.innerText()))
3500
3516
  }
3501
3517
 
3518
+ if (store?.currentStep?.opts?.ignoreCase === true) {
3519
+ text = text.toLowerCase()
3520
+ allText = allText.map(elText => elText.toLowerCase())
3521
+ }
3522
+
3502
3523
  if (strict) {
3503
3524
  return allText.map(elText => equals(description)[assertType](text, elText))
3504
3525
  }
@@ -2769,6 +2769,11 @@ async function proceedSee(assertType, text, context, strict = false) {
2769
2769
  allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
2770
2770
  }
2771
2771
 
2772
+ if (store?.currentStep?.opts?.ignoreCase === true) {
2773
+ text = text.toLowerCase()
2774
+ allText = allText.map(elText => elText.toLowerCase())
2775
+ }
2776
+
2772
2777
  if (strict) {
2773
2778
  return allText.map(elText => equals(description)[assertType](text, elText))
2774
2779
  }
@@ -7,6 +7,7 @@ const Helper = require('@codeceptjs/helper')
7
7
  const promiseRetry = require('promise-retry')
8
8
  const stringIncludes = require('../assert/include').includes
9
9
  const { urlEquals, equals } = require('../assert/equal')
10
+ const store = require('../store')
10
11
  const { debug } = require('../output')
11
12
  const { empty } = require('../assert/empty')
12
13
  const { truth } = require('../assert/truth')
@@ -23,6 +24,7 @@ const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTr
23
24
 
24
25
  const SHADOW = 'shadow'
25
26
  const webRoot = 'body'
27
+ let browserLogs = []
26
28
 
27
29
  /**
28
30
  * ## Configuration
@@ -620,6 +622,10 @@ class WebDriver extends Helper {
620
622
  }
621
623
 
622
624
  this.browser.on('dialog', () => {})
625
+
626
+ await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
627
+ this.browser.on('log.entryAdded', logEvents)
628
+
623
629
  return this.browser
624
630
  }
625
631
 
@@ -657,6 +663,7 @@ class WebDriver extends Helper {
657
663
  if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
658
664
  })
659
665
  await this.closeOtherTabs()
666
+ browserLogs = []
660
667
  return this.browser
661
668
  }
662
669
 
@@ -1521,11 +1528,7 @@ class WebDriver extends Helper {
1521
1528
  * {{> grabBrowserLogs }}
1522
1529
  */
1523
1530
  async grabBrowserLogs() {
1524
- if (this.browser.isW3C) {
1525
- this.debug('Logs not available in W3C specification')
1526
- return
1527
- }
1528
- return this.browser.getLogs('browser')
1531
+ return browserLogs
1529
1532
  }
1530
1533
 
1531
1534
  /**
@@ -1787,18 +1790,25 @@ class WebDriver extends Helper {
1787
1790
 
1788
1791
  if (browser) {
1789
1792
  this.debug(`Screenshot of ${sessionName} session has been saved to ${outputFile}`)
1790
- return browser.saveScreenshot(outputFile)
1793
+ await browser.saveScreenshot(outputFile)
1791
1794
  }
1792
1795
  }
1793
1796
  }
1794
1797
 
1795
1798
  if (!fullPage) {
1796
1799
  this.debug(`Screenshot has been saved to ${outputFile}`)
1797
- return this.browser.saveScreenshot(outputFile)
1800
+ await this.browser.saveScreenshot(outputFile)
1798
1801
  }
1799
1802
 
1800
1803
  const originalWindowSize = await this.browser.getWindowSize()
1801
1804
 
1805
+ // this case running on device, so we could not set the windowSize
1806
+ if (this.browser.isMobile) {
1807
+ this.debug(`Screenshot has been saved to ${outputFile}, size: ${originalWindowSize.width}x${originalWindowSize.height}`)
1808
+ const buffer = await this.browser.saveScreenshot(outputFile)
1809
+ return buffer
1810
+ }
1811
+
1802
1812
  let { width, height } = await this.browser
1803
1813
  .execute(function () {
1804
1814
  return {
@@ -2698,7 +2708,14 @@ async function proceedSee(assertType, text, context, strict = false) {
2698
2708
  const smartWaitEnabled = assertType === 'assert'
2699
2709
  const res = await this._locate(withStrictLocator(context), smartWaitEnabled)
2700
2710
  assertElementExists(res, context)
2701
- const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)))
2711
+ let selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)))
2712
+
2713
+ // apply ignoreCase option
2714
+ if (store?.currentStep?.opts?.ignoreCase === true) {
2715
+ text = text.toLowerCase()
2716
+ selected = selected.map(elText => elText.toLowerCase())
2717
+ }
2718
+
2702
2719
  if (strict) {
2703
2720
  if (Array.isArray(selected) && selected.length !== 0) {
2704
2721
  return selected.map(elText => equals(description)[assertType](text, elText))
@@ -3126,4 +3143,8 @@ function prepareLocateFn(context) {
3126
3143
  }
3127
3144
  }
3128
3145
 
3146
+ function logEvents(event) {
3147
+ browserLogs.push(event.text) // add log message to the array
3148
+ }
3149
+
3129
3150
  module.exports = WebDriver
@@ -2,6 +2,7 @@ const figures = require('figures')
2
2
  const Container = require('../container')
3
3
  const event = require('../event')
4
4
  const output = require('../output')
5
+ const { searchWithFusejs } = require('../utils')
5
6
 
6
7
  module.exports = function () {
7
8
  let isEmptyRun = true
@@ -15,8 +16,6 @@ module.exports = function () {
15
16
  const mocha = Container.mocha()
16
17
 
17
18
  if (mocha.options.grep) {
18
- const Fuse = require('fuse.js')
19
-
20
19
  output.print()
21
20
  output.print('No tests found by pattern: ' + mocha.options.grep)
22
21
 
@@ -27,14 +26,12 @@ module.exports = function () {
27
26
  })
28
27
  })
29
28
 
30
- const fuse = new Fuse(allTests, {
29
+ const results = searchWithFusejs(allTests, mocha.options.grep.toString(), {
31
30
  includeScore: true,
32
31
  threshold: 0.6,
33
32
  caseSensitive: false,
34
33
  })
35
34
 
36
- const results = fuse.search(mocha.options.grep.toString())
37
-
38
35
  if (results.length > 0) {
39
36
  output.print()
40
37
  output.print('Maybe you wanted to run one of these tests?')
@@ -1,20 +1,17 @@
1
1
  const event = require('../event')
2
+ const debug = require('debug')('codeceptjs:exit')
2
3
 
3
4
  module.exports = function () {
4
5
  let failedTests = []
5
6
 
6
- event.dispatcher.on(event.test.failed, testOrSuite => {
7
- // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
8
- // is a suite and not a test
9
- const id = testOrSuite.uid || (testOrSuite.ctx && testOrSuite.ctx.test.uid) || 'empty'
7
+ event.dispatcher.on(event.test.failed, test => {
8
+ const id = test.uid || (test.ctx && test.ctx.test.uid) || 'empty'
10
9
  failedTests.push(id)
11
10
  })
12
11
 
13
12
  // if test was successful after retries
14
- event.dispatcher.on(event.test.passed, testOrSuite => {
15
- // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object
16
- // is a suite and not a test
17
- const id = testOrSuite.uid || (testOrSuite.ctx && testOrSuite.ctx.test.uid) || 'empty'
13
+ event.dispatcher.on(event.test.passed, test => {
14
+ const id = test.uid || (test.ctx && test.ctx.test.uid) || 'empty'
18
15
  failedTests = failedTests.filter(failed => id !== failed)
19
16
  })
20
17
 
@@ -2,8 +2,10 @@ const event = require('../event')
2
2
  const output = require('../output')
3
3
  const recorder = require('../recorder')
4
4
  const Config = require('../config')
5
- const { timeouts } = require('../store')
6
- const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER
5
+ const store = require('../store')
6
+ const debug = require('debug')('codeceptjs:timeout')
7
+ const { TIMEOUT_ORDER, TimeoutError, TestTimeoutError, StepTimeoutError } = require('../timeout')
8
+ const { BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks')
7
9
 
8
10
  module.exports = function () {
9
11
  let timeout
@@ -11,16 +13,30 @@ module.exports = function () {
11
13
  let currentTest
12
14
  let currentTimeout
13
15
 
14
- if (!timeouts) {
16
+ if (!store.timeouts) {
15
17
  console.log('Timeouts were disabled')
16
18
  return
17
19
  }
18
20
 
21
+ // disable timeout for BeforeSuite/AfterSuite hooks
22
+ // add separate configs to them?
23
+ event.dispatcher.on(event.hook.started, hook => {
24
+ if (hook instanceof BeforeSuiteHook) {
25
+ timeout = null
26
+ suiteTimeout = []
27
+ }
28
+ if (hook instanceof AfterSuiteHook) {
29
+ timeout = null
30
+ suiteTimeout = []
31
+ }
32
+ })
33
+
19
34
  event.dispatcher.on(event.suite.before, suite => {
20
35
  suiteTimeout = []
21
36
  let timeoutConfig = Config.get('timeout')
22
37
 
23
38
  if (timeoutConfig) {
39
+ debug('config:', timeoutConfig)
24
40
  if (!Number.isNaN(+timeoutConfig)) {
25
41
  checkForSeconds(timeoutConfig)
26
42
  suiteTimeout.push(timeoutConfig)
@@ -40,6 +56,8 @@ module.exports = function () {
40
56
 
41
57
  if (suite.totalTimeout) suiteTimeout.push(suite.totalTimeout)
42
58
  output.log(`Timeouts: ${suiteTimeout}`)
59
+
60
+ if (suiteTimeout.length > 0) debug(suite.title, 'timeout', suiteTimeout)
43
61
  })
44
62
 
45
63
  event.dispatcher.on(event.test.before, test => {
@@ -64,6 +82,13 @@ module.exports = function () {
64
82
 
65
83
  timeout = test.totalTimeout || testTimeout || suiteTimeout[suiteTimeout.length - 1]
66
84
  if (!timeout) return
85
+
86
+ debug(test.title, 'timeout', {
87
+ 'config from file': testTimeout,
88
+ 'suite timeout': suiteTimeout,
89
+ 'dynamic config': test.totalTimeout,
90
+ })
91
+
67
92
  currentTimeout = timeout
68
93
  output.debug(`Test Timeout: ${timeout}s`)
69
94
  timeout *= 1000
@@ -80,24 +105,55 @@ module.exports = function () {
80
105
  event.dispatcher.on(event.step.before, step => {
81
106
  if (typeof timeout !== 'number') return
82
107
 
108
+ if (!store.timeouts) {
109
+ debug('step', step.toCode().trim(), 'timeout disabled')
110
+ return
111
+ }
112
+
83
113
  if (timeout < 0) {
114
+ debug('Previous steps timed out, setting timeout to 0.01s')
84
115
  step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite)
85
116
  } else {
117
+ debug(`Setting timeout ${timeout}ms for step ${step.toCode().trim()}`)
86
118
  step.setTimeout(timeout, TIMEOUT_ORDER.testOrSuite)
87
119
  }
88
120
  })
89
121
 
122
+ event.dispatcher.on(event.step.after, step => {
123
+ if (typeof timeout !== 'number') return
124
+ if (!store.timeouts) return
125
+
126
+ recorder.catchWithoutStop(err => {
127
+ // we wrap timeout errors in a StepTimeoutError
128
+ // but only if global timeout is set
129
+ // should we wrap all timeout errors?
130
+ if (err instanceof TimeoutError) {
131
+ const testTimeoutExceeded = timeout && +Date.now() - step.startTime >= timeout
132
+ debug('Step failed due to global test or suite timeout')
133
+ if (testTimeoutExceeded) {
134
+ debug('Test failed due to global test or suite timeout')
135
+ throw new TestTimeoutError(currentTimeout)
136
+ }
137
+ throw new StepTimeoutError(currentTimeout, step)
138
+ }
139
+ throw err
140
+ })
141
+ })
142
+
90
143
  event.dispatcher.on(event.step.finished, step => {
144
+ if (!store.timeouts) {
145
+ debug('step', step.toCode().trim(), 'timeout disabled')
146
+ return
147
+ }
148
+
149
+ if (typeof timeout === 'number') debug('Timeout', timeout)
150
+
151
+ debug(`step ${step.toCode().trim()}:${step.status} duration`, step.duration)
91
152
  if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration
92
153
 
93
154
  if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
94
- if (currentTest && currentTest.callback) {
95
- recorder.reset()
96
- // replace mocha timeout with custom timeout
97
- currentTest.timeout(0)
98
- currentTest.callback(new Error(`Timeout ${currentTimeout}s exceeded (with Before hook)`))
99
- currentTest.timedOut = true
100
- }
155
+ debug(`step ${step.toCode().trim()} timed out`)
156
+ recorder.throw(new TestTimeoutError(currentTimeout))
101
157
  }
102
158
  })
103
159
  }
@@ -0,0 +1,12 @@
1
+ const event = require('../event')
2
+ const container = require('../container')
3
+
4
+ module.exports = function () {
5
+ event.dispatcher.on(event.hook.failed, err => {
6
+ container.result().addStats({ failedHooks: 1 })
7
+ })
8
+
9
+ event.dispatcher.on(event.test.before, test => {
10
+ container.result().addTest(test)
11
+ })
12
+ }
@@ -13,7 +13,6 @@ let currentHook
13
13
  module.exports = function () {
14
14
  event.dispatcher.on(event.test.before, test => {
15
15
  test.startedAt = +new Date()
16
- test.artifacts = {}
17
16
  })
18
17
 
19
18
  event.dispatcher.on(event.test.started, test => {
@@ -71,8 +70,7 @@ module.exports = function () {
71
70
  })
72
71
 
73
72
  event.dispatcher.on(event.step.started, step => {
74
- step.startedAt = +new Date()
75
- step.test = currentTest
73
+ store.currentStep = step
76
74
  if (currentHook && Array.isArray(currentHook.steps)) {
77
75
  return currentHook.steps.push(step)
78
76
  }
@@ -81,8 +79,7 @@ module.exports = function () {
81
79
  })
82
80
 
83
81
  event.dispatcher.on(event.step.finished, step => {
84
- step.finishedAt = +new Date()
85
- if (step.startedAt) step.duration = step.finishedAt - step.startedAt
86
- debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`)
82
+ store.currentStep = null
83
+ store.stepOptions = null
87
84
  })
88
85
  }
@@ -2,11 +2,19 @@ const event = require('../event')
2
2
  const store = require('../store')
3
3
 
4
4
  module.exports = function () {
5
+ event.dispatcher.on(event.suite.before, suite => {
6
+ store.currentSuite = suite
7
+ })
8
+
9
+ event.dispatcher.on(event.suite.after, () => {
10
+ store.currentSuite = null
11
+ })
12
+
5
13
  event.dispatcher.on(event.test.before, test => {
6
14
  store.currentTest = test
7
15
  })
8
16
 
9
- event.dispatcher.on(event.test.finished, test => {
17
+ event.dispatcher.on(event.test.finished, () => {
10
18
  store.currentTest = null
11
19
  })
12
20
  }
@@ -13,12 +13,19 @@ const injectHook = function (inject, suite) {
13
13
  recorder.throw(err)
14
14
  }
15
15
  recorder.catch(err => {
16
- event.emit(event.test.failed, suite, err)
16
+ suiteTestFailedHookError(suite, err)
17
17
  throw err
18
18
  })
19
19
  return recorder.promise()
20
20
  }
21
21
 
22
+ function suiteTestFailedHookError(suite, err, hookName) {
23
+ suite.eachTest(test => {
24
+ test.err = err
25
+ event.emit(event.test.failed, test, err, ucfirst(hookName))
26
+ })
27
+ }
28
+
22
29
  function makeDoneCallableOnce(done) {
23
30
  let called = false
24
31
  return function (err) {
@@ -61,6 +68,7 @@ module.exports.test = test => {
61
68
  err = newErr
62
69
  }
63
70
  }
71
+ test.err = err
64
72
  event.emit(event.test.failed, test, err)
65
73
  event.emit(event.test.finished, test)
66
74
  recorder.add(() => doneFn(err))
@@ -112,7 +120,7 @@ module.exports.injected = function (fn, suite, hookName) {
112
120
  const errHandler = err => {
113
121
  recorder.session.start('teardown')
114
122
  recorder.cleanAsyncErr()
115
- event.emit(event.test.failed, suite, err)
123
+ if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName)
116
124
  if (hookName === 'after') event.emit(event.test.after, suite)
117
125
  if (hookName === 'afterSuite') event.emit(event.suite.after, suite)
118
126
  recorder.add(() => doneFn(err))
@@ -137,11 +145,13 @@ module.exports.injected = function (fn, suite, hookName) {
137
145
  const opts = suite.opts || {}
138
146
  const retries = opts[`retry${ucfirst(hookName)}`] || 0
139
147
 
148
+ const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
149
+
140
150
  promiseRetry(
141
151
  async (retry, number) => {
142
152
  try {
143
153
  recorder.startUnlessRunning()
144
- await fn.call(this, getInjectedArguments(fn))
154
+ await fn.call(this, { ...getInjectedArguments(fn), suite, test: currentTest })
145
155
  await recorder.promise().catch(err => retry(err))
146
156
  } catch (err) {
147
157
  retry(err)
@@ -156,6 +166,7 @@ module.exports.injected = function (fn, suite, hookName) {
156
166
  )
157
167
  .then(() => {
158
168
  recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite))
169
+ recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
159
170
  recorder.add(`finish ${hookName} hook`, doneFn)
160
171
  recorder.catch()
161
172
  })
@@ -166,6 +177,7 @@ module.exports.injected = function (fn, suite, hookName) {
166
177
  errHandler(err)
167
178
  })
168
179
  recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e))
180
+ recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
169
181
  })
170
182
  }
171
183
  }