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
package/lib/rerun.js
CHANGED
|
@@ -1,80 +1,125 @@
|
|
|
1
|
-
import fsPath from 'path'
|
|
2
|
-
import container from './container.js'
|
|
3
|
-
import
|
|
4
|
-
import BaseCodecept from './codecept.js'
|
|
5
|
-
import
|
|
1
|
+
import fsPath from 'path'
|
|
2
|
+
import container from './container.js'
|
|
3
|
+
import event from './event.js'
|
|
4
|
+
import BaseCodecept from './codecept.js'
|
|
5
|
+
import output from './output.js'
|
|
6
|
+
import { createRequire } from 'module'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
6
9
|
|
|
7
10
|
class CodeceptRerunner extends BaseCodecept {
|
|
8
|
-
runOnce(test) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test);
|
|
22
|
-
}
|
|
11
|
+
async runOnce(test) {
|
|
12
|
+
await container.started()
|
|
13
|
+
|
|
14
|
+
// Ensure translations are loaded for Gherkin features
|
|
15
|
+
try {
|
|
16
|
+
const { loadTranslations } = await import('./mocha/gherkin.js')
|
|
17
|
+
await loadTranslations()
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Ignore if gherkin module not available
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new Promise(async (resolve, reject) => {
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Create a fresh Mocha instance for each run
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
container.createMocha()
|
|
27
|
+
const mocha = container.mocha()
|
|
28
|
+
|
|
29
|
+
let filesToRun = this.testFiles
|
|
30
|
+
if (test) {
|
|
31
|
+
if (!fsPath.isAbsolute(test)) {
|
|
32
|
+
test = fsPath.join(global.codecept_dir, test)
|
|
33
|
+
}
|
|
34
|
+
filesToRun = this.testFiles.filter(t => fsPath.basename(t, '.js') === test || t === test)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Clear any existing tests/suites
|
|
38
|
+
mocha.suite.suites = []
|
|
39
|
+
mocha.suite.tests = []
|
|
40
|
+
|
|
41
|
+
// Manually load each test file by importing it
|
|
42
|
+
for (const file of filesToRun) {
|
|
43
|
+
try {
|
|
44
|
+
// Clear CommonJS cache if available (for mixed environments)
|
|
45
|
+
try {
|
|
46
|
+
delete require.cache[file]
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// ESM modules don't have require.cache, ignore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Force reload the module by using a cache-busting query parameter
|
|
52
|
+
const fileUrl = `${fsPath.resolve(file)}?t=${Date.now()}`
|
|
53
|
+
await import(fileUrl)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`Error loading test file ${file}:`, e)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const done = () => {
|
|
60
|
+
event.emit(event.all.result, container.result())
|
|
61
|
+
event.emit(event.all.after, this)
|
|
62
|
+
|
|
63
|
+
// Check if there were any failures
|
|
64
|
+
if (container.result().hasFailed) {
|
|
65
|
+
reject(new Error('Test run failed'))
|
|
27
66
|
} else {
|
|
28
|
-
|
|
67
|
+
resolve(undefined)
|
|
29
68
|
}
|
|
30
|
-
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
event.emit(event.all.before, this)
|
|
72
|
+
mocha.run(() => done())
|
|
31
73
|
} catch (e) {
|
|
32
|
-
|
|
74
|
+
output.error(e.stack)
|
|
75
|
+
reject(e)
|
|
33
76
|
}
|
|
34
|
-
})
|
|
77
|
+
})
|
|
35
78
|
}
|
|
36
79
|
|
|
37
80
|
async runTests(test) {
|
|
38
|
-
const configRerun = this.config.rerun || {}
|
|
39
|
-
const minSuccess = configRerun.minSuccess || 1
|
|
40
|
-
const maxReruns = configRerun.maxReruns || 1
|
|
81
|
+
const configRerun = this.config.rerun || {}
|
|
82
|
+
const minSuccess = configRerun.minSuccess || 1
|
|
83
|
+
const maxReruns = configRerun.maxReruns || 1
|
|
41
84
|
if (minSuccess > maxReruns) {
|
|
42
|
-
process.exitCode = 1
|
|
43
|
-
throw new Error(`run-rerun Configuration Error: minSuccess must be less than maxReruns. Current values: minSuccess=${minSuccess} maxReruns=${maxReruns}`)
|
|
85
|
+
process.exitCode = 1
|
|
86
|
+
throw new Error(`run-rerun Configuration Error: minSuccess must be less than maxReruns. Current values: minSuccess=${minSuccess} maxReruns=${maxReruns}`)
|
|
44
87
|
}
|
|
45
88
|
if (maxReruns === 1) {
|
|
46
|
-
await this.runOnce(test)
|
|
47
|
-
return
|
|
89
|
+
await this.runOnce(test)
|
|
90
|
+
return
|
|
48
91
|
}
|
|
49
|
-
let successCounter = 0
|
|
50
|
-
let rerunsCounter = 0
|
|
92
|
+
let successCounter = 0
|
|
93
|
+
let rerunsCounter = 0
|
|
51
94
|
while (rerunsCounter < maxReruns && successCounter < minSuccess) {
|
|
52
|
-
|
|
95
|
+
container.result().reset() // reset result
|
|
96
|
+
rerunsCounter++
|
|
53
97
|
try {
|
|
54
|
-
await this.runOnce(test)
|
|
55
|
-
successCounter
|
|
56
|
-
output.
|
|
98
|
+
await this.runOnce(test)
|
|
99
|
+
successCounter++
|
|
100
|
+
output.success(`\nProcess run ${rerunsCounter} of max ${maxReruns}, success runs ${successCounter}/${minSuccess}\n`)
|
|
57
101
|
} catch (e) {
|
|
58
|
-
output.
|
|
59
|
-
console.error(e)
|
|
102
|
+
output.error(`\nFail run ${rerunsCounter} of max ${maxReruns}, success runs ${successCounter}/${minSuccess} \n`)
|
|
103
|
+
console.error(e)
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
106
|
if (successCounter < minSuccess) {
|
|
63
|
-
throw new Error(`Flaky tests detected! ${successCounter} success runs achieved instead of ${minSuccess} success runs expected`)
|
|
107
|
+
throw new Error(`Flaky tests detected! ${successCounter} success runs achieved instead of ${minSuccess} success runs expected`)
|
|
64
108
|
}
|
|
65
109
|
}
|
|
66
110
|
|
|
67
111
|
async run(test) {
|
|
68
|
-
event.emit(event.all.before, this)
|
|
112
|
+
event.emit(event.all.before, this)
|
|
69
113
|
try {
|
|
70
|
-
await this.runTests(test)
|
|
114
|
+
await this.runTests(test)
|
|
71
115
|
} catch (e) {
|
|
72
|
-
output.
|
|
116
|
+
output.error(e.stack)
|
|
117
|
+
throw e
|
|
73
118
|
} finally {
|
|
74
|
-
event.emit(event.all.result, this)
|
|
75
|
-
event.emit(event.all.after, this)
|
|
119
|
+
event.emit(event.all.result, this)
|
|
120
|
+
event.emit(event.all.after, this)
|
|
76
121
|
}
|
|
77
122
|
}
|
|
78
123
|
}
|
|
79
124
|
|
|
80
|
-
export default CodeceptRerunner
|
|
125
|
+
export default CodeceptRerunner
|
package/lib/result.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { serializeTest } from './mocha/test.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} Stats Statistics for a test result.
|
|
7
|
+
* @property {number} passes Number of passed tests.
|
|
8
|
+
* @property {number} failures Number of failed tests.
|
|
9
|
+
* @property {number} tests Total number of tests.
|
|
10
|
+
* @property {number} pending Number of pending tests.
|
|
11
|
+
* @property {number} failedHooks Number of failed hooks.
|
|
12
|
+
* @property {Date} start Start time of the test run.
|
|
13
|
+
* @property {Date} end End time of the test run.
|
|
14
|
+
* @property {number} duration Duration of the test run, in milliseconds.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result of a test run. Will be emitted for example in "event.all.result" events.
|
|
19
|
+
*/
|
|
20
|
+
class Result {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._startTime = new Date()
|
|
23
|
+
this._endTime = null
|
|
24
|
+
|
|
25
|
+
this.reset()
|
|
26
|
+
this.start()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resets all collected stats, tests, and failure reports.
|
|
31
|
+
*/
|
|
32
|
+
reset() {
|
|
33
|
+
this._stats = {
|
|
34
|
+
passes: 0,
|
|
35
|
+
failures: 0,
|
|
36
|
+
tests: 0,
|
|
37
|
+
pending: 0,
|
|
38
|
+
failedHooks: 0,
|
|
39
|
+
start: null,
|
|
40
|
+
end: null,
|
|
41
|
+
duration: 0,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @type {CodeceptJS.Test[]}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
this._tests = []
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @type {string[][]}
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
this._failures = []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the start time to the current time.
|
|
59
|
+
*/
|
|
60
|
+
start() {
|
|
61
|
+
this._startTime = new Date()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sets the end time to the current time.
|
|
66
|
+
*/
|
|
67
|
+
finish() {
|
|
68
|
+
this._endTime = new Date()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether this result contains any failed tests.
|
|
73
|
+
*
|
|
74
|
+
* @type {boolean}
|
|
75
|
+
* @readonly
|
|
76
|
+
*/
|
|
77
|
+
get hasFailed() {
|
|
78
|
+
return this._stats.failures > 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* All collected tests.
|
|
83
|
+
*
|
|
84
|
+
* @type {CodeceptJS.Test[]}
|
|
85
|
+
* @readonly
|
|
86
|
+
*/
|
|
87
|
+
get tests() {
|
|
88
|
+
return this._tests
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The failure reports (array of strings per failed test).
|
|
93
|
+
*
|
|
94
|
+
* @type {string[][]}
|
|
95
|
+
* @readonly
|
|
96
|
+
*/
|
|
97
|
+
get failures() {
|
|
98
|
+
return this._failures.filter(f => f && (!Array.isArray(f) || f.length > 0))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The test statistics.
|
|
103
|
+
*
|
|
104
|
+
* @type {Stats}
|
|
105
|
+
* @readonly
|
|
106
|
+
*/
|
|
107
|
+
get stats() {
|
|
108
|
+
return this._stats
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The start time of the test run.
|
|
113
|
+
*
|
|
114
|
+
* @type {Date}
|
|
115
|
+
* @readonly
|
|
116
|
+
*/
|
|
117
|
+
get startTime() {
|
|
118
|
+
return this._startTime
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adds a test to this result.
|
|
123
|
+
*
|
|
124
|
+
* @param {CodeceptJS.Test} test
|
|
125
|
+
*/
|
|
126
|
+
addTest(test) {
|
|
127
|
+
const existingTestIndex = this._tests.findIndex(t => !!t.uid && t.uid === test.uid)
|
|
128
|
+
if (existingTestIndex >= 0) {
|
|
129
|
+
this._tests[existingTestIndex] = test
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this._tests.push(test)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Adds failure reports to this result.
|
|
138
|
+
*
|
|
139
|
+
* @param {string[][]} newFailures
|
|
140
|
+
*/
|
|
141
|
+
addFailures(newFailures) {
|
|
142
|
+
this._failures.push(...newFailures)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether this result contains any failed tests.
|
|
147
|
+
*
|
|
148
|
+
* @type {boolean}
|
|
149
|
+
* @readonly
|
|
150
|
+
*/
|
|
151
|
+
get hasFailures() {
|
|
152
|
+
return this.stats.failures > 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The duration of the test run, in milliseconds.
|
|
157
|
+
*
|
|
158
|
+
* @type {number}
|
|
159
|
+
* @readonly
|
|
160
|
+
*/
|
|
161
|
+
get duration() {
|
|
162
|
+
return this._endTime ? +this._endTime - +this._startTime : 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* All failed tests.
|
|
167
|
+
*
|
|
168
|
+
* @type {CodeceptJS.Test[]}
|
|
169
|
+
* readonly
|
|
170
|
+
*/
|
|
171
|
+
get failedTests() {
|
|
172
|
+
return this._tests.filter(test => test.state === 'failed')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* All passed tests.
|
|
177
|
+
*
|
|
178
|
+
* @type {CodeceptJS.Test[]}
|
|
179
|
+
* @readonly
|
|
180
|
+
*/
|
|
181
|
+
get passedTests() {
|
|
182
|
+
return this._tests.filter(test => test.state === 'passed')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* All skipped tests.
|
|
187
|
+
*
|
|
188
|
+
* @type {CodeceptJS.Test[]}
|
|
189
|
+
* @readonly
|
|
190
|
+
*/
|
|
191
|
+
get skippedTests() {
|
|
192
|
+
return this._tests.filter(test => test.state === 'skipped' || test.state === 'pending')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @returns {object} The JSON representation of this result.
|
|
197
|
+
*/
|
|
198
|
+
simplify() {
|
|
199
|
+
return {
|
|
200
|
+
hasFailed: this.hasFailed,
|
|
201
|
+
stats: this.stats,
|
|
202
|
+
duration: this.duration,
|
|
203
|
+
tests: this._tests.map(test => serializeTest(test)),
|
|
204
|
+
failures: this._failures,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Saves this result to a JSON file.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} [fileName] Path to the JSON file, relative to `output_dir`. Defaults to "result.json".
|
|
212
|
+
*/
|
|
213
|
+
save(fileName) {
|
|
214
|
+
if (!fileName) fileName = 'result.json'
|
|
215
|
+
fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(this.simplify(), null, 2))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Adds stats to this result.
|
|
220
|
+
*
|
|
221
|
+
* @param {Partial<Stats>} [newStats]
|
|
222
|
+
*/
|
|
223
|
+
addStats(newStats = {}) {
|
|
224
|
+
this._stats.passes += newStats.passes || 0
|
|
225
|
+
this._stats.failures += newStats.failures || 0
|
|
226
|
+
this._stats.tests += newStats.tests || 0
|
|
227
|
+
this._stats.pending += newStats.pending || 0
|
|
228
|
+
this._stats.failedHooks += newStats.failedHooks || 0
|
|
229
|
+
|
|
230
|
+
// do not override start time
|
|
231
|
+
this._stats.start = this._stats.start || newStats.start
|
|
232
|
+
|
|
233
|
+
this._stats.end = newStats.end || this._stats.end
|
|
234
|
+
this._stats.duration = newStats.duration
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default Result
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import output from './output.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry Coordinator - Central coordination for all retry mechanisms
|
|
5
|
+
*
|
|
6
|
+
* This module provides:
|
|
7
|
+
* 1. Priority-based retry coordination
|
|
8
|
+
* 2. Unified configuration validation
|
|
9
|
+
* 3. Consolidated retry reporting
|
|
10
|
+
* 4. Conflict detection and resolution
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Priority levels for retry mechanisms (higher number = higher priority)
|
|
15
|
+
*/
|
|
16
|
+
const RETRY_PRIORITIES = {
|
|
17
|
+
MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
|
|
18
|
+
STEP_PLUGIN: 50, // retryFailedStep plugin
|
|
19
|
+
SCENARIO_CONFIG: 30, // Global scenario retry config
|
|
20
|
+
FEATURE_CONFIG: 20, // Global feature retry config
|
|
21
|
+
HOOK_CONFIG: 10, // Hook retry config - lowest priority
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retry mechanism types
|
|
26
|
+
*/
|
|
27
|
+
const RETRY_TYPES = {
|
|
28
|
+
MANUAL_STEP: 'manual-step',
|
|
29
|
+
STEP_PLUGIN: 'step-plugin',
|
|
30
|
+
SCENARIO: 'scenario',
|
|
31
|
+
FEATURE: 'feature',
|
|
32
|
+
HOOK: 'hook',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Global retry coordination state
|
|
37
|
+
*/
|
|
38
|
+
let retryState = {
|
|
39
|
+
activeTest: null,
|
|
40
|
+
activeSuite: null,
|
|
41
|
+
retryHistory: [],
|
|
42
|
+
conflicts: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a retry mechanism for coordination
|
|
47
|
+
* @param {string} type - Type of retry mechanism
|
|
48
|
+
* @param {Object} config - Retry configuration
|
|
49
|
+
* @param {Object} target - Target object (test, suite, etc.)
|
|
50
|
+
* @param {number} priority - Priority level
|
|
51
|
+
*/
|
|
52
|
+
function registerRetry(type, config, target, priority = 0) {
|
|
53
|
+
const retryInfo = {
|
|
54
|
+
type,
|
|
55
|
+
config,
|
|
56
|
+
target,
|
|
57
|
+
priority,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect conflicts
|
|
62
|
+
const existingRetries = retryState.retryHistory.filter(r => r.target === target && r.type !== type && r.priority !== priority)
|
|
63
|
+
|
|
64
|
+
if (existingRetries.length > 0) {
|
|
65
|
+
const conflict = {
|
|
66
|
+
newRetry: retryInfo,
|
|
67
|
+
existingRetries: existingRetries,
|
|
68
|
+
resolved: false,
|
|
69
|
+
}
|
|
70
|
+
retryState.conflicts.push(conflict)
|
|
71
|
+
handleRetryConflict(conflict)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
retryState.retryHistory.push(retryInfo)
|
|
75
|
+
|
|
76
|
+
output.log(`[Retry Coordinator] Registered ${type} retry (priority: ${priority})`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handles conflicts between retry mechanisms
|
|
81
|
+
* @param {Object} conflict - Conflict information
|
|
82
|
+
*/
|
|
83
|
+
function handleRetryConflict(conflict) {
|
|
84
|
+
const { newRetry, existingRetries } = conflict
|
|
85
|
+
|
|
86
|
+
// Find highest priority retry
|
|
87
|
+
const allRetries = [newRetry, ...existingRetries]
|
|
88
|
+
const highestPriority = Math.max(...allRetries.map(r => r.priority))
|
|
89
|
+
const winningRetry = allRetries.find(r => r.priority === highestPriority)
|
|
90
|
+
|
|
91
|
+
// Log the conflict resolution
|
|
92
|
+
output.log(`[Retry Coordinator] Conflict detected:`)
|
|
93
|
+
allRetries.forEach(retry => {
|
|
94
|
+
const status = retry === winningRetry ? 'ACTIVE' : 'DEFERRED'
|
|
95
|
+
output.log(` - ${retry.type} (priority: ${retry.priority}) [${status}]`)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
conflict.resolved = true
|
|
99
|
+
conflict.winner = winningRetry
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gets the effective retry configuration for a target
|
|
104
|
+
* @param {Object} target - Target object (test, suite, etc.)
|
|
105
|
+
* @returns {Object} Effective retry configuration
|
|
106
|
+
*/
|
|
107
|
+
function getEffectiveRetryConfig(target) {
|
|
108
|
+
const targetRetries = retryState.retryHistory.filter(r => r.target === target)
|
|
109
|
+
|
|
110
|
+
if (targetRetries.length === 0) {
|
|
111
|
+
return { type: 'none', retries: 0 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find highest priority retry
|
|
115
|
+
const highestPriority = Math.max(...targetRetries.map(r => r.priority))
|
|
116
|
+
const effectiveRetry = targetRetries.find(r => r.priority === highestPriority)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
type: effectiveRetry.type,
|
|
120
|
+
retries: effectiveRetry.config.retries || effectiveRetry.config,
|
|
121
|
+
config: effectiveRetry.config,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generates a retry summary report
|
|
127
|
+
* @returns {Object} Retry summary
|
|
128
|
+
*/
|
|
129
|
+
function generateRetrySummary() {
|
|
130
|
+
const summary = {
|
|
131
|
+
totalRetryMechanisms: retryState.retryHistory.length,
|
|
132
|
+
conflicts: retryState.conflicts.length,
|
|
133
|
+
byType: {},
|
|
134
|
+
recommendations: [],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Count by type
|
|
138
|
+
retryState.retryHistory.forEach(retry => {
|
|
139
|
+
summary.byType[retry.type] = (summary.byType[retry.type] || 0) + 1
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Generate recommendations
|
|
143
|
+
if (summary.conflicts > 0) {
|
|
144
|
+
summary.recommendations.push('Consider consolidating retry configurations to avoid conflicts')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (summary.byType[RETRY_TYPES.STEP_PLUGIN] && summary.byType[RETRY_TYPES.SCENARIO]) {
|
|
148
|
+
summary.recommendations.push('Step-level and scenario-level retries are both active - consider using only one approach')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return summary
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resets the retry coordination state (useful for testing)
|
|
156
|
+
*/
|
|
157
|
+
function reset() {
|
|
158
|
+
retryState = {
|
|
159
|
+
activeTest: null,
|
|
160
|
+
activeSuite: null,
|
|
161
|
+
retryHistory: [],
|
|
162
|
+
conflicts: [],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validates retry configuration for common issues
|
|
168
|
+
* @param {Object} config - Configuration object
|
|
169
|
+
* @returns {Array} Array of validation warnings
|
|
170
|
+
*/
|
|
171
|
+
function validateConfig(config) {
|
|
172
|
+
const warnings = []
|
|
173
|
+
|
|
174
|
+
if (!config) return warnings
|
|
175
|
+
|
|
176
|
+
// Check for potential configuration conflicts
|
|
177
|
+
if (config.retry && config.plugins && config.plugins.retryFailedStep) {
|
|
178
|
+
const globalRetries = typeof config.retry === 'number' ? config.retry : config.retry.Scenario || config.retry.Feature
|
|
179
|
+
const stepRetries = config.plugins.retryFailedStep.retries || 3
|
|
180
|
+
|
|
181
|
+
if (globalRetries && stepRetries) {
|
|
182
|
+
warnings.push(`Both global retries (${globalRetries}) and step retries (${stepRetries}) are configured - total executions could be ${globalRetries * (stepRetries + 1)}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for excessive retry counts
|
|
187
|
+
if (config.retry) {
|
|
188
|
+
const retryValues = typeof config.retry === 'number' ? [config.retry] : Object.values(config.retry)
|
|
189
|
+
const maxRetries = Math.max(...retryValues.filter(v => typeof v === 'number'))
|
|
190
|
+
|
|
191
|
+
if (maxRetries > 5) {
|
|
192
|
+
warnings.push(`High retry count detected (${maxRetries}) - consider investigating test stability instead`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return warnings
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
RETRY_PRIORITIES,
|
|
201
|
+
RETRY_TYPES,
|
|
202
|
+
registerRetry,
|
|
203
|
+
getEffectiveRetryConfig,
|
|
204
|
+
generateRetrySummary,
|
|
205
|
+
validateConfig,
|
|
206
|
+
reset,
|
|
207
|
+
}
|
package/lib/secret.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
import { deepClone } from './utils.js';
|
|
1
|
+
import { deepClone } from './utils.js'
|
|
3
2
|
|
|
4
|
-
const maskedString = '*****'
|
|
3
|
+
const maskedString = '*****'
|
|
5
4
|
|
|
6
5
|
/** @param {string} secret */
|
|
7
|
-
|
|
6
|
+
class Secret {
|
|
8
7
|
constructor(secret) {
|
|
9
|
-
this._secret = secret
|
|
8
|
+
this._secret = secret
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
/** @returns {string} */
|
|
13
12
|
toString() {
|
|
14
|
-
return this._secret
|
|
13
|
+
return this._secret
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
getMasked() {
|
|
18
|
-
return maskedString
|
|
17
|
+
return maskedString
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/**
|
|
@@ -24,11 +23,11 @@ export default class Secret {
|
|
|
24
23
|
*/
|
|
25
24
|
static secret(secret) {
|
|
26
25
|
if (typeof secret === 'object') {
|
|
27
|
-
const fields = Array.from(arguments)
|
|
28
|
-
fields.shift()
|
|
29
|
-
return secretObject(secret, fields)
|
|
26
|
+
const fields = Array.from(arguments)
|
|
27
|
+
fields.shift()
|
|
28
|
+
return secretObject(secret, fields)
|
|
30
29
|
}
|
|
31
|
-
return new Secret(secret)
|
|
30
|
+
return new Secret(secret)
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -37,14 +36,17 @@ function secretObject(obj, fieldsToHide = []) {
|
|
|
37
36
|
get(obj, prop) {
|
|
38
37
|
if (prop === 'toString') {
|
|
39
38
|
return function () {
|
|
40
|
-
const maskedObject = deepClone(obj)
|
|
41
|
-
fieldsToHide.forEach(f => maskedObject[f] = maskedString)
|
|
42
|
-
return JSON.stringify(maskedObject)
|
|
43
|
-
}
|
|
39
|
+
const maskedObject = deepClone(obj)
|
|
40
|
+
fieldsToHide.forEach(f => (maskedObject[f] = maskedString))
|
|
41
|
+
return JSON.stringify(maskedObject)
|
|
42
|
+
}
|
|
44
43
|
}
|
|
45
|
-
return fieldsToHide.includes(prop) ? new Secret(obj[prop]) : obj[prop]
|
|
44
|
+
return fieldsToHide.includes(prop) ? new Secret(obj[prop]) : obj[prop]
|
|
46
45
|
},
|
|
47
|
-
}
|
|
46
|
+
}
|
|
48
47
|
|
|
49
|
-
return new Proxy(obj, handler)
|
|
48
|
+
return new Proxy(obj, handler)
|
|
50
49
|
}
|
|
50
|
+
|
|
51
|
+
export default Secret
|
|
52
|
+
export const secret = Secret.secret
|