codeceptjs 3.7.0-beta.1 → 3.7.0-beta.11
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.
- package/README.md +9 -10
- package/bin/codecept.js +7 -0
- package/lib/actor.js +46 -92
- package/lib/ai.js +130 -121
- package/lib/codecept.js +2 -2
- package/lib/command/check.js +186 -0
- package/lib/command/definitions.js +3 -1
- package/lib/command/interactive.js +1 -1
- package/lib/command/run-workers.js +2 -54
- package/lib/command/workers/runTests.js +64 -225
- package/lib/container.js +32 -0
- package/lib/effects.js +218 -0
- package/lib/els.js +87 -106
- package/lib/event.js +18 -17
- package/lib/heal.js +10 -0
- package/lib/helper/AI.js +2 -1
- package/lib/helper/Appium.js +31 -22
- package/lib/helper/Playwright.js +22 -1
- package/lib/helper/Puppeteer.js +5 -0
- package/lib/helper/WebDriver.js +29 -8
- package/lib/listener/emptyRun.js +2 -5
- package/lib/listener/exit.js +5 -8
- package/lib/listener/globalTimeout.js +66 -10
- package/lib/listener/result.js +12 -0
- package/lib/listener/steps.js +3 -6
- package/lib/listener/store.js +9 -1
- package/lib/mocha/asyncWrapper.js +15 -3
- package/lib/mocha/cli.js +79 -28
- package/lib/mocha/featureConfig.js +13 -0
- package/lib/mocha/hooks.js +32 -3
- package/lib/mocha/inject.js +5 -0
- package/lib/mocha/scenarioConfig.js +11 -0
- package/lib/mocha/suite.js +27 -1
- package/lib/mocha/test.js +102 -3
- package/lib/mocha/types.d.ts +11 -0
- package/lib/output.js +75 -73
- package/lib/pause.js +3 -10
- package/lib/plugin/analyze.js +349 -0
- package/lib/plugin/autoDelay.js +2 -2
- package/lib/plugin/commentStep.js +5 -0
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/heal.js +30 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryTo.js +18 -118
- package/lib/plugin/screenshotOnFail.js +12 -17
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +6 -5
- package/lib/plugin/stepTimeout.js +1 -1
- package/lib/plugin/tryTo.js +17 -107
- package/lib/recorder.js +5 -5
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/step/base.js +228 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +20 -347
- package/lib/steps.js +50 -0
- package/lib/store.js +4 -0
- package/lib/timeout.js +66 -0
- package/lib/utils.js +93 -0
- package/lib/within.js +2 -2
- package/lib/workers.js +29 -49
- package/package.json +23 -20
- package/typings/index.d.ts +5 -4
- package/typings/promiseBasedTypes.d.ts +507 -49
- package/typings/types.d.ts +623 -73
- package/lib/listener/artifacts.js +0 -19
- 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
|
}
|
package/lib/helper/Appium.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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()}/
|
|
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()}/
|
|
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()}/
|
|
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()}/
|
|
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(
|
|
962
|
+
async hideDeviceKeyboard() {
|
|
971
963
|
onlyForApps.call(this)
|
|
972
|
-
|
|
973
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -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
|
}
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -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
|
}
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/listener/emptyRun.js
CHANGED
|
@@ -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
|
|
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?')
|
package/lib/listener/exit.js
CHANGED
|
@@ -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,
|
|
7
|
-
|
|
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,
|
|
15
|
-
|
|
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
|
|
6
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
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
|
+
}
|
package/lib/listener/steps.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`)
|
|
82
|
+
store.currentStep = null
|
|
83
|
+
store.stepOptions = null
|
|
87
84
|
})
|
|
88
85
|
}
|
package/lib/listener/store.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|