codeceptjs 4.0.0-beta.4 → 4.0.0-beta.5
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 +134 -119
- package/bin/codecept.js +12 -2
- package/bin/test-server.js +53 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +66 -102
- package/lib/ai.js +130 -121
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +139 -87
- package/lib/command/check.js +201 -0
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +8 -26
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +75 -73
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +42 -8
- package/lib/command/init.js +13 -12
- package/lib/command/interactive.js +10 -2
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple/chunk.js +48 -45
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +21 -58
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +262 -220
- package/lib/container.js +386 -238
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/effects.js +223 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +158 -0
- package/lib/event.js +21 -17
- package/lib/heal.js +88 -80
- package/lib/helper/AI.js +2 -1
- package/lib/helper/ApiDataFactory.js +3 -6
- package/lib/helper/Appium.js +47 -51
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +3 -3
- package/lib/helper/JSONResponse.js +75 -37
- package/lib/helper/Mochawesome.js +31 -9
- package/lib/helper/Nightmare.js +35 -53
- package/lib/helper/Playwright.js +262 -267
- package/lib/helper/Protractor.js +54 -77
- package/lib/helper/Puppeteer.js +246 -260
- package/lib/helper/REST.js +5 -17
- package/lib/helper/TestCafe.js +21 -44
- package/lib/helper/WebDriver.js +151 -170
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/testcafe/testcafe-utils.js +26 -27
- package/lib/listener/emptyRun.js +55 -0
- package/lib/listener/exit.js +7 -10
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/globalTimeout.js +165 -0
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/result.js +12 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +32 -18
- package/lib/listener/store.js +20 -0
- package/lib/mocha/asyncWrapper.js +231 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +308 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +32 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +112 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +29 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
- package/lib/mocha/suite.js +82 -0
- package/lib/mocha/test.js +181 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +232 -0
- package/lib/output.js +82 -62
- package/lib/pause.js +160 -138
- package/lib/plugin/analyze.js +396 -0
- package/lib/plugin/auth.js +435 -0
- package/lib/plugin/autoDelay.js +8 -8
- package/lib/plugin/autoLogin.js +3 -338
- package/lib/plugin/commentStep.js +6 -1
- package/lib/plugin/coverage.js +10 -19
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +36 -9
- package/lib/plugin/htmlReporter.js +1947 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryFailedStep.js +17 -18
- package/lib/plugin/retryTo.js +2 -113
- package/lib/plugin/screenshotOnFail.js +17 -58
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +56 -17
- package/lib/plugin/stepTimeout.js +5 -12
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +3 -102
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +155 -124
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/secret.js +1 -1
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -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 +21 -332
- package/lib/steps.js +50 -0
- package/lib/store.js +37 -5
- package/lib/template/heal.js +2 -11
- package/lib/test-server.js +323 -0
- package/lib/timeout.js +66 -0
- package/lib/utils.js +351 -218
- package/lib/within.js +75 -55
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +386 -276
- package/package.json +76 -70
- package/translations/de-DE.js +4 -3
- package/translations/fr-FR.js +4 -3
- package/translations/index.js +1 -0
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +9 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +188 -186
- package/typings/promiseBasedTypes.d.ts +18 -705
- package/typings/types.d.ts +301 -804
- package/lib/cli.js +0 -256
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -113
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
|
@@ -2,66 +2,66 @@
|
|
|
2
2
|
* Class to handle the interaction with the Dialog (Popup) Class from Puppeteer
|
|
3
3
|
*/
|
|
4
4
|
class Popup {
|
|
5
|
-
constructor(popup, defaultAction) {
|
|
6
|
-
this._popup = popup
|
|
7
|
-
this._actionType = ''
|
|
8
|
-
this._defaultAction = defaultAction
|
|
5
|
+
constructor(popup = null, defaultAction = '') {
|
|
6
|
+
this._popup = popup
|
|
7
|
+
this._actionType = ''
|
|
8
|
+
this._defaultAction = defaultAction
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
_assertValidActionType(action) {
|
|
12
12
|
if (['accept', 'cancel'].indexOf(action) === -1) {
|
|
13
|
-
throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted')
|
|
13
|
+
throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted')
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
set defaultAction(action) {
|
|
18
|
-
this._assertValidActionType(action)
|
|
19
|
-
this._defaultAction = action
|
|
18
|
+
this._assertValidActionType(action)
|
|
19
|
+
this._defaultAction = action
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
get defaultAction() {
|
|
23
|
-
return this._defaultAction
|
|
23
|
+
return this._defaultAction
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
get popup() {
|
|
27
|
-
return this._popup
|
|
27
|
+
return this._popup
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
set popup(popup) {
|
|
31
31
|
if (this._popup) {
|
|
32
|
-
|
|
32
|
+
return
|
|
33
33
|
}
|
|
34
|
-
this._popup = popup
|
|
34
|
+
this._popup = popup
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
get actionType() {
|
|
38
|
-
return this._actionType
|
|
38
|
+
return this._actionType
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
set actionType(action) {
|
|
42
|
-
this._assertValidActionType(action)
|
|
43
|
-
this._actionType = action
|
|
42
|
+
this._assertValidActionType(action)
|
|
43
|
+
this._actionType = action
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
clear() {
|
|
47
|
-
this._popup = null
|
|
48
|
-
this._actionType = ''
|
|
47
|
+
this._popup = null
|
|
48
|
+
this._actionType = ''
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
assertPopupVisible() {
|
|
52
52
|
if (!this._popup) {
|
|
53
|
-
throw new Error('There is no Popup visible')
|
|
53
|
+
throw new Error('There is no Popup visible')
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
assertPopupActionType(type) {
|
|
58
|
-
this.assertPopupVisible()
|
|
59
|
-
const expectedAction = this._actionType || this._defaultAction
|
|
58
|
+
this.assertPopupVisible()
|
|
59
|
+
const expectedAction = this._actionType || this._defaultAction
|
|
60
60
|
if (expectedAction !== type) {
|
|
61
|
-
throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`)
|
|
61
|
+
throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`)
|
|
62
62
|
}
|
|
63
|
-
this.clear()
|
|
63
|
+
this.clear()
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
module.exports = Popup
|
|
67
|
+
module.exports = Popup
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const { ClientFunction } = require('testcafe')
|
|
1
|
+
const { ClientFunction } = require('testcafe')
|
|
2
2
|
|
|
3
|
-
const assert = require('assert')
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const { getParamNames } = require('../../utils')
|
|
3
|
+
const assert = require('assert')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const { getParamNames } = require('../../utils')
|
|
7
7
|
|
|
8
8
|
const createTestFile = () => {
|
|
9
|
-
assert(global.output_dir, 'global.output_dir must be set')
|
|
9
|
+
assert(global.output_dir, 'global.output_dir must be set')
|
|
10
10
|
|
|
11
|
-
const testFile = path.join(global.output_dir, `${Date.now()}_test.js`)
|
|
12
|
-
const testControllerHolderDir = __dirname.replace(/\\/g, '/')
|
|
11
|
+
const testFile = path.join(global.output_dir, `${Date.now()}_test.js`)
|
|
12
|
+
const testControllerHolderDir = __dirname.replace(/\\/g, '/')
|
|
13
13
|
|
|
14
14
|
fs.writeFileSync(
|
|
15
15
|
testFile,
|
|
@@ -17,40 +17,39 @@ const createTestFile = () => {
|
|
|
17
17
|
fixture("fixture")\n
|
|
18
18
|
test\n
|
|
19
19
|
("test", testControllerHolder.capture)`,
|
|
20
|
-
)
|
|
20
|
+
)
|
|
21
21
|
|
|
22
|
-
return testFile
|
|
23
|
-
}
|
|
22
|
+
return testFile
|
|
23
|
+
}
|
|
24
24
|
|
|
25
25
|
// TODO Better error mapping (actual, expected)
|
|
26
|
-
const mapError =
|
|
26
|
+
const mapError = testcafeError => {
|
|
27
27
|
// console.log('TODO map error better', JSON.stringify(testcafeError, null, 2));
|
|
28
28
|
if (testcafeError.errMsg) {
|
|
29
|
-
throw new Error(testcafeError.errMsg)
|
|
29
|
+
throw new Error(testcafeError.errMsg)
|
|
30
30
|
}
|
|
31
|
-
const errorInfo = `${testcafeError.callsite ? JSON.stringify(testcafeError.callsite) : ''} ${testcafeError.apiFnChain || JSON.stringify(testcafeError)}
|
|
32
|
-
throw new Error(`TestCafe Error: ${errorInfo}`)
|
|
33
|
-
}
|
|
31
|
+
const errorInfo = `${testcafeError.callsite ? JSON.stringify(testcafeError.callsite) : ''} ${testcafeError.apiFnChain || JSON.stringify(testcafeError)}`
|
|
32
|
+
throw new Error(`TestCafe Error: ${errorInfo}`)
|
|
33
|
+
}
|
|
34
34
|
|
|
35
35
|
function createClientFunction(func, args) {
|
|
36
36
|
if (!args || !args.length) {
|
|
37
|
-
return ClientFunction(func)
|
|
37
|
+
return ClientFunction(func)
|
|
38
38
|
}
|
|
39
|
-
const paramNames = getParamNames(func)
|
|
40
|
-
const dependencies = {}
|
|
41
|
-
paramNames.forEach((param, i) => dependencies[param] = args[i])
|
|
39
|
+
const paramNames = getParamNames(func)
|
|
40
|
+
const dependencies = {}
|
|
41
|
+
paramNames.forEach((param, i) => (dependencies[param] = args[i]))
|
|
42
42
|
|
|
43
|
-
return ClientFunction(getFuncBody(func), { dependencies })
|
|
43
|
+
return ClientFunction(getFuncBody(func), { dependencies })
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function getFuncBody(func) {
|
|
47
|
-
let fnStr = func.toString()
|
|
48
|
-
const arrowIndex = fnStr.indexOf('=>')
|
|
47
|
+
let fnStr = func.toString()
|
|
48
|
+
const arrowIndex = fnStr.indexOf('=>')
|
|
49
49
|
if (arrowIndex >= 0) {
|
|
50
|
-
fnStr = fnStr.slice(arrowIndex + 2)
|
|
50
|
+
fnStr = fnStr.slice(arrowIndex + 2)
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
return eval(`() => ${fnStr}`);
|
|
52
|
+
return eval(`() => ${fnStr}`)
|
|
54
53
|
}
|
|
55
54
|
// TODO: support general functions
|
|
56
55
|
}
|
|
@@ -59,4 +58,4 @@ module.exports = {
|
|
|
59
58
|
createTestFile,
|
|
60
59
|
mapError,
|
|
61
60
|
createClientFunction,
|
|
62
|
-
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const figures = require('figures')
|
|
2
|
+
const Container = require('../container')
|
|
3
|
+
const event = require('../event')
|
|
4
|
+
const output = require('../output')
|
|
5
|
+
const { searchWithFusejs } = require('../utils')
|
|
6
|
+
|
|
7
|
+
module.exports = function () {
|
|
8
|
+
let isEmptyRun = true
|
|
9
|
+
|
|
10
|
+
event.dispatcher.on(event.test.before, test => {
|
|
11
|
+
isEmptyRun = false
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
event.dispatcher.on(event.all.result, () => {
|
|
15
|
+
if (isEmptyRun) {
|
|
16
|
+
const mocha = Container.mocha()
|
|
17
|
+
|
|
18
|
+
if (mocha.options.grep) {
|
|
19
|
+
output.print()
|
|
20
|
+
output.print('No tests found by pattern: ' + mocha.options.grep)
|
|
21
|
+
|
|
22
|
+
const allTests = []
|
|
23
|
+
mocha.suite.suites.forEach(suite => {
|
|
24
|
+
suite.tests.forEach(test => {
|
|
25
|
+
allTests.push(test.fullTitle())
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const results = searchWithFusejs(allTests, mocha.options.grep.toString(), {
|
|
30
|
+
includeScore: true,
|
|
31
|
+
threshold: 0.6,
|
|
32
|
+
caseSensitive: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (results.length > 0) {
|
|
36
|
+
output.print()
|
|
37
|
+
output.print('Maybe you wanted to run one of these tests?')
|
|
38
|
+
results.forEach(result => {
|
|
39
|
+
output.print(figures.checkboxOff, output.styles.log(result.item))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
output.print()
|
|
43
|
+
output.print(output.styles.debug('To run the first test use the following command:'))
|
|
44
|
+
output.print(output.styles.bold('npx codeceptjs run --debug --grep "' + results[0].item + '"'))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (process.env.CI && !process.env.DONT_FAIL_ON_EMPTY_RUN) {
|
|
48
|
+
output.print()
|
|
49
|
+
output.error('No tests were executed. Failing on CI to avoid false positives')
|
|
50
|
+
output.error('To disable this check, set `DONT_FAIL_ON_EMPTY_RUN` environment variable to true in CI config')
|
|
51
|
+
process.exitCode = 1
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
package/lib/listener/exit.js
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
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
|
-
|
|
17
|
-
const id = testOrSuite.uid || (testOrSuite.ctx && testOrSuite.ctx.test.uid) || 'empty'
|
|
18
|
-
failedTests = failedTests.filter((failed) => id !== failed)
|
|
13
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
14
|
+
const id = test.uid || (test.ctx && test.ctx.test.uid) || 'empty'
|
|
15
|
+
failedTests = failedTests.filter(failed => id !== failed)
|
|
19
16
|
})
|
|
20
17
|
|
|
21
|
-
process.on('beforeExit',
|
|
18
|
+
process.on('beforeExit', code => {
|
|
22
19
|
if (failedTests.length) {
|
|
23
20
|
code = 1
|
|
24
21
|
}
|
|
@@ -6,7 +6,7 @@ const { isNotSet } = require('../utils')
|
|
|
6
6
|
const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
|
|
7
7
|
|
|
8
8
|
module.exports = function () {
|
|
9
|
-
event.dispatcher.on(event.suite.before,
|
|
9
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
10
10
|
let retryConfig = Config.get('retry')
|
|
11
11
|
if (!retryConfig) return
|
|
12
12
|
|
|
@@ -28,8 +28,8 @@ module.exports = function () {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
hooks
|
|
31
|
-
.filter(
|
|
32
|
-
.forEach(
|
|
31
|
+
.filter(hook => !!config[hook])
|
|
32
|
+
.forEach(hook => {
|
|
33
33
|
if (isNotSet(suite.opts[`retry${hook}`])) suite.opts[`retry${hook}`] = config[hook]
|
|
34
34
|
})
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@ module.exports = function () {
|
|
|
41
41
|
}
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
event.dispatcher.on(event.test.before,
|
|
44
|
+
event.dispatcher.on(event.test.before, test => {
|
|
45
45
|
let retryConfig = Config.get('retry')
|
|
46
46
|
if (!retryConfig) return
|
|
47
47
|
|
|
@@ -54,7 +54,7 @@ module.exports = function () {
|
|
|
54
54
|
retryConfig = [retryConfig]
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
retryConfig = retryConfig.filter(
|
|
57
|
+
retryConfig = retryConfig.filter(config => !!config.Scenario)
|
|
58
58
|
|
|
59
59
|
for (const config of retryConfig) {
|
|
60
60
|
if (config.grep) {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const event = require('../event')
|
|
2
|
+
const output = require('../output')
|
|
3
|
+
const recorder = require('../recorder')
|
|
4
|
+
const Config = require('../config')
|
|
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')
|
|
9
|
+
|
|
10
|
+
module.exports = function () {
|
|
11
|
+
let timeout
|
|
12
|
+
let suiteTimeout = []
|
|
13
|
+
let currentTest
|
|
14
|
+
let currentTimeout
|
|
15
|
+
|
|
16
|
+
if (!store.timeouts) {
|
|
17
|
+
console.log('Timeouts were disabled')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
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
|
+
|
|
34
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
35
|
+
suiteTimeout = []
|
|
36
|
+
let timeoutConfig = Config.get('timeout')
|
|
37
|
+
|
|
38
|
+
if (timeoutConfig) {
|
|
39
|
+
debug('config:', timeoutConfig)
|
|
40
|
+
if (!Number.isNaN(+timeoutConfig)) {
|
|
41
|
+
checkForSeconds(timeoutConfig)
|
|
42
|
+
suiteTimeout.push(timeoutConfig)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Array.isArray(timeoutConfig)) {
|
|
46
|
+
timeoutConfig = [timeoutConfig]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const config of timeoutConfig.filter(c => !!c.Feature)) {
|
|
50
|
+
if (config.grep) {
|
|
51
|
+
if (!suite.title.includes(config.grep)) continue
|
|
52
|
+
}
|
|
53
|
+
suiteTimeout.push(config.Feature)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (suite.totalTimeout) suiteTimeout.push(suite.totalTimeout)
|
|
58
|
+
output.log(`Timeouts: ${suiteTimeout}`)
|
|
59
|
+
|
|
60
|
+
if (suiteTimeout.length > 0) debug(suite.title, 'timeout', suiteTimeout)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
event.dispatcher.on(event.test.before, test => {
|
|
64
|
+
currentTest = test
|
|
65
|
+
let testTimeout = null
|
|
66
|
+
|
|
67
|
+
let timeoutConfig = Config.get('timeout')
|
|
68
|
+
|
|
69
|
+
if (typeof timeoutConfig === 'object' || Array.isArray(timeoutConfig)) {
|
|
70
|
+
if (!Array.isArray(timeoutConfig)) {
|
|
71
|
+
timeoutConfig = [timeoutConfig]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const config of timeoutConfig.filter(c => !!c.Scenario)) {
|
|
75
|
+
console.log('Test Timeout', config, test.title.includes(config.grep))
|
|
76
|
+
if (config.grep) {
|
|
77
|
+
if (!test.title.includes(config.grep)) continue
|
|
78
|
+
}
|
|
79
|
+
testTimeout = config.Scenario
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
timeout = test.totalTimeout || testTimeout || suiteTimeout[suiteTimeout.length - 1]
|
|
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
|
+
|
|
92
|
+
currentTimeout = timeout
|
|
93
|
+
output.debug(`Test Timeout: ${timeout}s`)
|
|
94
|
+
timeout *= 1000
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
98
|
+
currentTest = null
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
event.dispatcher.on(event.test.failed, test => {
|
|
102
|
+
currentTest = null
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
event.dispatcher.on(event.step.before, step => {
|
|
106
|
+
if (typeof timeout !== 'number') return
|
|
107
|
+
|
|
108
|
+
if (!store.timeouts) {
|
|
109
|
+
debug('step', step.toCode().trim(), 'timeout disabled')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (timeout < 0) {
|
|
114
|
+
debug('Previous steps timed out, setting timeout to 0.01s')
|
|
115
|
+
step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite)
|
|
116
|
+
} else {
|
|
117
|
+
debug(`Setting timeout ${timeout}ms for step ${step.toCode().trim()}`)
|
|
118
|
+
step.setTimeout(timeout, TIMEOUT_ORDER.testOrSuite)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
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
|
+
|
|
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)
|
|
152
|
+
if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration
|
|
153
|
+
|
|
154
|
+
if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
|
|
155
|
+
debug(`step ${step.toCode().trim()} timed out`)
|
|
156
|
+
recorder.throw(new TestTimeoutError(currentTimeout))
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function checkForSeconds(timeout) {
|
|
162
|
+
if (timeout >= 1000) {
|
|
163
|
+
console.log(`Warning: Timeout was set to ${timeout}secs.\nGlobal timeout should be specified in seconds.`)
|
|
164
|
+
}
|
|
165
|
+
}
|
package/lib/listener/helpers.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function () {
|
|
|
12
12
|
|
|
13
13
|
const runHelpersHook = (hook, param) => {
|
|
14
14
|
if (store.dryRun) return
|
|
15
|
-
Object.values(helpers).forEach(
|
|
15
|
+
Object.values(helpers).forEach(helper => {
|
|
16
16
|
if (helper[hook]) {
|
|
17
17
|
helper[hook](param)
|
|
18
18
|
}
|
|
@@ -21,55 +21,55 @@ module.exports = function () {
|
|
|
21
21
|
|
|
22
22
|
const runAsyncHelpersHook = (hook, param, force) => {
|
|
23
23
|
if (store.dryRun) return
|
|
24
|
-
Object.keys(helpers).forEach(
|
|
24
|
+
Object.keys(helpers).forEach(key => {
|
|
25
25
|
if (!helpers[key][hook]) return
|
|
26
26
|
recorder.add(`hook ${key}.${hook}()`, () => helpers[key][hook](param), force, false)
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
event.dispatcher.on(event.suite.before,
|
|
30
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
31
31
|
// if (suite.parent) return; // only for root suite
|
|
32
32
|
runAsyncHelpersHook('_beforeSuite', suite, true)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
event.dispatcher.on(event.suite.after,
|
|
35
|
+
event.dispatcher.on(event.suite.after, suite => {
|
|
36
36
|
// if (suite.parent) return; // only for root suite
|
|
37
37
|
runAsyncHelpersHook('_afterSuite', suite, true)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
event.dispatcher.on(event.test.started,
|
|
40
|
+
event.dispatcher.on(event.test.started, test => {
|
|
41
41
|
runHelpersHook('_test', test)
|
|
42
|
-
recorder.catch(
|
|
42
|
+
recorder.catch(e => error(e))
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
event.dispatcher.on(event.test.before,
|
|
45
|
+
event.dispatcher.on(event.test.before, test => {
|
|
46
46
|
// schedule config to revert changes
|
|
47
47
|
runAsyncHelpersHook('_before', test, true)
|
|
48
|
-
recorder.catchWithoutStop(
|
|
48
|
+
recorder.catchWithoutStop(e => error(e))
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
event.dispatcher.on(event.test.passed,
|
|
51
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
52
52
|
runAsyncHelpersHook('_passed', test, true)
|
|
53
53
|
// should not fail test execution, so errors should be caught
|
|
54
|
-
recorder.catchWithoutStop(
|
|
54
|
+
recorder.catchWithoutStop(e => error(e))
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
event.dispatcher.on(event.test.failed,
|
|
57
|
+
event.dispatcher.on(event.test.failed, test => {
|
|
58
58
|
runAsyncHelpersHook('_failed', test, true)
|
|
59
59
|
// should not fail test execution, so errors should be caught
|
|
60
|
-
recorder.catchWithoutStop(
|
|
60
|
+
recorder.catchWithoutStop(e => error(e))
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
event.dispatcher.on(event.test.after, () => {
|
|
64
64
|
runAsyncHelpersHook('_after', {}, true)
|
|
65
|
-
recorder.catchWithoutStop(
|
|
65
|
+
recorder.catchWithoutStop(e => error(e))
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
event.dispatcher.on(event.step.before,
|
|
68
|
+
event.dispatcher.on(event.step.before, step => {
|
|
69
69
|
runAsyncHelpersHook('_beforeStep', step)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
event.dispatcher.on(event.step.after,
|
|
72
|
+
event.dispatcher.on(event.step.after, step => {
|
|
73
73
|
runAsyncHelpersHook('_afterStep', step)
|
|
74
74
|
})
|
|
75
75
|
|
package/lib/listener/mocha.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const event = require('../event')
|
|
2
|
+
const { enhanceMochaTest } = require('../mocha/test')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhance retried tests by copying CodeceptJS-specific properties from the original test
|
|
6
|
+
* This fixes the issue where Mocha's shallow clone during retries loses CodeceptJS properties
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function () {
|
|
9
|
+
event.dispatcher.on(event.test.before, test => {
|
|
10
|
+
// Check if this test is a retry (has a reference to the original test)
|
|
11
|
+
const originalTest = test.retriedTest && test.retriedTest()
|
|
12
|
+
|
|
13
|
+
if (originalTest) {
|
|
14
|
+
// This is a retried test - copy CodeceptJS-specific properties from the original
|
|
15
|
+
copyCodeceptJSProperties(originalTest, test)
|
|
16
|
+
|
|
17
|
+
// Ensure the test is enhanced with CodeceptJS functionality
|
|
18
|
+
enhanceMochaTest(test)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Copy CodeceptJS-specific properties from the original test to the retried test
|
|
25
|
+
* @param {CodeceptJS.Test} originalTest - The original test object
|
|
26
|
+
* @param {CodeceptJS.Test} retriedTest - The retried test object
|
|
27
|
+
*/
|
|
28
|
+
function copyCodeceptJSProperties(originalTest, retriedTest) {
|
|
29
|
+
// Copy CodeceptJS-specific properties
|
|
30
|
+
if (originalTest.opts !== undefined) {
|
|
31
|
+
retriedTest.opts = originalTest.opts ? { ...originalTest.opts } : {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (originalTest.tags !== undefined) {
|
|
35
|
+
retriedTest.tags = originalTest.tags ? [...originalTest.tags] : []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (originalTest.notes !== undefined) {
|
|
39
|
+
retriedTest.notes = originalTest.notes ? [...originalTest.notes] : []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (originalTest.meta !== undefined) {
|
|
43
|
+
retriedTest.meta = originalTest.meta ? { ...originalTest.meta } : {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (originalTest.artifacts !== undefined) {
|
|
47
|
+
retriedTest.artifacts = originalTest.artifacts ? [...originalTest.artifacts] : []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (originalTest.steps !== undefined) {
|
|
51
|
+
retriedTest.steps = originalTest.steps ? [...originalTest.steps] : []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (originalTest.config !== undefined) {
|
|
55
|
+
retriedTest.config = originalTest.config ? { ...originalTest.config } : {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (originalTest.inject !== undefined) {
|
|
59
|
+
retriedTest.inject = originalTest.inject ? { ...originalTest.inject } : {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Copy methods that might be missing
|
|
63
|
+
if (originalTest.addNote && !retriedTest.addNote) {
|
|
64
|
+
retriedTest.addNote = function (type, note) {
|
|
65
|
+
this.notes = this.notes || []
|
|
66
|
+
this.notes.push({ type, text: note })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (originalTest.applyOptions && !retriedTest.applyOptions) {
|
|
71
|
+
retriedTest.applyOptions = originalTest.applyOptions.bind(retriedTest)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (originalTest.simplify && !retriedTest.simplify) {
|
|
75
|
+
retriedTest.simplify = originalTest.simplify.bind(retriedTest)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Preserve the uid if it exists
|
|
79
|
+
if (originalTest.uid !== undefined) {
|
|
80
|
+
retriedTest.uid = originalTest.uid
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mark as enhanced
|
|
84
|
+
retriedTest.codeceptjs = true
|
|
85
|
+
}
|