codeceptjs 3.6.10 → 3.7.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -119
- package/bin/codecept.js +9 -2
- 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 +87 -83
- package/lib/command/check.js +186 -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 +10 -8
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +1 -3
- package/lib/command/init.js +8 -12
- package/lib/command/interactive.js +2 -2
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +5 -57
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +68 -232
- package/lib/container.js +354 -237
- 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 +218 -0
- package/lib/els.js +158 -0
- package/lib/event.js +19 -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 +45 -51
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +3 -3
- package/lib/helper/JSONResponse.js +57 -37
- package/lib/helper/Nightmare.js +35 -53
- package/lib/helper/Playwright.js +211 -252
- package/lib/helper/Protractor.js +54 -77
- package/lib/helper/Puppeteer.js +139 -232
- package/lib/helper/REST.js +5 -17
- package/lib/helper/TestCafe.js +21 -44
- package/lib/helper/WebDriver.js +131 -169
- 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/steps.js +20 -18
- package/lib/listener/store.js +20 -0
- package/lib/mocha/asyncWrapper.js +216 -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 +24 -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 +21 -6
- package/lib/mocha/suite.js +81 -0
- package/lib/mocha/test.js +159 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +219 -0
- package/lib/output.js +82 -62
- package/lib/pause.js +155 -138
- package/lib/plugin/analyze.js +349 -0
- package/lib/plugin/autoDelay.js +6 -6
- package/lib/plugin/autoLogin.js +6 -7
- 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/pageInfo.js +140 -0
- package/lib/plugin/retryFailedStep.js +4 -4
- package/lib/plugin/retryTo.js +18 -118
- package/lib/plugin/screenshotOnFail.js +17 -49
- 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 +17 -107
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +146 -125
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/secret.js +1 -1
- 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 +21 -332
- package/lib/steps.js +50 -0
- package/lib/store.js +10 -2
- package/lib/template/heal.js +2 -11
- package/lib/timeout.js +66 -0
- package/lib/utils.js +317 -216
- package/lib/within.js +73 -55
- package/lib/workers.js +259 -275
- package/package.json +56 -54
- package/typings/index.d.ts +175 -186
- package/typings/promiseBasedTypes.d.ts +164 -17
- package/typings/types.d.ts +284 -115
- 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
package/lib/plugin/heal.js
CHANGED
|
@@ -5,6 +5,7 @@ const event = require('../event')
|
|
|
5
5
|
const output = require('../output')
|
|
6
6
|
const heal = require('../heal')
|
|
7
7
|
const store = require('../store')
|
|
8
|
+
const container = require('../container')
|
|
8
9
|
|
|
9
10
|
const defaultConfig = {
|
|
10
11
|
healLimit: 2,
|
|
@@ -31,10 +32,7 @@ const defaultConfig = {
|
|
|
31
32
|
module.exports = function (config = {}) {
|
|
32
33
|
if (store.debugMode && !process.env.DEBUG) {
|
|
33
34
|
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
|
-
)
|
|
35
|
+
output.plugin('heal', 'Healing is disabled in --debug mode, use DEBUG="codeceptjs:heal" to enable it in debug mode')
|
|
38
36
|
})
|
|
39
37
|
return
|
|
40
38
|
}
|
|
@@ -48,21 +46,21 @@ module.exports = function (config = {}) {
|
|
|
48
46
|
|
|
49
47
|
config = Object.assign(defaultConfig, config)
|
|
50
48
|
|
|
51
|
-
event.dispatcher.on(event.test.before,
|
|
49
|
+
event.dispatcher.on(event.test.before, test => {
|
|
52
50
|
currentTest = test
|
|
53
51
|
healedSteps = 0
|
|
54
52
|
caughtError = null
|
|
55
53
|
})
|
|
56
54
|
|
|
57
|
-
event.dispatcher.on(event.step.started,
|
|
55
|
+
event.dispatcher.on(event.step.started, step => (currentStep = step))
|
|
58
56
|
|
|
59
|
-
event.dispatcher.on(event.step.after,
|
|
57
|
+
event.dispatcher.on(event.step.after, step => {
|
|
60
58
|
if (isHealing) return
|
|
61
59
|
if (healTries >= config.healLimit) return // out of limit
|
|
62
60
|
|
|
63
61
|
if (!heal.hasCorrespondingRecipes(step)) return
|
|
64
62
|
|
|
65
|
-
recorder.catchWithoutStop(async
|
|
63
|
+
recorder.catchWithoutStop(async err => {
|
|
66
64
|
isHealing = true
|
|
67
65
|
if (caughtError === err) throw err // avoid double handling
|
|
68
66
|
caughtError = err
|
|
@@ -99,7 +97,7 @@ module.exports = function (config = {}) {
|
|
|
99
97
|
|
|
100
98
|
print(`${colors.bold(heal.fixes.length)} ${heal.fixes.length === 1 ? 'step was' : 'steps were'} healed`)
|
|
101
99
|
|
|
102
|
-
const suggestions = heal.fixes.filter(
|
|
100
|
+
const suggestions = heal.fixes.filter(fix => fix.recipe && heal.recipes[fix.recipe].suggest)
|
|
103
101
|
|
|
104
102
|
if (!suggestions.length) return
|
|
105
103
|
|
|
@@ -118,4 +116,33 @@ module.exports = function (config = {}) {
|
|
|
118
116
|
i++
|
|
119
117
|
}
|
|
120
118
|
})
|
|
119
|
+
|
|
120
|
+
event.dispatcher.on(event.workers.result, result => {
|
|
121
|
+
const { print } = output
|
|
122
|
+
|
|
123
|
+
const healedTests = Object.values(result.tests)
|
|
124
|
+
.flat()
|
|
125
|
+
.filter(test => test.notes.some(note => note.type === 'heal'))
|
|
126
|
+
if (!healedTests.length) return
|
|
127
|
+
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
print('')
|
|
130
|
+
print('===================')
|
|
131
|
+
print(colors.bold.green('Self-Healing Report:'))
|
|
132
|
+
|
|
133
|
+
print('')
|
|
134
|
+
print('Suggested changes:')
|
|
135
|
+
print('')
|
|
136
|
+
|
|
137
|
+
healedTests.forEach(test => {
|
|
138
|
+
print(`${colors.bold.magenta(test.title)}`)
|
|
139
|
+
test.notes
|
|
140
|
+
.filter(note => note.type === 'heal')
|
|
141
|
+
.forEach(note => {
|
|
142
|
+
print(note.text)
|
|
143
|
+
print('')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
}, 0)
|
|
147
|
+
})
|
|
121
148
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const Container = require('../container')
|
|
4
|
+
const recorder = require('../recorder')
|
|
5
|
+
const event = require('../event')
|
|
6
|
+
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
7
|
+
const { scanForErrorMessages } = require('../html')
|
|
8
|
+
const { output } = require('..')
|
|
9
|
+
const { humanizeString, ucfirst } = require('../utils')
|
|
10
|
+
const { testToFileName } = require('../mocha/test')
|
|
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
|
+
module.exports = 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
|
+
}
|
|
@@ -76,14 +76,14 @@ const defaultConfig = {
|
|
|
76
76
|
* ```
|
|
77
77
|
*
|
|
78
78
|
*/
|
|
79
|
-
module.exports =
|
|
79
|
+
module.exports = config => {
|
|
80
80
|
config = Object.assign(defaultConfig, config)
|
|
81
81
|
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
|
|
82
82
|
const customWhen = config.when
|
|
83
83
|
|
|
84
84
|
let enableRetry = false
|
|
85
85
|
|
|
86
|
-
const when =
|
|
86
|
+
const when = err => {
|
|
87
87
|
if (!enableRetry) return
|
|
88
88
|
const store = require('../store')
|
|
89
89
|
if (store.debugMode) return false
|
|
@@ -92,7 +92,7 @@ module.exports = (config) => {
|
|
|
92
92
|
}
|
|
93
93
|
config.when = when
|
|
94
94
|
|
|
95
|
-
event.dispatcher.on(event.step.started,
|
|
95
|
+
event.dispatcher.on(event.step.started, step => {
|
|
96
96
|
if (process.env.TRY_TO === 'true') {
|
|
97
97
|
log('Info: RetryFailedStep plugin is disabled inside tryTo block')
|
|
98
98
|
return
|
|
@@ -112,7 +112,7 @@ module.exports = (config) => {
|
|
|
112
112
|
enableRetry = false
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
event.dispatcher.on(event.test.before,
|
|
115
|
+
event.dispatcher.on(event.test.before, test => {
|
|
116
116
|
if (test && test.disableRetryFailedStep) return // disable retry when a test is not active
|
|
117
117
|
// this env var is used to set the retries inside _before() block of helpers
|
|
118
118
|
process.env.FAILED_STEP_RETRIES = config.retries
|
package/lib/plugin/retryTo.js
CHANGED
|
@@ -1,123 +1,23 @@
|
|
|
1
|
-
const
|
|
2
|
-
const { debug } = require('../output')
|
|
1
|
+
const { retryTo } = require('../effects')
|
|
3
2
|
|
|
4
|
-
const defaultConfig = {
|
|
5
|
-
registerGlobal: true,
|
|
6
|
-
pollInterval: 200,
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Adds global `retryTo` which retries steps a few times before failing.
|
|
13
|
-
*
|
|
14
|
-
* Enable this plugin in `codecept.conf.js` (enabled by default for new setups):
|
|
15
|
-
*
|
|
16
|
-
* ```js
|
|
17
|
-
* plugins: {
|
|
18
|
-
* retryTo: {
|
|
19
|
-
* enabled: true
|
|
20
|
-
* }
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* Use it in your tests:
|
|
25
|
-
*
|
|
26
|
-
* ```js
|
|
27
|
-
* // retry these steps 5 times before failing
|
|
28
|
-
* await retryTo((tryNum) => {
|
|
29
|
-
* I.switchTo('#editor frame');
|
|
30
|
-
* I.click('Open');
|
|
31
|
-
* I.see('Opened')
|
|
32
|
-
* }, 5);
|
|
33
|
-
* ```
|
|
34
|
-
* Set polling interval as 3rd argument (200ms by default):
|
|
35
|
-
*
|
|
36
|
-
* ```js
|
|
37
|
-
* // retry these steps 5 times before failing
|
|
38
|
-
* await retryTo((tryNum) => {
|
|
39
|
-
* I.switchTo('#editor frame');
|
|
40
|
-
* I.click('Open');
|
|
41
|
-
* I.see('Opened')
|
|
42
|
-
* }, 5, 100);
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* Default polling interval can be changed in a config:
|
|
46
|
-
*
|
|
47
|
-
* ```js
|
|
48
|
-
* plugins: {
|
|
49
|
-
* retryTo: {
|
|
50
|
-
* enabled: true,
|
|
51
|
-
* pollInterval: 500,
|
|
52
|
-
* }
|
|
53
|
-
* }
|
|
54
|
-
* ```
|
|
55
|
-
*
|
|
56
|
-
* Disables retryFailedStep plugin for steps inside a block;
|
|
57
|
-
*
|
|
58
|
-
* Use this plugin if:
|
|
59
|
-
*
|
|
60
|
-
* * you need repeat a set of actions in flaky tests
|
|
61
|
-
* * iframe was not rendered and you need to retry switching to it
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* #### Configuration
|
|
65
|
-
*
|
|
66
|
-
* * `pollInterval` - default interval between retries in ms. 200 by default.
|
|
67
|
-
* * `registerGlobal` - to register `retryTo` function globally, true by default
|
|
68
|
-
*
|
|
69
|
-
* If `registerGlobal` is false you can use retryTo from the plugin:
|
|
70
|
-
*
|
|
71
|
-
* ```js
|
|
72
|
-
* const retryTo = codeceptjs.container.plugins('retryTo');
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
*/
|
|
76
3
|
module.exports = function (config) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Call done if no errors
|
|
97
|
-
recorder.add(() => {
|
|
98
|
-
recorder.session.restore(`retryTo ${tries}`)
|
|
99
|
-
done(null)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Catch errors and retry
|
|
103
|
-
recorder.session.catch((err) => {
|
|
104
|
-
recorder.session.restore(`retryTo ${tries}`)
|
|
105
|
-
if (tries <= maxTries) {
|
|
106
|
-
debug(`Error ${err}... Retrying`)
|
|
107
|
-
recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval))
|
|
108
|
-
} else {
|
|
109
|
-
// if maxTries reached
|
|
110
|
-
handleRetryException(err)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
recorder.add('retryTo', tryBlock).catch((err) => {
|
|
116
|
-
console.error('An error occurred:', err)
|
|
117
|
-
done(null)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
}
|
|
4
|
+
console.log(`
|
|
5
|
+
Deprecation Warning: 'retryTo' has been moved to the effects module.
|
|
6
|
+
You should update your tests to use it as follows:
|
|
7
|
+
|
|
8
|
+
\`\`\`javascript
|
|
9
|
+
const { retryTo } = require('codeceptjs/effects');
|
|
10
|
+
|
|
11
|
+
// Example: Retry these steps 5 times before failing
|
|
12
|
+
await retryTo((tryNum) => {
|
|
13
|
+
I.switchTo('#editor frame');
|
|
14
|
+
I.click('Open');
|
|
15
|
+
I.see('Opened');
|
|
16
|
+
}, 5);
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
For more details, refer to the documentation.
|
|
20
|
+
`)
|
|
121
21
|
|
|
122
22
|
if (config.registerGlobal) {
|
|
123
23
|
global.retryTo = retryTo
|
|
@@ -5,8 +5,9 @@ const Container = require('../container')
|
|
|
5
5
|
const recorder = require('../recorder')
|
|
6
6
|
const event = require('../event')
|
|
7
7
|
const output = require('../output')
|
|
8
|
-
const { fileExists
|
|
8
|
+
const { fileExists } = require('../utils')
|
|
9
9
|
const Codeceptjs = require('../index')
|
|
10
|
+
const { testToFileName } = require('../mocha/test')
|
|
10
11
|
|
|
11
12
|
const defaultConfig = {
|
|
12
13
|
uniqueScreenshotNames: false,
|
|
@@ -14,7 +15,7 @@ const defaultConfig = {
|
|
|
14
15
|
fullPageScreenshots: false,
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const supportedHelpers =
|
|
18
|
+
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Creates screenshot on failure. Screenshot is saved into `output` directory.
|
|
@@ -63,9 +64,7 @@ module.exports = function (config) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
if (Codeceptjs.container.mocha()) {
|
|
66
|
-
options.reportDir =
|
|
67
|
-
Codeceptjs.container.mocha().options.reporterOptions &&
|
|
68
|
-
Codeceptjs.container.mocha().options.reporterOptions.reportDir
|
|
67
|
+
options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions && Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
if (options.disableScreenshots) {
|
|
@@ -73,25 +72,19 @@ module.exports = function (config) {
|
|
|
73
72
|
return
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
event.dispatcher.on(event.test.failed, (test) => {
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
'screenshotOnFail',
|
|
80
|
-
'BeforeSuite/AfterSuite do not have any access to the browser, hence it could not take screenshot.',
|
|
81
|
-
)
|
|
75
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
76
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
|
|
77
|
+
// no browser here
|
|
82
78
|
return
|
|
83
79
|
}
|
|
80
|
+
|
|
84
81
|
recorder.add(
|
|
85
82
|
'screenshot of failed test',
|
|
86
83
|
async () => {
|
|
87
|
-
let fileName = clearString(test.title)
|
|
88
84
|
const dataType = 'image/png'
|
|
89
85
|
// This prevents data driven to be included in the failed screenshot file name
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook')
|
|
94
|
-
fileName = clearString(`${test.title}_${test.ctx.test.title}`)
|
|
86
|
+
let fileName = testToFileName(test)
|
|
87
|
+
|
|
95
88
|
if (options.uniqueScreenshotNames && test) {
|
|
96
89
|
const uuid = _getUUID(test)
|
|
97
90
|
fileName = `${fileName.substring(0, 10)}_${uuid}.failed.png`
|
|
@@ -112,34 +105,20 @@ module.exports = function (config) {
|
|
|
112
105
|
|
|
113
106
|
if (!test.artifacts) test.artifacts = {}
|
|
114
107
|
test.artifacts.screenshot = path.join(global.output_dir, fileName)
|
|
115
|
-
if (
|
|
116
|
-
Container.mocha().options.reporterOptions['mocha-junit-reporter'] &&
|
|
117
|
-
Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments
|
|
118
|
-
) {
|
|
108
|
+
if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
|
|
119
109
|
test.attachments = [path.join(global.output_dir, fileName)]
|
|
120
110
|
}
|
|
121
111
|
|
|
122
112
|
const allureReporter = Container.plugins('allure')
|
|
123
113
|
if (allureReporter) {
|
|
124
|
-
allureReporter.addAttachment(
|
|
125
|
-
'Main session - Last Seen Screenshot',
|
|
126
|
-
fs.readFileSync(path.join(global.output_dir, fileName)),
|
|
127
|
-
dataType,
|
|
128
|
-
)
|
|
114
|
+
allureReporter.addAttachment('Main session - Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), dataType)
|
|
129
115
|
|
|
130
116
|
if (helper.activeSessionName) {
|
|
131
117
|
const sessions = helper.sessionPages || helper.sessionWindows
|
|
132
118
|
for (const sessionName in sessions) {
|
|
133
119
|
const screenshotFileName = `${sessionName}_${fileName}`
|
|
134
|
-
test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(
|
|
135
|
-
|
|
136
|
-
screenshotFileName,
|
|
137
|
-
)
|
|
138
|
-
allureReporter.addAttachment(
|
|
139
|
-
`${sessionName} - Last Seen Screenshot`,
|
|
140
|
-
fs.readFileSync(path.join(global.output_dir, screenshotFileName)),
|
|
141
|
-
dataType,
|
|
142
|
-
)
|
|
120
|
+
test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName)
|
|
121
|
+
allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType)
|
|
143
122
|
}
|
|
144
123
|
}
|
|
145
124
|
}
|
|
@@ -150,14 +129,7 @@ module.exports = function (config) {
|
|
|
150
129
|
}
|
|
151
130
|
} catch (err) {
|
|
152
131
|
output.plugin(err)
|
|
153
|
-
if (
|
|
154
|
-
err &&
|
|
155
|
-
err.type &&
|
|
156
|
-
err.type === 'RuntimeError' &&
|
|
157
|
-
err.message &&
|
|
158
|
-
(err.message.indexOf('was terminated due to') > -1 ||
|
|
159
|
-
err.message.indexOf('no such window: target window already closed') > -1)
|
|
160
|
-
) {
|
|
132
|
+
if (err && err.type && err.type === 'RuntimeError' && err.message && (err.message.indexOf('was terminated due to') > -1 || err.message.indexOf('no such window: target window already closed') > -1)) {
|
|
161
133
|
output.log(`Can't make screenshot, ${err}`)
|
|
162
134
|
helper.isRunning = false
|
|
163
135
|
}
|
|
@@ -168,12 +140,8 @@ module.exports = function (config) {
|
|
|
168
140
|
})
|
|
169
141
|
|
|
170
142
|
function _getUUID(test) {
|
|
171
|
-
if (test.
|
|
172
|
-
return test.
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (test.ctx && test.ctx.test.uuid) {
|
|
176
|
-
return test.ctx.test.uuid
|
|
143
|
+
if (test.uid) {
|
|
144
|
+
return test.uid
|
|
177
145
|
}
|
|
178
146
|
|
|
179
147
|
return Math.floor(new Date().getTime() / 1000)
|
package/lib/plugin/selenoid.js
CHANGED
|
@@ -41,12 +41,7 @@ const dockerCreateScriptArr = [
|
|
|
41
41
|
'aerokube/selenoid:latest-release -log-output-dir /opt/selenoid/logs',
|
|
42
42
|
]
|
|
43
43
|
|
|
44
|
-
const dockerImageCheckScript = [
|
|
45
|
-
'docker images',
|
|
46
|
-
"--filter reference='selenoid/video-recorder'",
|
|
47
|
-
"--filter reference='selenoid/chrome:latest'",
|
|
48
|
-
"--filter reference='selenoid/firefox:latest'",
|
|
49
|
-
].join(' ')
|
|
44
|
+
const dockerImageCheckScript = ['docker images', "--filter reference='selenoid/video-recorder'", "--filter reference='selenoid/chrome:latest'", "--filter reference='selenoid/firefox:latest'"].join(' ')
|
|
50
45
|
|
|
51
46
|
let dockerCreateScript = dockerCreateScriptArr.join(' ')
|
|
52
47
|
let dockerStartScript = 'docker start $name$'
|
|
@@ -56,8 +51,8 @@ let seleniumUrl = 'http://localhost:$port$'
|
|
|
56
51
|
const supportedHelpers = ['WebDriver']
|
|
57
52
|
const SELENOID_START_TIMEOUT = 2000
|
|
58
53
|
const SELENOID_STOP_TIMEOUT = 10000
|
|
59
|
-
const wait =
|
|
60
|
-
new Promise(
|
|
54
|
+
const wait = time =>
|
|
55
|
+
new Promise(res => {
|
|
61
56
|
setTimeout(() => {
|
|
62
57
|
// @ts-ignore
|
|
63
58
|
res()
|
|
@@ -179,7 +174,7 @@ const wait = (time) =>
|
|
|
179
174
|
*
|
|
180
175
|
*/
|
|
181
176
|
|
|
182
|
-
const selenoid =
|
|
177
|
+
const selenoid = config => {
|
|
183
178
|
const helpers = container.helpers()
|
|
184
179
|
let helperName
|
|
185
180
|
|
|
@@ -194,14 +189,7 @@ const selenoid = (config) => {
|
|
|
194
189
|
return // no helpers for Selenoid
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
const {
|
|
198
|
-
autoStart,
|
|
199
|
-
name = 'selenoid',
|
|
200
|
-
deletePassed = true,
|
|
201
|
-
additionalParams = '',
|
|
202
|
-
autoCreate = true,
|
|
203
|
-
port = 4444,
|
|
204
|
-
} = config
|
|
192
|
+
const { autoStart, name = 'selenoid', deletePassed = true, additionalParams = '', autoCreate = true, port = 4444 } = config
|
|
205
193
|
const passedTests = []
|
|
206
194
|
|
|
207
195
|
recorder.startUnlessRunning()
|
|
@@ -213,7 +201,7 @@ const selenoid = (config) => {
|
|
|
213
201
|
output.debug('Staring Selenoid... ')
|
|
214
202
|
return createAndStart(autoCreate)
|
|
215
203
|
.then(() => output.debug('Selenoid started'))
|
|
216
|
-
.catch(
|
|
204
|
+
.catch(err => {
|
|
217
205
|
throw new Error(err.stack)
|
|
218
206
|
})
|
|
219
207
|
})
|
|
@@ -226,7 +214,7 @@ const selenoid = (config) => {
|
|
|
226
214
|
.then(() => deletePassedTests(passedTests))
|
|
227
215
|
.then(stopSelenoid)
|
|
228
216
|
.then(() => output.debug('Selenoid stopped'))
|
|
229
|
-
.catch(
|
|
217
|
+
.catch(err => {
|
|
230
218
|
throw new Error(err.stack)
|
|
231
219
|
})
|
|
232
220
|
})
|
|
@@ -241,7 +229,7 @@ const selenoid = (config) => {
|
|
|
241
229
|
}
|
|
242
230
|
})
|
|
243
231
|
|
|
244
|
-
event.dispatcher.on(event.test.before,
|
|
232
|
+
event.dispatcher.on(event.test.before, test => {
|
|
245
233
|
switch (helperName) {
|
|
246
234
|
case 'WebDriver':
|
|
247
235
|
setTestConfigForWebdriver(test)
|
|
@@ -250,7 +238,7 @@ const selenoid = (config) => {
|
|
|
250
238
|
})
|
|
251
239
|
|
|
252
240
|
if (config.enableVideo) {
|
|
253
|
-
event.dispatcher.on(event.test.passed,
|
|
241
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
254
242
|
if (deletePassed) {
|
|
255
243
|
passedTests.push(test.title)
|
|
256
244
|
} else {
|
|
@@ -258,7 +246,7 @@ const selenoid = (config) => {
|
|
|
258
246
|
}
|
|
259
247
|
})
|
|
260
248
|
|
|
261
|
-
event.dispatcher.on(event.test.failed,
|
|
249
|
+
event.dispatcher.on(event.test.failed, test => {
|
|
262
250
|
test.artifacts.video = videoSaved(test)
|
|
263
251
|
})
|
|
264
252
|
}
|
|
@@ -312,16 +300,12 @@ const pullImage = async () => {
|
|
|
312
300
|
console.time('Pulled containers')
|
|
313
301
|
if (!stdout.includes('selenoid/video-recorder')) {
|
|
314
302
|
output.debug('Pulling selenoid/video-recorder...')
|
|
315
|
-
resultPromise = exec('docker pull selenoid/video-recorder:latest-release').then(() =>
|
|
316
|
-
output.debug('Pulled in selenoid/video-recorder'),
|
|
317
|
-
)
|
|
303
|
+
resultPromise = exec('docker pull selenoid/video-recorder:latest-release').then(() => output.debug('Pulled in selenoid/video-recorder'))
|
|
318
304
|
pulls.push(resultPromise)
|
|
319
305
|
}
|
|
320
306
|
if (!stdout.includes('selenoid/chrome')) {
|
|
321
307
|
output.debug('Pulling selenoid/chrome...')
|
|
322
|
-
resultPromise = exec('docker pull selenoid/chrome:latest').then(() =>
|
|
323
|
-
output.debug('Pulled in selenoid/video-recorder'),
|
|
324
|
-
)
|
|
308
|
+
resultPromise = exec('docker pull selenoid/chrome:latest').then(() => output.debug('Pulled in selenoid/video-recorder'))
|
|
325
309
|
pulls.push(resultPromise)
|
|
326
310
|
}
|
|
327
311
|
if (!stdout.includes('selenoid/firefox')) {
|
|
@@ -341,16 +325,12 @@ function createAndStart(autoCreate) {
|
|
|
341
325
|
}
|
|
342
326
|
|
|
343
327
|
function deletePassedTests(passedTests) {
|
|
344
|
-
const deleteVideoPromiseList = passedTests
|
|
345
|
-
|
|
346
|
-
.map((test) => axios.delete(`${seleniumUrl}/video/${test}.mp4`))
|
|
347
|
-
const deleteLogPromiseList = passedTests
|
|
348
|
-
.map(clearString)
|
|
349
|
-
.map((test) => axios.delete(`${seleniumUrl}/logs/${test}.log`))
|
|
328
|
+
const deleteVideoPromiseList = passedTests.map(clearString).map(test => axios.delete(`${seleniumUrl}/video/${test}.mp4`))
|
|
329
|
+
const deleteLogPromiseList = passedTests.map(clearString).map(test => axios.delete(`${seleniumUrl}/logs/${test}.log`))
|
|
350
330
|
|
|
351
331
|
return Promise.all(deleteVideoPromiseList.concat(deleteLogPromiseList))
|
|
352
332
|
.then(() => output.debug('Deleted videos and logs for all passed tests'))
|
|
353
|
-
.catch(
|
|
333
|
+
.catch(err => output.error(`Error while deleting video and log files ${err.stack}`))
|
|
354
334
|
}
|
|
355
335
|
|
|
356
336
|
function setOptionsForWebdriver(config) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const Container = require('../container')
|
|
2
|
+
// due to using this in internal tooling we won't post deprecation warning
|
|
3
|
+
// but please switch to Container.STANDARD_ACTING_HELPERS
|
|
4
|
+
const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
|
|
2
5
|
|
|
3
6
|
module.exports = standardActingHelpers
|