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/workers.js
CHANGED
|
@@ -1,263 +1,326 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
} from './utils.js'
|
|
16
|
-
import mainConfig from './config.js'
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
codecept
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { dirname } from 'path'
|
|
4
|
+
import { mkdirp } from 'mkdirp'
|
|
5
|
+
import { Worker } from 'worker_threads'
|
|
6
|
+
import { EventEmitter } from 'events'
|
|
7
|
+
import ms from 'ms'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = dirname(__filename)
|
|
11
|
+
import Codecept from './codecept.js'
|
|
12
|
+
import MochaFactory from './mocha/factory.js'
|
|
13
|
+
import Container from './container.js'
|
|
14
|
+
import { getTestRoot } from './command/utils.js'
|
|
15
|
+
import { isFunction, fileExists, replaceValueDeep, deepClone } from './utils.js'
|
|
16
|
+
import mainConfig from './config.js'
|
|
17
|
+
import output from './output.js'
|
|
18
|
+
import event from './event.js'
|
|
19
|
+
import { deserializeTest } from './mocha/test.js'
|
|
20
|
+
import { deserializeSuite } from './mocha/suite.js'
|
|
21
|
+
import recorder from './recorder.js'
|
|
22
|
+
import runHook from './hooks.js'
|
|
23
|
+
import WorkerStorage from './workerStorage.js'
|
|
24
|
+
import { createRuns } from './command/run-multiple/collection.js'
|
|
25
|
+
|
|
26
|
+
const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
|
|
27
|
+
|
|
28
|
+
const initializeCodecept = async (configPath, options = {}) => {
|
|
29
|
+
const config = await mainConfig.load(configPath || '.')
|
|
30
|
+
const codecept = new Codecept(config, options)
|
|
31
|
+
await codecept.init(getTestRoot(configPath))
|
|
32
|
+
codecept.loadTests()
|
|
33
|
+
|
|
34
|
+
return codecept
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const createOutputDir = async configPath => {
|
|
38
|
+
const config = await mainConfig.load(configPath || '.')
|
|
39
|
+
const testRoot = getTestRoot(configPath)
|
|
40
|
+
const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output)
|
|
39
41
|
|
|
40
42
|
if (!fileExists(outputDir)) {
|
|
41
|
-
output.print(`creating output directory: ${outputDir}`)
|
|
42
|
-
mkdirp.sync(outputDir)
|
|
43
|
+
output.print(`creating output directory: ${outputDir}`)
|
|
44
|
+
mkdirp.sync(outputDir)
|
|
43
45
|
}
|
|
44
|
-
}
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
const groups = []
|
|
48
|
+
const populateGroups = numberOfWorkers => {
|
|
49
|
+
const groups = []
|
|
48
50
|
for (let i = 0; i < numberOfWorkers; i++) {
|
|
49
|
-
groups[i] = []
|
|
51
|
+
groups[i] = []
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
return groups
|
|
54
|
+
return groups
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
const createWorker = (workerObject) => {
|
|
57
|
+
const createWorker = (workerObject, isPoolMode = false) => {
|
|
56
58
|
const worker = new Worker(pathToWorker, {
|
|
57
59
|
workerData: {
|
|
58
60
|
options: simplifyObject(workerObject.options),
|
|
59
61
|
tests: workerObject.tests,
|
|
60
62
|
testRoot: workerObject.testRoot,
|
|
61
63
|
workerIndex: workerObject.workerIndex + 1,
|
|
64
|
+
poolMode: isPoolMode,
|
|
62
65
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
stdout: true,
|
|
67
|
+
stderr: true,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Pipe worker stdout/stderr to main process
|
|
71
|
+
if (worker.stdout) {
|
|
72
|
+
worker.stdout.setEncoding('utf8')
|
|
73
|
+
worker.stdout.on('data', (data) => {
|
|
74
|
+
process.stdout.write(data)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
if (worker.stderr) {
|
|
78
|
+
worker.stderr.setEncoding('utf8')
|
|
79
|
+
worker.stderr.on('data', (data) => {
|
|
80
|
+
process.stderr.write(data)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
worker.on('error', err => {
|
|
85
|
+
console.error(`[Main] Worker Error:`, err)
|
|
86
|
+
output.error(`Worker Error: ${err.stack}`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
WorkerStorage.addWorker(worker)
|
|
90
|
+
return worker
|
|
91
|
+
}
|
|
70
92
|
|
|
71
|
-
const simplifyObject =
|
|
93
|
+
const simplifyObject = object => {
|
|
72
94
|
return Object.keys(object)
|
|
73
|
-
.filter(
|
|
74
|
-
.filter(
|
|
75
|
-
.filter(
|
|
95
|
+
.filter(k => k.indexOf('_') !== 0)
|
|
96
|
+
.filter(k => typeof object[k] !== 'function')
|
|
97
|
+
.filter(k => typeof object[k] !== 'object')
|
|
76
98
|
.reduce((obj, key) => {
|
|
77
|
-
obj[key] = object[key]
|
|
78
|
-
return obj
|
|
79
|
-
}, {})
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const repackTest = (test) => {
|
|
83
|
-
test = Object.assign(new Test(test.title || '', () => { }), test);
|
|
84
|
-
test.parent = Object.assign(new Suite(test.parent.title), test.parent);
|
|
85
|
-
return test;
|
|
86
|
-
};
|
|
99
|
+
obj[key] = object[key]
|
|
100
|
+
return obj
|
|
101
|
+
}, {})
|
|
102
|
+
}
|
|
87
103
|
|
|
88
104
|
const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns) => {
|
|
89
|
-
selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns
|
|
105
|
+
selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns
|
|
90
106
|
if (selectedRuns === undefined || !selectedRuns.length || config.multiple === undefined) {
|
|
91
107
|
return testGroups.map((tests, index) => {
|
|
92
|
-
const workerObj = new WorkerObject(index)
|
|
93
|
-
workerObj.addConfig(config)
|
|
94
|
-
workerObj.addTests(tests)
|
|
95
|
-
workerObj.setTestRoot(testRoot)
|
|
96
|
-
workerObj.addOptions(options)
|
|
97
|
-
return workerObj
|
|
98
|
-
})
|
|
108
|
+
const workerObj = new WorkerObject(index)
|
|
109
|
+
workerObj.addConfig(config)
|
|
110
|
+
workerObj.addTests(tests)
|
|
111
|
+
workerObj.setTestRoot(testRoot)
|
|
112
|
+
workerObj.addOptions(options)
|
|
113
|
+
return workerObj
|
|
114
|
+
})
|
|
99
115
|
}
|
|
100
|
-
const workersToExecute = []
|
|
116
|
+
const workersToExecute = []
|
|
101
117
|
|
|
102
|
-
const currentOutputFolder = config.output
|
|
103
|
-
let currentMochawesomeReportDir
|
|
104
|
-
let currentMochaJunitReporterFile
|
|
118
|
+
const currentOutputFolder = config.output
|
|
119
|
+
let currentMochawesomeReportDir
|
|
120
|
+
let currentMochaJunitReporterFile
|
|
105
121
|
|
|
106
122
|
if (config.mocha && config.mocha.reporterOptions) {
|
|
107
|
-
currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir
|
|
108
|
-
currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile
|
|
123
|
+
currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir
|
|
124
|
+
currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
const separator = path.sep
|
|
113
|
-
const _config = { ...config }
|
|
114
|
-
let workerName = worker.name.replace(':', '_')
|
|
115
|
-
_config.output = `${currentOutputFolder}${separator}${workerName}
|
|
127
|
+
createRuns(selectedRuns, config).forEach(worker => {
|
|
128
|
+
const separator = path.sep
|
|
129
|
+
const _config = { ...config }
|
|
130
|
+
let workerName = worker.name.replace(':', '_')
|
|
131
|
+
_config.output = `${currentOutputFolder}${separator}${workerName}`
|
|
116
132
|
if (config.mocha && config.mocha.reporterOptions) {
|
|
117
|
-
_config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}
|
|
118
|
-
|
|
119
|
-
const _tempArray = currentMochaJunitReporterFile.split(separator)
|
|
120
|
-
_tempArray.splice(
|
|
121
|
-
|
|
133
|
+
_config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`
|
|
134
|
+
|
|
135
|
+
const _tempArray = currentMochaJunitReporterFile.split(separator)
|
|
136
|
+
_tempArray.splice(
|
|
137
|
+
_tempArray.findIndex(item => item.includes('.xml')),
|
|
138
|
+
0,
|
|
139
|
+
workerName,
|
|
140
|
+
)
|
|
141
|
+
_config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator)
|
|
122
142
|
}
|
|
123
|
-
workerName = worker.getOriginalName() || worker.getName()
|
|
124
|
-
const workerConfig = worker.getConfig()
|
|
125
|
-
workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config))
|
|
126
|
-
})
|
|
127
|
-
const workers = []
|
|
128
|
-
let index = 0
|
|
129
|
-
testGroups.forEach(
|
|
130
|
-
const testWorkerArray = []
|
|
131
|
-
workersToExecute.forEach(
|
|
132
|
-
const workerObj = new WorkerObject(index++)
|
|
133
|
-
workerObj.addConfig(finalConfig)
|
|
134
|
-
workerObj.addTests(tests)
|
|
135
|
-
workerObj.setTestRoot(testRoot)
|
|
136
|
-
workerObj.addOptions(options)
|
|
137
|
-
testWorkerArray.push(workerObj)
|
|
138
|
-
})
|
|
139
|
-
workers.push(...testWorkerArray)
|
|
140
|
-
})
|
|
141
|
-
return workers
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const indexOfSmallestElement =
|
|
145
|
-
let i = 0
|
|
143
|
+
workerName = worker.getOriginalName() || worker.getName()
|
|
144
|
+
const workerConfig = worker.getConfig()
|
|
145
|
+
workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config))
|
|
146
|
+
})
|
|
147
|
+
const workers = []
|
|
148
|
+
let index = 0
|
|
149
|
+
testGroups.forEach(tests => {
|
|
150
|
+
const testWorkerArray = []
|
|
151
|
+
workersToExecute.forEach(finalConfig => {
|
|
152
|
+
const workerObj = new WorkerObject(index++)
|
|
153
|
+
workerObj.addConfig(finalConfig)
|
|
154
|
+
workerObj.addTests(tests)
|
|
155
|
+
workerObj.setTestRoot(testRoot)
|
|
156
|
+
workerObj.addOptions(options)
|
|
157
|
+
testWorkerArray.push(workerObj)
|
|
158
|
+
})
|
|
159
|
+
workers.push(...testWorkerArray)
|
|
160
|
+
})
|
|
161
|
+
return workers
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const indexOfSmallestElement = groups => {
|
|
165
|
+
let i = 0
|
|
146
166
|
for (let j = 1; j < groups.length; j++) {
|
|
147
167
|
if (groups[j - 1].length > groups[j].length) {
|
|
148
|
-
i = j
|
|
168
|
+
i = j
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
|
-
return i
|
|
152
|
-
}
|
|
171
|
+
return i
|
|
172
|
+
}
|
|
153
173
|
|
|
154
|
-
const convertToMochaTests =
|
|
155
|
-
const group = []
|
|
174
|
+
const convertToMochaTests = testGroup => {
|
|
175
|
+
const group = []
|
|
156
176
|
if (testGroup instanceof Array) {
|
|
157
|
-
const mocha = MochaFactory.create({}, {})
|
|
158
|
-
mocha.files = testGroup
|
|
159
|
-
mocha.loadFiles()
|
|
160
|
-
mocha.suite.eachTest(
|
|
161
|
-
group.push(test.uid)
|
|
162
|
-
})
|
|
163
|
-
mocha.unloadFiles()
|
|
177
|
+
const mocha = MochaFactory.create({}, {})
|
|
178
|
+
mocha.files = testGroup
|
|
179
|
+
mocha.loadFiles()
|
|
180
|
+
mocha.suite.eachTest(test => {
|
|
181
|
+
group.push(test.uid)
|
|
182
|
+
})
|
|
183
|
+
mocha.unloadFiles()
|
|
164
184
|
}
|
|
165
185
|
|
|
166
|
-
return group
|
|
167
|
-
}
|
|
186
|
+
return group
|
|
187
|
+
}
|
|
168
188
|
|
|
169
189
|
const getOverridenConfig = (workerName, workerConfig, config) => {
|
|
170
190
|
// clone config
|
|
171
|
-
const overriddenConfig = deepClone(config)
|
|
191
|
+
const overriddenConfig = deepClone(config)
|
|
172
192
|
|
|
173
193
|
// get configuration
|
|
174
|
-
const browserConfig = workerConfig.browser
|
|
194
|
+
const browserConfig = workerConfig.browser
|
|
175
195
|
|
|
176
196
|
for (const key in browserConfig) {
|
|
177
|
-
overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key])
|
|
197
|
+
overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key])
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
// override tests configuration
|
|
181
201
|
if (overriddenConfig.tests) {
|
|
182
|
-
overriddenConfig.tests = workerConfig.tests
|
|
202
|
+
overriddenConfig.tests = workerConfig.tests
|
|
183
203
|
}
|
|
184
204
|
|
|
185
205
|
if (overriddenConfig.gherkin && workerConfig.gherkin && workerConfig.gherkin.features) {
|
|
186
|
-
overriddenConfig.gherkin.features = workerConfig.gherkin.features
|
|
206
|
+
overriddenConfig.gherkin.features = workerConfig.gherkin.features
|
|
187
207
|
}
|
|
188
|
-
return overriddenConfig
|
|
189
|
-
}
|
|
208
|
+
return overriddenConfig
|
|
209
|
+
}
|
|
190
210
|
|
|
191
211
|
class WorkerObject {
|
|
192
212
|
/**
|
|
193
213
|
* @param {Number} workerIndex - Unique ID for worker
|
|
194
214
|
*/
|
|
195
215
|
constructor(workerIndex) {
|
|
196
|
-
this.workerIndex = workerIndex
|
|
197
|
-
this.options = {}
|
|
198
|
-
this.tests = []
|
|
199
|
-
this.testRoot = getTestRoot()
|
|
216
|
+
this.workerIndex = workerIndex
|
|
217
|
+
this.options = {}
|
|
218
|
+
this.tests = []
|
|
219
|
+
this.testRoot = getTestRoot()
|
|
200
220
|
}
|
|
201
221
|
|
|
202
222
|
addConfig(config) {
|
|
203
|
-
const oldConfig = JSON.parse(this.options.override || '{}')
|
|
223
|
+
const oldConfig = JSON.parse(this.options.override || '{}')
|
|
224
|
+
|
|
225
|
+
// Remove customLocatorStrategies from both old and new config before JSON serialization
|
|
226
|
+
// since functions cannot be serialized and will be lost, causing workers to have empty strategies
|
|
227
|
+
const configWithoutFunctions = { ...config }
|
|
228
|
+
|
|
229
|
+
// Clean both old and new config
|
|
230
|
+
const cleanConfig = (cfg) => {
|
|
231
|
+
if (cfg.helpers) {
|
|
232
|
+
cfg.helpers = { ...cfg.helpers }
|
|
233
|
+
Object.keys(cfg.helpers).forEach(helperName => {
|
|
234
|
+
if (cfg.helpers[helperName] && cfg.helpers[helperName].customLocatorStrategies !== undefined) {
|
|
235
|
+
cfg.helpers[helperName] = { ...cfg.helpers[helperName] }
|
|
236
|
+
delete cfg.helpers[helperName].customLocatorStrategies
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
return cfg
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const cleanedOldConfig = cleanConfig(oldConfig)
|
|
244
|
+
const cleanedNewConfig = cleanConfig(configWithoutFunctions)
|
|
245
|
+
|
|
204
246
|
const newConfig = {
|
|
205
|
-
...
|
|
206
|
-
...
|
|
207
|
-
}
|
|
208
|
-
this.options.override = JSON.stringify(newConfig)
|
|
247
|
+
...cleanedOldConfig,
|
|
248
|
+
...cleanedNewConfig,
|
|
249
|
+
}
|
|
250
|
+
this.options.override = JSON.stringify(newConfig)
|
|
209
251
|
}
|
|
210
252
|
|
|
211
253
|
addTestFiles(testGroup) {
|
|
212
|
-
this.addTests(convertToMochaTests(testGroup))
|
|
254
|
+
this.addTests(convertToMochaTests(testGroup))
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
addTests(tests) {
|
|
216
|
-
this.tests = this.tests.concat(tests)
|
|
258
|
+
this.tests = this.tests.concat(tests)
|
|
217
259
|
}
|
|
218
260
|
|
|
219
261
|
setTestRoot(path) {
|
|
220
|
-
this.testRoot = getTestRoot(path)
|
|
262
|
+
this.testRoot = getTestRoot(path)
|
|
221
263
|
}
|
|
222
264
|
|
|
223
265
|
addOptions(opts) {
|
|
224
266
|
this.options = {
|
|
225
267
|
...this.options,
|
|
226
268
|
...opts,
|
|
227
|
-
}
|
|
269
|
+
}
|
|
228
270
|
}
|
|
229
271
|
}
|
|
230
272
|
|
|
231
|
-
|
|
273
|
+
class Workers extends EventEmitter {
|
|
232
274
|
/**
|
|
233
275
|
* @param {Number} numberOfWorkers
|
|
234
276
|
* @param {Object} config
|
|
235
277
|
*/
|
|
236
278
|
constructor(numberOfWorkers, config = { by: 'test' }) {
|
|
237
|
-
super()
|
|
238
|
-
this.setMaxListeners(50)
|
|
239
|
-
this.
|
|
240
|
-
this.
|
|
241
|
-
this.
|
|
242
|
-
this.
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
245
|
-
this.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
279
|
+
super()
|
|
280
|
+
this.setMaxListeners(50)
|
|
281
|
+
this.codeceptPromise = initializeCodecept(config.testConfig, config.options)
|
|
282
|
+
this.codecept = null
|
|
283
|
+
this.config = config // Save config
|
|
284
|
+
this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
|
|
285
|
+
this.options = config.options || {}
|
|
286
|
+
this.errors = []
|
|
287
|
+
this.numberOfWorkers = 0
|
|
288
|
+
this.closedWorkers = 0
|
|
289
|
+
this.workers = []
|
|
290
|
+
this.testGroups = []
|
|
291
|
+
this.testPool = []
|
|
292
|
+
this.testPoolInitialized = false
|
|
293
|
+
this.isPoolMode = config.by === 'pool'
|
|
294
|
+
this.activeWorkers = new Map()
|
|
295
|
+
this.maxWorkers = numberOfWorkers // Track original worker count for pool mode
|
|
296
|
+
|
|
297
|
+
createOutputDir(config.testConfig)
|
|
298
|
+
// Defer worker initialization until codecept is ready
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async _ensureInitialized() {
|
|
302
|
+
if (!this.codecept) {
|
|
303
|
+
this.codecept = await this.codeceptPromise
|
|
304
|
+
// Initialize workers in these cases:
|
|
305
|
+
// 1. Positive number requested AND no manual workers pre-spawned
|
|
306
|
+
// 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned
|
|
307
|
+
const shouldAutoInit = this.workers.length === 0 && (
|
|
308
|
+
(Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) ||
|
|
309
|
+
(this.numberOfWorkersRequested < 0 && isFunction(this.config.by))
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if (shouldAutoInit) {
|
|
313
|
+
this._initWorkers(this.numberOfWorkersRequested, this.config)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
255
316
|
}
|
|
256
317
|
|
|
257
318
|
_initWorkers(numberOfWorkers, config) {
|
|
258
|
-
this.splitTestsByGroups(numberOfWorkers, config)
|
|
259
|
-
|
|
260
|
-
|
|
319
|
+
this.splitTestsByGroups(numberOfWorkers, config)
|
|
320
|
+
// For function-based grouping, use the actual number of test groups created
|
|
321
|
+
const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
|
|
322
|
+
this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
|
|
323
|
+
this.numberOfWorkers = this.workers.length
|
|
261
324
|
}
|
|
262
325
|
|
|
263
326
|
/**
|
|
@@ -268,22 +331,27 @@ export class Workers extends EventEmitter {
|
|
|
268
331
|
*
|
|
269
332
|
* - `suite`
|
|
270
333
|
* - `test`
|
|
334
|
+
* - `pool`
|
|
271
335
|
* - function(numberOfWorkers)
|
|
272
336
|
*
|
|
273
337
|
* This method can be overridden for a better split.
|
|
274
338
|
*/
|
|
275
339
|
splitTestsByGroups(numberOfWorkers, config) {
|
|
276
340
|
if (isFunction(config.by)) {
|
|
277
|
-
const createTests = config.by
|
|
278
|
-
const testGroups = createTests(numberOfWorkers)
|
|
341
|
+
const createTests = config.by
|
|
342
|
+
const testGroups = createTests(numberOfWorkers)
|
|
279
343
|
if (!(testGroups instanceof Array)) {
|
|
280
|
-
throw new Error('Test group should be an array')
|
|
344
|
+
throw new Error('Test group should be an array')
|
|
281
345
|
}
|
|
282
346
|
for (const testGroup of testGroups) {
|
|
283
|
-
this.testGroups.push(convertToMochaTests(testGroup))
|
|
347
|
+
this.testGroups.push(convertToMochaTests(testGroup))
|
|
284
348
|
}
|
|
285
349
|
} else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) {
|
|
286
|
-
|
|
350
|
+
if (config.by === 'pool') {
|
|
351
|
+
this.createTestPool(numberOfWorkers)
|
|
352
|
+
} else {
|
|
353
|
+
this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
|
|
354
|
+
}
|
|
287
355
|
}
|
|
288
356
|
}
|
|
289
357
|
|
|
@@ -293,53 +361,113 @@ export class Workers extends EventEmitter {
|
|
|
293
361
|
* @returns {WorkerObject}
|
|
294
362
|
*/
|
|
295
363
|
spawn() {
|
|
296
|
-
const worker = new WorkerObject(this.numberOfWorkers)
|
|
297
|
-
this.workers.push(worker)
|
|
298
|
-
this.numberOfWorkers += 1
|
|
299
|
-
return worker
|
|
364
|
+
const worker = new WorkerObject(this.numberOfWorkers)
|
|
365
|
+
this.workers.push(worker)
|
|
366
|
+
this.numberOfWorkers += 1
|
|
367
|
+
return worker
|
|
300
368
|
}
|
|
301
369
|
|
|
302
370
|
/**
|
|
303
371
|
* @param {Number} numberOfWorkers
|
|
304
372
|
*/
|
|
305
373
|
createGroupsOfTests(numberOfWorkers) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
mocha.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
374
|
+
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
375
|
+
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
376
|
+
const files = this.codecept.testFiles
|
|
377
|
+
const mocha = Container.mocha()
|
|
378
|
+
mocha.files = files
|
|
379
|
+
mocha.loadFiles()
|
|
380
|
+
|
|
381
|
+
const groups = populateGroups(numberOfWorkers)
|
|
382
|
+
let groupCounter = 0
|
|
383
|
+
|
|
384
|
+
mocha.suite.eachTest(test => {
|
|
385
|
+
const i = groupCounter % groups.length
|
|
316
386
|
if (test) {
|
|
317
|
-
groups[i].push(test.uid)
|
|
318
|
-
groupCounter
|
|
387
|
+
groups[i].push(test.uid)
|
|
388
|
+
groupCounter++
|
|
319
389
|
}
|
|
320
|
-
})
|
|
321
|
-
return groups
|
|
390
|
+
})
|
|
391
|
+
return groups
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @param {Number} numberOfWorkers
|
|
396
|
+
*/
|
|
397
|
+
createTestPool(numberOfWorkers) {
|
|
398
|
+
// For pool mode, create empty groups for each worker and initialize empty pool
|
|
399
|
+
// Test pool will be populated lazily when getNextTest() is first called
|
|
400
|
+
this.testPool = []
|
|
401
|
+
this.testPoolInitialized = false
|
|
402
|
+
this.testGroups = populateGroups(numberOfWorkers)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Initialize the test pool if not already done
|
|
407
|
+
* This is called lazily to avoid state pollution issues during construction
|
|
408
|
+
*/
|
|
409
|
+
_initializeTestPool() {
|
|
410
|
+
if (this.testPoolInitialized) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Ensure codecept is initialized
|
|
415
|
+
if (!this.codecept) {
|
|
416
|
+
output.log('Warning: codecept not initialized when initializing test pool')
|
|
417
|
+
this.testPoolInitialized = true
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const files = this.codecept.testFiles
|
|
422
|
+
if (!files || files.length === 0) {
|
|
423
|
+
this.testPoolInitialized = true
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// In ESM, test UIDs are not stable across different mocha instances
|
|
428
|
+
// So instead of using UIDs, we distribute test FILES
|
|
429
|
+
// Each file may contain multiple tests
|
|
430
|
+
for (const file of files) {
|
|
431
|
+
this.testPool.push(file)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this.testPoolInitialized = true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Gets the next test from the pool
|
|
439
|
+
* @returns {String|null} test file path or null if no tests available
|
|
440
|
+
*/
|
|
441
|
+
getNextTest() {
|
|
442
|
+
// Lazy initialization of test pool on first call
|
|
443
|
+
if (!this.testPoolInitialized) {
|
|
444
|
+
this._initializeTestPool()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return this.testPool.shift()
|
|
322
448
|
}
|
|
323
449
|
|
|
324
450
|
/**
|
|
325
451
|
* @param {Number} numberOfWorkers
|
|
326
452
|
*/
|
|
327
453
|
createGroupsOfSuites(numberOfWorkers) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
mocha.
|
|
334
|
-
mocha.
|
|
335
|
-
|
|
336
|
-
|
|
454
|
+
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
455
|
+
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
456
|
+
const files = this.codecept.testFiles
|
|
457
|
+
const groups = populateGroups(numberOfWorkers)
|
|
458
|
+
|
|
459
|
+
const mocha = Container.mocha()
|
|
460
|
+
mocha.files = files
|
|
461
|
+
mocha.loadFiles()
|
|
462
|
+
mocha.suite.suites.forEach(suite => {
|
|
463
|
+
const i = indexOfSmallestElement(groups)
|
|
464
|
+
suite.tests.forEach(test => {
|
|
337
465
|
if (test) {
|
|
338
|
-
groups[i].push(test.uid)
|
|
466
|
+
groups[i].push(test.uid)
|
|
339
467
|
}
|
|
340
|
-
})
|
|
341
|
-
})
|
|
342
|
-
return groups
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
return groups
|
|
343
471
|
}
|
|
344
472
|
|
|
345
473
|
/**
|
|
@@ -347,159 +475,267 @@ export class Workers extends EventEmitter {
|
|
|
347
475
|
*/
|
|
348
476
|
overrideConfig(config) {
|
|
349
477
|
for (const worker of this.workers) {
|
|
350
|
-
worker.addConfig(config)
|
|
478
|
+
worker.addConfig(config)
|
|
351
479
|
}
|
|
352
480
|
}
|
|
353
481
|
|
|
354
482
|
async bootstrapAll() {
|
|
355
|
-
|
|
483
|
+
await this._ensureInitialized()
|
|
484
|
+
return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll')
|
|
356
485
|
}
|
|
357
486
|
|
|
358
487
|
async teardownAll() {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this.
|
|
365
|
-
recorder.startUnlessRunning()
|
|
366
|
-
event.dispatcher.emit(event.workers.before)
|
|
367
|
-
process.env.RUNS_WITH_WORKERS = 'true'
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
488
|
+
await this._ensureInitialized()
|
|
489
|
+
return runHook(this.codecept.config.teardownAll, 'teardownAll')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async run() {
|
|
493
|
+
await this._ensureInitialized()
|
|
494
|
+
recorder.startUnlessRunning()
|
|
495
|
+
event.dispatcher.emit(event.workers.before)
|
|
496
|
+
process.env.RUNS_WITH_WORKERS = 'true'
|
|
497
|
+
|
|
498
|
+
// Create workers and set up message handlers immediately (not in recorder queue)
|
|
499
|
+
// This prevents a race condition where workers start sending messages before handlers are attached
|
|
500
|
+
const workerThreads = []
|
|
501
|
+
for (const worker of this.workers) {
|
|
502
|
+
const workerThread = createWorker(worker, this.isPoolMode)
|
|
503
|
+
this._listenWorkerEvents(workerThread)
|
|
504
|
+
workerThreads.push(workerThread)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
recorder.add('workers started', () => {
|
|
508
|
+
// Workers are already running, this is just a placeholder step
|
|
509
|
+
})
|
|
510
|
+
|
|
374
511
|
return new Promise(resolve => {
|
|
375
|
-
this.on('end', resolve)
|
|
376
|
-
})
|
|
512
|
+
this.on('end', resolve)
|
|
513
|
+
})
|
|
377
514
|
}
|
|
378
515
|
|
|
379
516
|
/**
|
|
380
517
|
* @returns {Array<WorkerObject>}
|
|
381
518
|
*/
|
|
382
519
|
getWorkers() {
|
|
383
|
-
return this.workers
|
|
520
|
+
return this.workers
|
|
384
521
|
}
|
|
385
522
|
|
|
386
523
|
/**
|
|
387
524
|
* @returns {Boolean}
|
|
388
525
|
*/
|
|
389
526
|
isFailed() {
|
|
390
|
-
return (
|
|
527
|
+
return (Container.result().failures.length || this.errors.length) > 0
|
|
391
528
|
}
|
|
392
529
|
|
|
393
530
|
_listenWorkerEvents(worker) {
|
|
394
|
-
worker
|
|
395
|
-
|
|
531
|
+
// Track worker thread for pool mode
|
|
532
|
+
if (this.isPoolMode) {
|
|
533
|
+
this.activeWorkers.set(worker, { available: true, workerIndex: null })
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
worker.on('message', message => {
|
|
537
|
+
output.process(message.workerIndex)
|
|
538
|
+
|
|
539
|
+
// Handle test requests for pool mode
|
|
540
|
+
if (message.type === 'REQUEST_TEST') {
|
|
541
|
+
if (this.isPoolMode) {
|
|
542
|
+
const nextTest = this.getNextTest()
|
|
543
|
+
if (nextTest) {
|
|
544
|
+
worker.postMessage({ type: 'TEST_ASSIGNED', test: nextTest })
|
|
545
|
+
} else {
|
|
546
|
+
worker.postMessage({ type: 'NO_MORE_TESTS' })
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return
|
|
550
|
+
}
|
|
396
551
|
|
|
397
552
|
// deal with events that are not test cycle related
|
|
398
553
|
if (!message.event) {
|
|
399
|
-
return this.emit('message', message)
|
|
554
|
+
return this.emit('message', message)
|
|
400
555
|
}
|
|
401
556
|
|
|
402
557
|
switch (message.event) {
|
|
403
|
-
case event.all.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
558
|
+
case event.all.result:
|
|
559
|
+
// we ensure consistency of result by adding tests in the very end
|
|
560
|
+
// Check if message.data.stats is valid before adding
|
|
561
|
+
if (message.data.stats) {
|
|
562
|
+
Container.result().addStats(message.data.stats)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (message.data.failures) {
|
|
566
|
+
Container.result().addFailures(message.data.failures)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (message.data.tests) {
|
|
570
|
+
message.data.tests.forEach(test => {
|
|
571
|
+
Container.result().addTest(deserializeTest(test))
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
break
|
|
407
576
|
case event.suite.before:
|
|
408
|
-
this.emit(event.suite.before,
|
|
409
|
-
break
|
|
410
|
-
case event.hook.failed:
|
|
411
|
-
this.emit(event.hook.failed, repackTest(message.data));
|
|
412
|
-
this.errors.push(message.data.err);
|
|
413
|
-
break;
|
|
577
|
+
this.emit(event.suite.before, deserializeSuite(message.data))
|
|
578
|
+
break
|
|
414
579
|
case event.test.before:
|
|
415
|
-
this.emit(event.test.before,
|
|
416
|
-
break
|
|
580
|
+
this.emit(event.test.before, deserializeTest(message.data))
|
|
581
|
+
break
|
|
417
582
|
case event.test.started:
|
|
418
|
-
this.emit(event.test.started,
|
|
419
|
-
break
|
|
583
|
+
this.emit(event.test.started, deserializeTest(message.data))
|
|
584
|
+
break
|
|
420
585
|
case event.test.failed:
|
|
421
|
-
|
|
422
|
-
|
|
586
|
+
// For hook failures, emit immediately as there won't be a test.finished event
|
|
587
|
+
// Regular test failures are handled via test.finished to support retries
|
|
588
|
+
if (message.data?.hookName) {
|
|
589
|
+
this.emit(event.test.failed, deserializeTest(message.data))
|
|
590
|
+
}
|
|
591
|
+
// Otherwise skip - we'll emit based on finished state
|
|
592
|
+
break
|
|
423
593
|
case event.test.passed:
|
|
424
|
-
|
|
425
|
-
break
|
|
594
|
+
// Skip individual passed events - we'll emit based on finished state
|
|
595
|
+
break
|
|
426
596
|
case event.test.skipped:
|
|
427
|
-
this.emit(event.test.skipped,
|
|
428
|
-
break
|
|
597
|
+
this.emit(event.test.skipped, deserializeTest(message.data))
|
|
598
|
+
break
|
|
429
599
|
case event.test.finished:
|
|
430
|
-
|
|
431
|
-
|
|
600
|
+
// Handle different types of test completion properly
|
|
601
|
+
{
|
|
602
|
+
const data = message.data
|
|
603
|
+
const uid = data?.uid
|
|
604
|
+
const isFailed = !!data?.err || data?.state === 'failed'
|
|
605
|
+
|
|
606
|
+
if (uid) {
|
|
607
|
+
// Track states for each test UID
|
|
608
|
+
if (!this._testStates) this._testStates = new Map()
|
|
609
|
+
|
|
610
|
+
if (!this._testStates.has(uid)) {
|
|
611
|
+
this._testStates.set(uid, { states: [], lastData: data })
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const testState = this._testStates.get(uid)
|
|
615
|
+
testState.states.push({ isFailed, data })
|
|
616
|
+
testState.lastData = data
|
|
617
|
+
} else {
|
|
618
|
+
// For tests without UID, emit immediately
|
|
619
|
+
if (isFailed) {
|
|
620
|
+
this.emit(event.test.failed, deserializeTest(data))
|
|
621
|
+
} else {
|
|
622
|
+
this.emit(event.test.passed, deserializeTest(data))
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
this.emit(event.test.finished, deserializeTest(data))
|
|
627
|
+
}
|
|
628
|
+
break
|
|
432
629
|
case event.test.after:
|
|
433
|
-
this.emit(event.test.after,
|
|
434
|
-
break
|
|
630
|
+
this.emit(event.test.after, deserializeTest(message.data))
|
|
631
|
+
break
|
|
435
632
|
case event.step.finished:
|
|
436
|
-
this.emit(event.step.finished, message.data)
|
|
437
|
-
break
|
|
633
|
+
this.emit(event.step.finished, message.data)
|
|
634
|
+
break
|
|
438
635
|
case event.step.started:
|
|
439
|
-
this.emit(event.step.started, message.data)
|
|
440
|
-
break
|
|
636
|
+
this.emit(event.step.started, message.data)
|
|
637
|
+
break
|
|
441
638
|
case event.step.passed:
|
|
442
|
-
this.emit(event.step.passed, message.data)
|
|
443
|
-
break
|
|
639
|
+
this.emit(event.step.passed, message.data)
|
|
640
|
+
break
|
|
444
641
|
case event.step.failed:
|
|
445
|
-
this.emit(event.step.failed, message.data)
|
|
446
|
-
break
|
|
642
|
+
this.emit(event.step.failed, message.data, message.data.error)
|
|
643
|
+
break
|
|
644
|
+
case event.hook.failed:
|
|
645
|
+
// Hook failures are already reported as test failures by the worker
|
|
646
|
+
// Just emit the hook.failed event for listeners
|
|
647
|
+
this.emit(event.hook.failed, message.data)
|
|
648
|
+
break
|
|
447
649
|
}
|
|
448
|
-
})
|
|
650
|
+
})
|
|
449
651
|
|
|
450
|
-
worker.on('error',
|
|
451
|
-
this.errors.push(err)
|
|
452
|
-
})
|
|
652
|
+
worker.on('error', err => {
|
|
653
|
+
this.errors.push(err)
|
|
654
|
+
})
|
|
453
655
|
|
|
454
656
|
worker.on('exit', () => {
|
|
455
|
-
this.closedWorkers += 1
|
|
456
|
-
|
|
457
|
-
|
|
657
|
+
this.closedWorkers += 1
|
|
658
|
+
|
|
659
|
+
if (this.isPoolMode) {
|
|
660
|
+
// Pool mode: finish when all workers have exited and no more tests
|
|
661
|
+
if (this.closedWorkers === this.numberOfWorkers) {
|
|
662
|
+
this._finishRun()
|
|
663
|
+
}
|
|
664
|
+
} else if (this.closedWorkers === this.numberOfWorkers) {
|
|
665
|
+
// Regular mode: finish when all original workers have exited
|
|
666
|
+
this._finishRun()
|
|
458
667
|
}
|
|
459
|
-
})
|
|
668
|
+
})
|
|
460
669
|
}
|
|
461
670
|
|
|
462
671
|
_finishRun() {
|
|
463
|
-
event.dispatcher.emit(event.workers.after)
|
|
464
|
-
if (
|
|
465
|
-
process.exitCode = 1
|
|
672
|
+
event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
|
|
673
|
+
if (Container.result().hasFailed) {
|
|
674
|
+
process.exitCode = 1
|
|
466
675
|
} else {
|
|
467
|
-
process.exitCode = 0
|
|
676
|
+
process.exitCode = 0
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Emit states for all tracked tests before emitting results
|
|
680
|
+
if (this._testStates) {
|
|
681
|
+
for (const [uid, { states, lastData }] of this._testStates) {
|
|
682
|
+
// For tests with retries configured, emit all failures + final success
|
|
683
|
+
// For tests without retries, emit only final state
|
|
684
|
+
const lastState = states[states.length - 1]
|
|
685
|
+
|
|
686
|
+
// Check if this test had retries by looking for failure followed by success
|
|
687
|
+
const hasRetryPattern = states.length > 1 &&
|
|
688
|
+
states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
|
|
689
|
+
|
|
690
|
+
if (hasRetryPattern) {
|
|
691
|
+
// Emit all intermediate failures and final success for retries
|
|
692
|
+
for (const state of states) {
|
|
693
|
+
if (state.isFailed) {
|
|
694
|
+
this.emit(event.test.failed, deserializeTest(state.data))
|
|
695
|
+
} else {
|
|
696
|
+
this.emit(event.test.passed, deserializeTest(state.data))
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
// For non-retries (like step failures), emit only the final state
|
|
701
|
+
if (lastState.isFailed) {
|
|
702
|
+
this.emit(event.test.failed, deserializeTest(lastState.data))
|
|
703
|
+
} else {
|
|
704
|
+
this.emit(event.test.passed, deserializeTest(lastState.data))
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
this._testStates.clear()
|
|
468
709
|
}
|
|
469
|
-
// removed this.finishedTests because in all /lib only first argument (!this.isFailed()) is used)
|
|
470
|
-
this.emit(event.all.result, { status: !this.isFailed(), stats: this.stats });
|
|
471
|
-
this.emit('end'); // internal event
|
|
472
|
-
}
|
|
473
710
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.
|
|
477
|
-
this.stats.tests += newStats.tests;
|
|
478
|
-
this.stats.pending += newStats.pending;
|
|
479
|
-
this.stats.failedHooks += newStats.failedHooks;
|
|
711
|
+
this.emit(event.all.result, Container.result())
|
|
712
|
+
event.dispatcher.emit(event.workers.result, Container.result())
|
|
713
|
+
this.emit('end') // internal event
|
|
480
714
|
}
|
|
481
715
|
|
|
482
716
|
printResults() {
|
|
483
|
-
|
|
484
|
-
|
|
717
|
+
const result = Container.result()
|
|
718
|
+
result.finish()
|
|
485
719
|
|
|
486
720
|
// Reset process for logs in main thread
|
|
487
|
-
output.
|
|
488
|
-
output.print()
|
|
721
|
+
output.process(null)
|
|
722
|
+
output.print()
|
|
489
723
|
|
|
490
|
-
this.failuresLog =
|
|
724
|
+
this.failuresLog = result.failures
|
|
491
725
|
.filter(log => log.length && typeof log[1] === 'number')
|
|
492
726
|
// mocha/lib/reporters/base.js
|
|
493
|
-
.map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack])
|
|
727
|
+
.map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack])
|
|
494
728
|
|
|
495
729
|
if (this.failuresLog.length) {
|
|
496
|
-
output.print()
|
|
497
|
-
output.print('-- FAILURES:')
|
|
498
|
-
this.failuresLog.forEach(log => output.print(...log))
|
|
730
|
+
output.print()
|
|
731
|
+
output.print('-- FAILURES:')
|
|
732
|
+
this.failuresLog.forEach(log => output.print(...log))
|
|
499
733
|
}
|
|
500
734
|
|
|
501
|
-
output.result(
|
|
502
|
-
|
|
503
|
-
process.env.RUNS_WITH_WORKERS = 'false'
|
|
735
|
+
output.result(result.stats?.passes || 0, result.stats?.failures || 0, result.stats?.pending || 0, ms(result.duration), result.stats?.failedHooks || 0)
|
|
736
|
+
|
|
737
|
+
process.env.RUNS_WITH_WORKERS = 'false'
|
|
504
738
|
}
|
|
505
739
|
}
|
|
740
|
+
|
|
741
|
+
export default Workers
|