codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20
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 +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +71 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +262 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +301 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +109 -50
- package/lib/container.js +641 -261
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- 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/PlaywrightReactVueLocator.js +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- 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 +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +47 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- 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 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils/typescript.js +237 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +124 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- 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 +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -875
- package/typings/types.d.ts +547 -992
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- 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 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
|
@@ -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
|
-
import
|
|
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,18 +22,18 @@ import pause from '../pause';
|
|
|
21
22
|
* ```
|
|
22
23
|
*
|
|
23
24
|
*/
|
|
24
|
-
export default ()
|
|
25
|
-
let failed = false
|
|
25
|
+
export default function() {
|
|
26
|
+
let failed = false
|
|
26
27
|
|
|
27
28
|
event.dispatcher.on(event.test.started, () => {
|
|
28
|
-
failed = false
|
|
29
|
-
})
|
|
29
|
+
failed = false
|
|
30
|
+
})
|
|
30
31
|
|
|
31
32
|
event.dispatcher.on(event.step.failed, () => {
|
|
32
|
-
failed = true
|
|
33
|
-
})
|
|
33
|
+
failed = true
|
|
34
|
+
})
|
|
34
35
|
|
|
35
36
|
event.dispatcher.on(event.test.after, () => {
|
|
36
|
-
if (failed) pause()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
37
|
+
if (failed) pause()
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import recorder from '../recorder.js';
|
|
3
|
-
import * as output from '../output.js';
|
|
1
|
+
import event from '../event.js'
|
|
4
2
|
|
|
5
|
-
import
|
|
3
|
+
import recorder from '../recorder.js'
|
|
4
|
+
|
|
5
|
+
import store from '../store.js'
|
|
6
6
|
|
|
7
7
|
const defaultConfig = {
|
|
8
8
|
retries: 3,
|
|
9
|
-
defaultIgnoredSteps: [
|
|
10
|
-
'amOnPage',
|
|
11
|
-
'wait*',
|
|
12
|
-
'send*',
|
|
13
|
-
'execute*',
|
|
14
|
-
'run*',
|
|
15
|
-
'have*',
|
|
16
|
-
],
|
|
9
|
+
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
|
|
17
10
|
factor: 1.5,
|
|
18
11
|
ignoredSteps: [],
|
|
19
|
-
}
|
|
12
|
+
}
|
|
20
13
|
|
|
21
14
|
/**
|
|
22
15
|
* Retries each failed step in a test.
|
|
@@ -78,51 +71,93 @@ const defaultConfig = {
|
|
|
78
71
|
* Use scenario configuration to disable plugin for a test
|
|
79
72
|
*
|
|
80
73
|
* ```js
|
|
81
|
-
* Scenario('scenario tite', () => {
|
|
74
|
+
* Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
|
|
82
75
|
* // test goes here
|
|
83
|
-
* })
|
|
76
|
+
* })
|
|
84
77
|
* ```
|
|
85
78
|
*
|
|
86
79
|
*/
|
|
87
|
-
export default (config)
|
|
88
|
-
config = Object.assign(defaultConfig, config)
|
|
89
|
-
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
|
|
90
|
-
const customWhen = config.when
|
|
80
|
+
export default function (config) {
|
|
81
|
+
config = Object.assign(defaultConfig, config)
|
|
82
|
+
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
|
|
83
|
+
const customWhen = config.when
|
|
91
84
|
|
|
92
|
-
let enableRetry = false
|
|
85
|
+
let enableRetry = false
|
|
93
86
|
|
|
94
|
-
const when =
|
|
95
|
-
if (!enableRetry) return
|
|
96
|
-
if (store.debugMode) return false
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
const when = err => {
|
|
88
|
+
if (!enableRetry) return
|
|
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
|
|
95
|
+
if (customWhen) return customWhen(err)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
config.when = when
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
100
|
+
// Ensure retry options are available before any steps run
|
|
101
|
+
if (!recorder.retries.find(r => r === config)) {
|
|
102
|
+
recorder.retries.push(config)
|
|
103
|
+
}
|
|
107
104
|
|
|
105
|
+
event.dispatcher.on(event.step.started, step => {
|
|
108
106
|
// if a step is ignored - return
|
|
109
107
|
for (const ignored of config.ignoredSteps) {
|
|
110
|
-
if (step.name === ignored) return
|
|
108
|
+
if (step.name === ignored) return
|
|
111
109
|
if (ignored instanceof RegExp) {
|
|
112
|
-
if (step.name.match(ignored)) return
|
|
113
|
-
} else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
|
|
110
|
+
if (step.name.match(ignored)) return
|
|
111
|
+
} else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
|
|
112
|
+
}
|
|
113
|
+
enableRetry = true // enable retry for a step
|
|
114
|
+
})
|
|
115
|
+
|
|
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, () => {
|
|
118
|
+
enableRetry = false
|
|
119
|
+
})
|
|
120
|
+
|
|
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) test.opts = {}
|
|
125
|
+
if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
|
|
126
|
+
store.autoRetries = false
|
|
127
|
+
return // disable retry when a test is not active
|
|
114
128
|
}
|
|
115
|
-
enableRetry = true; // enable retry for a step
|
|
116
|
-
});
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
// Don't apply plugin retry logic if there are already manual retries configured
|
|
131
|
+
// Check if any retry configs exist that aren't from this plugin
|
|
132
|
+
const hasManualRetries = recorder.retries.some(retry => retry !== config)
|
|
133
|
+
if (hasManualRetries) {
|
|
134
|
+
store.autoRetries = false
|
|
135
|
+
return
|
|
136
|
+
}
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
// this option is used to set the retries inside _before() block of helpers
|
|
139
|
+
store.autoRetries = true
|
|
140
|
+
test.opts.conditionalRetries = config.retries
|
|
141
|
+
// debug: record applied retries value for tests
|
|
142
|
+
if (process.env.DEBUG_RETRY_PLUGIN) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
|
|
145
|
+
}
|
|
146
|
+
recorder.retry(config)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Fallback for environments where event.test.before wasn't emitted (runner scenarios)
|
|
150
|
+
event.dispatcher.on(event.test.started, test => {
|
|
151
|
+
if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
|
|
152
|
+
|
|
153
|
+
// Don't apply plugin retry logic if there are already manual retries configured
|
|
154
|
+
// Check if any retry configs exist that aren't from this plugin
|
|
155
|
+
const hasManualRetries = recorder.retries.some(retry => retry !== config)
|
|
156
|
+
if (hasManualRetries) return
|
|
157
|
+
|
|
158
|
+
if (!store.autoRetries) {
|
|
159
|
+
store.autoRetries = true
|
|
160
|
+
test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import Container from '../container.js';
|
|
4
|
-
import recorder from '../recorder.js';
|
|
5
|
-
import * as event from '../event.js';
|
|
6
|
-
import * as output from '../output.js';
|
|
7
|
-
import { fileExists, clearString } from '../utils.js';
|
|
8
|
-
import * as Codeceptjs from '../index.js';
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
9
3
|
|
|
10
|
-
import
|
|
4
|
+
import Container from '../container.js'
|
|
5
|
+
|
|
6
|
+
import recorder from '../recorder.js'
|
|
7
|
+
|
|
8
|
+
import event from '../event.js'
|
|
9
|
+
|
|
10
|
+
import output from '../output.js'
|
|
11
|
+
|
|
12
|
+
import { fileExists } from '../utils.js'
|
|
13
|
+
import Codeceptjs from '../index.js'
|
|
14
|
+
import { testToFileName } from '../mocha/test.js'
|
|
11
15
|
|
|
12
16
|
const defaultConfig = {
|
|
13
17
|
uniqueScreenshotNames: false,
|
|
14
18
|
disableScreenshots: false,
|
|
15
19
|
fullPageScreenshots: false,
|
|
16
|
-
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Creates screenshot on failure. Screenshot is saved into `output` directory.
|
|
@@ -42,118 +48,131 @@ const defaultConfig = {
|
|
|
42
48
|
*
|
|
43
49
|
*/
|
|
44
50
|
export default function (config) {
|
|
45
|
-
const helpers = Container.helpers()
|
|
46
|
-
let helper
|
|
51
|
+
const helpers = Container.helpers()
|
|
52
|
+
let helper
|
|
47
53
|
|
|
48
54
|
for (const helperName of supportedHelpers) {
|
|
49
55
|
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
50
|
-
helper = helpers[helperName]
|
|
56
|
+
helper = helpers[helperName]
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
if (!helper) return
|
|
60
|
+
if (!helper) return // no helpers for screenshot
|
|
55
61
|
|
|
56
|
-
const options = Object.assign(defaultConfig, helper.options, config)
|
|
62
|
+
const options = Object.assign(defaultConfig, helper.options, config)
|
|
57
63
|
|
|
58
64
|
if (helpers.Mochawesome) {
|
|
59
65
|
if (helpers.Mochawesome.config) {
|
|
60
|
-
options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames
|
|
66
|
+
options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
if (Codeceptjs.container.mocha) {
|
|
65
|
-
options.reportDir = Codeceptjs.container.mocha.options
|
|
66
|
-
&& Codeceptjs.container.mocha.options.reporterOptions.reportDir;
|
|
70
|
+
if (Codeceptjs.container.mocha()) {
|
|
71
|
+
options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions && Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
if (options.disableScreenshots) {
|
|
70
75
|
// old version of disabling screenshots
|
|
71
|
-
return
|
|
76
|
+
return
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
event.dispatcher.on(event.test.failed, (test) => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
79
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
80
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
|
|
81
|
+
// no browser here
|
|
77
82
|
return
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
recorder.add(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} else {
|
|
92
|
-
fileName += '.failed.png';
|
|
93
|
-
}
|
|
94
|
-
output.output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot');
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
if (options.reportDir) {
|
|
98
|
-
fileName = path.join(options.reportDir, fileName);
|
|
99
|
-
const mochaReportDir = path.resolve(process.cwd(), options.reportDir);
|
|
100
|
-
if (!fileExists(mochaReportDir)) {
|
|
101
|
-
fs.mkdirSync(mochaReportDir);
|
|
102
|
-
}
|
|
85
|
+
recorder.add(
|
|
86
|
+
'screenshot of failed test',
|
|
87
|
+
async () => {
|
|
88
|
+
const dataType = 'image/png'
|
|
89
|
+
// This prevents data driven to be included in the failed screenshot file name
|
|
90
|
+
let fileName
|
|
91
|
+
|
|
92
|
+
if (options.uniqueScreenshotNames && test) {
|
|
93
|
+
fileName = `${testToFileName(test, { suffix: '', unique: true })}.failed.png`
|
|
94
|
+
} else {
|
|
95
|
+
fileName = `${testToFileName(test, { suffix: '', unique: false })}.failed.png`
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
test.artifacts.screenshot = path.join(global.output_dir, fileName);
|
|
108
|
-
if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
|
|
109
|
-
test.attachments = [path.join(global.output_dir, fileName)];
|
|
97
|
+
const quietMode = !('output_dir' in global) || !global.output_dir
|
|
98
|
+
if (!quietMode) {
|
|
99
|
+
output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot')
|
|
110
100
|
}
|
|
111
101
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName);
|
|
120
|
-
allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType);
|
|
121
|
-
}
|
|
102
|
+
// Re-check helpers at runtime in case they weren't ready during plugin init
|
|
103
|
+
const runtimeHelpers = Container.helpers()
|
|
104
|
+
let runtimeHelper = null
|
|
105
|
+
for (const helperName of supportedHelpers) {
|
|
106
|
+
if (Object.keys(runtimeHelpers).indexOf(helperName) > -1) {
|
|
107
|
+
runtimeHelper = runtimeHelpers[helperName]
|
|
108
|
+
break
|
|
122
109
|
}
|
|
123
110
|
}
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
cucumberReporter.addScreenshot(test.artifacts.screenshot);
|
|
128
|
-
}
|
|
129
|
-
} catch (err) {
|
|
130
|
-
output.output.plugin(err);
|
|
131
|
-
if (
|
|
132
|
-
err
|
|
133
|
-
&& err.type
|
|
134
|
-
&& err.type === 'RuntimeError'
|
|
135
|
-
&& err.message
|
|
136
|
-
&& (
|
|
137
|
-
err.message.indexOf('was terminated due to') > -1
|
|
138
|
-
|| err.message.indexOf('no such window: target window already closed') > -1
|
|
139
|
-
)
|
|
140
|
-
) {
|
|
141
|
-
output.output.log(`Can't make screenshot, ${err}`);
|
|
142
|
-
helper.isRunning = false;
|
|
112
|
+
if (runtimeHelper && typeof runtimeHelper.saveScreenshot === 'function') {
|
|
113
|
+
helper = runtimeHelper
|
|
143
114
|
}
|
|
144
|
-
}
|
|
145
|
-
}, true);
|
|
146
|
-
});
|
|
147
115
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
116
|
+
try {
|
|
117
|
+
if (options.reportDir) {
|
|
118
|
+
fileName = path.join(options.reportDir, fileName)
|
|
119
|
+
const mochaReportDir = path.resolve(process.cwd(), options.reportDir)
|
|
120
|
+
if (!fileExists(mochaReportDir)) {
|
|
121
|
+
fs.mkdirSync(mochaReportDir)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
152
124
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
125
|
+
// Check if browser/page is still available before attempting screenshot
|
|
126
|
+
if (helper.page && helper.page.isClosed && helper.page.isClosed()) {
|
|
127
|
+
throw new Error('Browser page has been closed')
|
|
128
|
+
}
|
|
129
|
+
if (helper.browser && helper.browser.isConnected && !helper.browser.isConnected()) {
|
|
130
|
+
throw new Error('Browser has been disconnected')
|
|
131
|
+
}
|
|
156
132
|
|
|
157
|
-
|
|
158
|
-
|
|
133
|
+
// Add timeout wrapper to prevent hanging with shorter timeout for ESM
|
|
134
|
+
const screenshotPromise = helper.saveScreenshot(fileName, options.fullPageScreenshots)
|
|
135
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
136
|
+
setTimeout(() => reject(new Error('Screenshot timeout after 5 seconds')), 5000)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await Promise.race([screenshotPromise, timeoutPromise])
|
|
140
|
+
|
|
141
|
+
if (!test.artifacts) test.artifacts = {}
|
|
142
|
+
// Some unit tests may not define global.output_dir; avoid throwing when it is undefined
|
|
143
|
+
// Detect output directory safely (may not be initialized in narrow unit tests)
|
|
144
|
+
const baseOutputDir = 'output_dir' in global && typeof global.output_dir === 'string' && global.output_dir ? global.output_dir : null
|
|
145
|
+
if (baseOutputDir) {
|
|
146
|
+
test.artifacts.screenshot = path.join(baseOutputDir, fileName)
|
|
147
|
+
if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
|
|
148
|
+
test.attachments = [path.join(baseOutputDir, fileName)]
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Fallback: just store the file name to keep tests stable without triggering path errors
|
|
152
|
+
test.artifacts.screenshot = fileName
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (!quietMode) {
|
|
156
|
+
output.plugin('screenshotOnFail', `Failed to save screenshot: ${err.message}`)
|
|
157
|
+
}
|
|
158
|
+
// Enhanced error handling for browser closed scenarios
|
|
159
|
+
if (
|
|
160
|
+
err &&
|
|
161
|
+
((err.message &&
|
|
162
|
+
(err.message.includes('Target page, context or browser has been closed') ||
|
|
163
|
+
err.message.includes('Browser page has been closed') ||
|
|
164
|
+
err.message.includes('Browser has been disconnected') ||
|
|
165
|
+
err.message.includes('was terminated due to') ||
|
|
166
|
+
err.message.includes('no such window: target window already closed') ||
|
|
167
|
+
err.message.includes('Screenshot timeout after'))) ||
|
|
168
|
+
(err.type && err.type === 'RuntimeError'))
|
|
169
|
+
) {
|
|
170
|
+
output.log(`Can't make screenshot, ${err.message}`)
|
|
171
|
+
helper.isRunning = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
true,
|
|
176
|
+
)
|
|
177
|
+
})
|
|
159
178
|
}
|