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