codeceptjs 4.0.0-beta.6.esm-aria → 4.0.0-beta.8.esm-aria
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +116 -26
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- 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/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +60 -52
- package/translations/utils.js +2 -10
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5525 -3759
- package/typings/types.d.ts +5791 -3781
package/lib/command/generate.js
CHANGED
|
@@ -288,7 +288,7 @@ export async function heal(genPath) {
|
|
|
288
288
|
import './heal.js'
|
|
289
289
|
|
|
290
290
|
export const config = {
|
|
291
|
-
// ...
|
|
291
|
+
// ...
|
|
292
292
|
plugins: {
|
|
293
293
|
heal: {
|
|
294
294
|
enabled: true
|
|
@@ -301,3 +301,35 @@ export const config = {
|
|
|
301
301
|
if (!safeFileWrite(healFile, healTemplate)) return
|
|
302
302
|
output.success(`Heal recipes were created in ${healFile}`)
|
|
303
303
|
}
|
|
304
|
+
|
|
305
|
+
export async function prompt(promptName, genPath) {
|
|
306
|
+
if (!promptName) {
|
|
307
|
+
output.error('Please specify prompt name: writeStep, healStep, or generatePageObject')
|
|
308
|
+
output.print('Usage: npx codeceptjs generate:prompt <promptName>')
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const validPrompts = ['writeStep', 'healStep', 'generatePageObject']
|
|
313
|
+
if (!validPrompts.includes(promptName)) {
|
|
314
|
+
output.error(`Invalid prompt name: ${promptName}`)
|
|
315
|
+
output.print(`Valid prompts: ${validPrompts.join(', ')}`)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const testsPath = getTestRoot(genPath)
|
|
320
|
+
|
|
321
|
+
const promptsDir = path.join(testsPath, 'prompts')
|
|
322
|
+
if (!fileExists(promptsDir)) {
|
|
323
|
+
mkdirp.sync(promptsDir)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const templatePath = path.join(__dirname, `../template/prompts/${promptName}.js`)
|
|
327
|
+
const promptContent = fs.readFileSync(templatePath, 'utf8')
|
|
328
|
+
|
|
329
|
+
const promptFile = path.join(promptsDir, `${promptName}.${extension}`)
|
|
330
|
+
if (!safeFileWrite(promptFile, promptContent)) return
|
|
331
|
+
|
|
332
|
+
output.success(`Prompt ${promptName} was created in ${promptFile}`)
|
|
333
|
+
output.print('Customize this prompt to fit your needs.')
|
|
334
|
+
output.print('This prompt will be automatically loaded when AI features are enabled.')
|
|
335
|
+
}
|
package/lib/command/init.js
CHANGED
|
@@ -19,6 +19,11 @@ const defaultConfig = {
|
|
|
19
19
|
output: '',
|
|
20
20
|
helpers: {},
|
|
21
21
|
include: {},
|
|
22
|
+
plugins: {
|
|
23
|
+
htmlReporter: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium']
|
|
@@ -35,7 +40,6 @@ const packages = []
|
|
|
35
40
|
let isTypeScript = false
|
|
36
41
|
let extension = 'js'
|
|
37
42
|
|
|
38
|
-
const requireCodeceptConfigure = "const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure');"
|
|
39
43
|
const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';"
|
|
40
44
|
|
|
41
45
|
const configHeader = `
|
|
@@ -232,9 +236,9 @@ export default async function (initPath) {
|
|
|
232
236
|
fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8')
|
|
233
237
|
print(`Config created at ${typeScriptconfigFile}`)
|
|
234
238
|
} else {
|
|
235
|
-
configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\
|
|
239
|
+
configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexport const config = ${inspect(config, false, 4, false)}`)
|
|
236
240
|
|
|
237
|
-
if (hasConfigure) configSource =
|
|
241
|
+
if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource
|
|
238
242
|
|
|
239
243
|
fs.writeFileSync(configFile, configSource, 'utf-8')
|
|
240
244
|
print(`Config created at ${configFile}`)
|
|
@@ -12,7 +12,22 @@ export default async function (workerCount, selectedRuns, options) {
|
|
|
12
12
|
|
|
13
13
|
const { config: testConfig, override = '' } = options
|
|
14
14
|
const overrideConfigs = tryOrDefault(() => JSON.parse(override), {})
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// Determine test split strategy
|
|
17
|
+
let by = 'test' // default
|
|
18
|
+
if (options.by) {
|
|
19
|
+
// Explicit --by option takes precedence
|
|
20
|
+
by = options.by
|
|
21
|
+
} else if (options.suites) {
|
|
22
|
+
// Legacy --suites option
|
|
23
|
+
by = 'suite'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Validate the by option
|
|
27
|
+
const validStrategies = ['test', 'suite', 'pool']
|
|
28
|
+
if (!validStrategies.includes(by)) {
|
|
29
|
+
throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
|
|
30
|
+
}
|
|
16
31
|
delete options.parent
|
|
17
32
|
const config = {
|
|
18
33
|
by,
|
|
@@ -57,8 +72,22 @@ export default async function (workerCount, selectedRuns, options) {
|
|
|
57
72
|
await workers.run()
|
|
58
73
|
} catch (err) {
|
|
59
74
|
output.error(err)
|
|
60
|
-
process.
|
|
75
|
+
process.exitCode = 1
|
|
61
76
|
} finally {
|
|
62
77
|
await workers.teardownAll()
|
|
78
|
+
|
|
79
|
+
// Force exit if event loop doesn't clear naturally
|
|
80
|
+
// This is needed because worker threads may leave handles open
|
|
81
|
+
// even after proper cleanup, preventing natural process termination
|
|
82
|
+
if (!options.noExit) {
|
|
83
|
+
// Use beforeExit to ensure we run after all other exit handlers
|
|
84
|
+
// have set the correct exit code
|
|
85
|
+
process.once('beforeExit', (code) => {
|
|
86
|
+
// Give cleanup a moment to complete, then force exit with the correct code
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
process.exit(code || process.exitCode || 0)
|
|
89
|
+
}, 100)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
63
92
|
}
|
|
64
93
|
}
|
package/lib/command/run.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getConfig, printError, getTestRoot, createOutputDir } from './utils.js'
|
|
|
2
2
|
import Config from '../config.js'
|
|
3
3
|
import store from '../store.js'
|
|
4
4
|
import Codecept from '../codecept.js'
|
|
5
|
+
import container from '../container.js'
|
|
5
6
|
|
|
6
7
|
export default async function (test, options) {
|
|
7
8
|
// registering options globally to use in config
|
|
@@ -42,5 +43,19 @@ export default async function (test, options) {
|
|
|
42
43
|
process.exitCode = 1
|
|
43
44
|
} finally {
|
|
44
45
|
await codecept.teardown()
|
|
46
|
+
|
|
47
|
+
// Schedule a delayed exit to prevent process hanging due to browser helper event loops
|
|
48
|
+
// Only needed for Playwright/Puppeteer which keep the event loop alive
|
|
49
|
+
// Wait 1 second to allow final cleanup and output to complete
|
|
50
|
+
if (!process.env.CODECEPT_DISABLE_AUTO_EXIT) {
|
|
51
|
+
const helpers = container.helpers()
|
|
52
|
+
const hasBrowserHelper = helpers && (helpers.Playwright || helpers.Puppeteer || helpers.WebDriver)
|
|
53
|
+
|
|
54
|
+
if (hasBrowserHelper) {
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
process.exit(process.exitCode || 0)
|
|
57
|
+
}, 1000).unref()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
}
|
|
@@ -8,72 +8,336 @@ if (!tty.getWindowSize) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
import { parentPort, workerData } from 'worker_threads'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
|
|
12
|
+
// Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
|
|
13
|
+
// These will be imported dynamically when needed
|
|
14
|
+
let event, container, Codecept, getConfig, tryOrDefault, deepMerge
|
|
15
15
|
|
|
16
16
|
let stdout = ''
|
|
17
17
|
|
|
18
18
|
const stderr = ''
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
import Codecept from '../../codecept.js'
|
|
22
|
-
|
|
23
|
-
const { options, tests, testRoot, workerIndex } = workerData
|
|
20
|
+
const { options, tests, testRoot, workerIndex, poolMode } = workerData
|
|
24
21
|
|
|
25
22
|
// hide worker output
|
|
26
|
-
if
|
|
23
|
+
// In pool mode, only suppress output if debug is NOT enabled
|
|
24
|
+
// In regular mode, hide result output but allow step output in verbose/debug
|
|
25
|
+
if (poolMode && !options.debug) {
|
|
26
|
+
// In pool mode without debug, allow test names and important output but suppress verbose details
|
|
27
|
+
const originalWrite = process.stdout.write
|
|
28
|
+
process.stdout.write = string => {
|
|
29
|
+
// Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
|
|
30
|
+
if (
|
|
31
|
+
string.includes('✔') ||
|
|
32
|
+
string.includes('✖') ||
|
|
33
|
+
string.includes('Scenario Steps:') ||
|
|
34
|
+
string.includes('◯ Scenario Steps:') ||
|
|
35
|
+
string.includes('-- FAILURES:') ||
|
|
36
|
+
string.includes('AssertionError:') ||
|
|
37
|
+
string.includes('Feature(')
|
|
38
|
+
) {
|
|
39
|
+
return originalWrite.call(process.stdout, string)
|
|
40
|
+
}
|
|
41
|
+
// Suppress result summaries to avoid duplicates
|
|
42
|
+
if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('◯ File:')) {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
return originalWrite.call(process.stdout, string)
|
|
46
|
+
}
|
|
47
|
+
} else if (!poolMode && !options.debug && !options.verbose) {
|
|
27
48
|
process.stdout.write = string => {
|
|
28
49
|
stdout += string
|
|
29
50
|
return true
|
|
30
51
|
}
|
|
52
|
+
} else {
|
|
53
|
+
// In verbose/debug mode for test/suite modes, show step details
|
|
54
|
+
// but suppress individual worker result summaries to avoid duplicate output
|
|
55
|
+
const originalWrite = process.stdout.write
|
|
56
|
+
const originalConsoleLog = console.log
|
|
57
|
+
|
|
58
|
+
process.stdout.write = string => {
|
|
59
|
+
// Suppress individual worker result summaries and failure reports
|
|
60
|
+
if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:') || string.includes('◯ Scenario Steps:')) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
return originalWrite.call(process.stdout, string)
|
|
64
|
+
}
|
|
31
65
|
|
|
32
|
-
|
|
66
|
+
// Override console.log to catch result summaries
|
|
67
|
+
console.log = (...args) => {
|
|
68
|
+
const fullMessage = args.join(' ')
|
|
69
|
+
if (fullMessage.includes(' FAIL |') || fullMessage.includes(' OK |') || fullMessage.includes('-- FAILURES:')) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
return originalConsoleLog.apply(console, args)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
33
75
|
|
|
34
|
-
//
|
|
35
|
-
|
|
76
|
+
// Declare codecept and mocha at module level so they can be accessed by functions
|
|
77
|
+
let codecept
|
|
78
|
+
let mocha
|
|
79
|
+
let initPromise
|
|
80
|
+
let config
|
|
36
81
|
|
|
37
82
|
// Load test and run
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
initPromise = (async function () {
|
|
84
|
+
try {
|
|
85
|
+
// Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
|
|
86
|
+
const eventModule = await import('../../event.js')
|
|
87
|
+
const containerModule = await import('../../container.js')
|
|
88
|
+
const utilsModule = await import('../utils.js')
|
|
89
|
+
const coreUtilsModule = await import('../../utils.js')
|
|
90
|
+
const CodeceptModule = await import('../../codecept.js')
|
|
91
|
+
|
|
92
|
+
event = eventModule.default
|
|
93
|
+
container = containerModule.default
|
|
94
|
+
getConfig = utilsModule.getConfig
|
|
95
|
+
tryOrDefault = coreUtilsModule.tryOrDefault
|
|
96
|
+
deepMerge = coreUtilsModule.deepMerge
|
|
97
|
+
Codecept = CodeceptModule.default
|
|
98
|
+
|
|
99
|
+
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
100
|
+
|
|
101
|
+
// IMPORTANT: await is required here since getConfig is async
|
|
102
|
+
const baseConfig = await getConfig(options.config || testRoot)
|
|
103
|
+
|
|
104
|
+
// important deep merge so dynamic things e.g. functions on config are not overridden
|
|
105
|
+
config = deepMerge(baseConfig, overrideConfigs)
|
|
106
|
+
|
|
107
|
+
codecept = new Codecept(config, options)
|
|
108
|
+
await codecept.init(testRoot)
|
|
109
|
+
codecept.loadTests()
|
|
110
|
+
mocha = container.mocha()
|
|
43
111
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
112
|
+
if (poolMode) {
|
|
113
|
+
// In pool mode, don't filter tests upfront - wait for assignments
|
|
114
|
+
// We'll reload test files fresh for each test request
|
|
115
|
+
} else {
|
|
116
|
+
// Legacy mode - filter tests upfront
|
|
117
|
+
filterTests()
|
|
118
|
+
}
|
|
48
119
|
|
|
49
|
-
|
|
50
|
-
|
|
120
|
+
// run tests
|
|
121
|
+
if (poolMode) {
|
|
122
|
+
await runPoolTests()
|
|
123
|
+
} else if (mocha.suite.total()) {
|
|
124
|
+
await runTests()
|
|
125
|
+
} else {
|
|
126
|
+
// No tests to run, close the worker
|
|
127
|
+
parentPort?.close()
|
|
51
128
|
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('Error in worker initialization:', err)
|
|
131
|
+
process.exit(1)
|
|
52
132
|
}
|
|
133
|
+
})()
|
|
53
134
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
135
|
+
let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
|
|
136
|
+
|
|
137
|
+
async function runTests() {
|
|
138
|
+
try {
|
|
139
|
+
await codecept.bootstrap()
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw new Error(`Error while running bootstrap file :${err}`)
|
|
142
|
+
}
|
|
143
|
+
listenToParentThread()
|
|
144
|
+
initializeListeners()
|
|
145
|
+
disablePause()
|
|
146
|
+
try {
|
|
147
|
+
await codecept.run()
|
|
148
|
+
} finally {
|
|
149
|
+
await codecept.teardown()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runPoolTests() {
|
|
154
|
+
try {
|
|
155
|
+
await codecept.bootstrap()
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new Error(`Error while running bootstrap file :${err}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
initializeListeners()
|
|
161
|
+
disablePause()
|
|
162
|
+
|
|
163
|
+
// Emit event.all.before once at the start of pool mode
|
|
164
|
+
event.dispatcher.emit(event.all.before, codecept)
|
|
165
|
+
|
|
166
|
+
// Accumulate results across all tests in pool mode
|
|
167
|
+
let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
|
|
168
|
+
let allTests = []
|
|
169
|
+
let allFailures = []
|
|
170
|
+
let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
|
|
171
|
+
|
|
172
|
+
// Keep requesting tests until no more available
|
|
173
|
+
while (true) {
|
|
174
|
+
// Request a test assignment and wait for response
|
|
175
|
+
const testResult = await new Promise((resolve, reject) => {
|
|
176
|
+
// Set up pool mode message handler FIRST before sending request
|
|
177
|
+
const messageHandler = async eventData => {
|
|
178
|
+
// Remove handler immediately to prevent duplicate processing
|
|
179
|
+
parentPort?.off('message', messageHandler)
|
|
180
|
+
|
|
181
|
+
if (eventData.type === 'TEST_ASSIGNED') {
|
|
182
|
+
// In pool mode with ESM, we receive test FILE paths instead of UIDs
|
|
183
|
+
// because UIDs are not stable across different mocha instances
|
|
184
|
+
const testIdentifier = eventData.test
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Create a fresh Mocha instance for each test file
|
|
188
|
+
container.createMocha()
|
|
189
|
+
const mocha = container.mocha()
|
|
190
|
+
|
|
191
|
+
// Load only the assigned test file
|
|
192
|
+
mocha.files = [testIdentifier]
|
|
193
|
+
mocha.loadFiles()
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Loaded ${testIdentifier}, tests: ${mocha.suite.total()}\n`)
|
|
197
|
+
} catch (e) { /* ignore */ }
|
|
198
|
+
|
|
199
|
+
if (mocha.suite.total() > 0) {
|
|
200
|
+
// Run only the tests in the current mocha suite
|
|
201
|
+
// Don't use codecept.run() as it overwrites mocha.files with ALL test files
|
|
202
|
+
await new Promise((resolve, reject) => {
|
|
203
|
+
mocha.run(() => {
|
|
204
|
+
try {
|
|
205
|
+
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Finished ${testIdentifier}\n`)
|
|
206
|
+
} catch (e) { /* ignore */ }
|
|
207
|
+
resolve()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Get the results from this specific test run
|
|
212
|
+
const result = container.result()
|
|
213
|
+
const currentStats = result.stats || {}
|
|
214
|
+
|
|
215
|
+
// Calculate the difference from previous accumulated stats
|
|
216
|
+
const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes)
|
|
217
|
+
const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures)
|
|
218
|
+
const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests)
|
|
219
|
+
const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending)
|
|
220
|
+
const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks)
|
|
221
|
+
|
|
222
|
+
// Add only the new results
|
|
223
|
+
consolidatedStats.passes += newPasses
|
|
224
|
+
consolidatedStats.failures += newFailures
|
|
225
|
+
consolidatedStats.tests += newTests
|
|
226
|
+
consolidatedStats.pending += newPending
|
|
227
|
+
consolidatedStats.failedHooks += newFailedHooks
|
|
228
|
+
|
|
229
|
+
// Update previous stats for next comparison
|
|
230
|
+
previousStats = { ...currentStats }
|
|
231
|
+
|
|
232
|
+
// Add new failures to consolidated collections
|
|
233
|
+
if (result.failures && result.failures.length > allFailures.length) {
|
|
234
|
+
const newFailures = result.failures.slice(allFailures.length)
|
|
235
|
+
allFailures.push(...newFailures)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Signal test completed
|
|
240
|
+
resolve('TEST_COMPLETED')
|
|
241
|
+
} catch (err) {
|
|
242
|
+
reject(err)
|
|
243
|
+
}
|
|
244
|
+
} else if (eventData.type === 'NO_MORE_TESTS') {
|
|
245
|
+
// No tests available, exit worker
|
|
246
|
+
resolve('NO_MORE_TESTS')
|
|
247
|
+
} else {
|
|
248
|
+
// Handle other message types (support messages, etc.)
|
|
249
|
+
container.append({ support: eventData.data })
|
|
250
|
+
// Don't re-add handler - each test request creates its own one-time handler
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Set up handler BEFORE sending request to avoid race condition
|
|
255
|
+
parentPort?.on('message', messageHandler)
|
|
256
|
+
|
|
257
|
+
// Now send the request
|
|
258
|
+
sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Exit if no more tests
|
|
262
|
+
if (testResult === 'NO_MORE_TESTS') {
|
|
263
|
+
break
|
|
59
264
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Emit event.all.after once at the end of pool mode
|
|
268
|
+
event.dispatcher.emit(event.all.after, codecept)
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await codecept.teardown()
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// Log teardown errors but don't fail
|
|
274
|
+
console.error('Teardown error:', err)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Send final consolidated results for the entire worker
|
|
278
|
+
const finalResult = {
|
|
279
|
+
hasFailed: consolidatedStats.failures > 0,
|
|
280
|
+
stats: consolidatedStats,
|
|
281
|
+
duration: 0, // Pool mode doesn't track duration per worker
|
|
282
|
+
tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient
|
|
283
|
+
failures: allFailures, // Include all failures for error reporting
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
sendToParentThread({ event: event.all.after, workerIndex, data: finalResult })
|
|
287
|
+
sendToParentThread({ event: event.all.result, workerIndex, data: finalResult })
|
|
288
|
+
|
|
289
|
+
// Add longer delay to ensure messages are delivered before closing
|
|
290
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
291
|
+
|
|
292
|
+
// Close worker thread when pool mode is complete
|
|
293
|
+
parentPort?.close()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function filterTestById(testUid) {
|
|
297
|
+
// In pool mode with ESM, test files are already loaded once at initialization
|
|
298
|
+
// We just need to filter the existing mocha suite to only include the target test
|
|
299
|
+
|
|
300
|
+
// Get the existing mocha instance
|
|
301
|
+
const mocha = container.mocha()
|
|
302
|
+
|
|
303
|
+
// Save reference to all suites before clearing
|
|
304
|
+
const allSuites = [...mocha.suite.suites]
|
|
305
|
+
|
|
306
|
+
// Clear suites and tests but preserve other mocha settings
|
|
307
|
+
mocha.suite.suites = []
|
|
308
|
+
mocha.suite.tests = []
|
|
309
|
+
|
|
310
|
+
// Find and add only the suite containing our target test
|
|
311
|
+
let foundTest = false
|
|
312
|
+
for (const suite of allSuites) {
|
|
313
|
+
const originalTests = [...suite.tests]
|
|
314
|
+
|
|
315
|
+
// Check if this suite has our target test
|
|
316
|
+
const targetTest = originalTests.find(test => test.uid === testUid)
|
|
317
|
+
|
|
318
|
+
if (targetTest) {
|
|
319
|
+
// Create a filtered suite with only the target test
|
|
320
|
+
suite.tests = [targetTest]
|
|
321
|
+
mocha.suite.suites.push(suite)
|
|
322
|
+
foundTest = true
|
|
323
|
+
break // Only include one test
|
|
67
324
|
}
|
|
68
325
|
}
|
|
69
326
|
|
|
70
|
-
|
|
327
|
+
if (!foundTest) {
|
|
328
|
+
console.error(`WARNING: Test with UID ${testUid} not found in mocha suites`)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
71
331
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
332
|
+
function filterTests() {
|
|
333
|
+
const files = codecept.testFiles
|
|
334
|
+
mocha.files = files
|
|
335
|
+
mocha.loadFiles()
|
|
336
|
+
|
|
337
|
+
for (const suite of mocha.suite.suites) {
|
|
338
|
+
suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
|
|
75
339
|
}
|
|
76
|
-
}
|
|
340
|
+
}
|
|
77
341
|
|
|
78
342
|
function initializeListeners() {
|
|
79
343
|
// suite
|
|
@@ -92,10 +356,11 @@ function initializeListeners() {
|
|
|
92
356
|
const serializableErr = serializeError(err)
|
|
93
357
|
safelySendToParent({ event: event.test.finished, workerIndex, data: { ...simplifiedData, err: serializableErr } })
|
|
94
358
|
})
|
|
95
|
-
event.dispatcher.on(event.test.failed, (test, err) => {
|
|
359
|
+
event.dispatcher.on(event.test.failed, (test, err, hookName) => {
|
|
96
360
|
const simplifiedData = test.simplify()
|
|
97
361
|
const serializableErr = serializeError(err)
|
|
98
|
-
|
|
362
|
+
// Include hookName to identify hook failures
|
|
363
|
+
safelySendToParent({ event: event.test.failed, workerIndex, data: { ...simplifiedData, err: serializableErr, hookName } })
|
|
99
364
|
})
|
|
100
365
|
event.dispatcher.on(event.test.passed, (test, err) => safelySendToParent({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } }))
|
|
101
366
|
event.dispatcher.on(event.test.started, test => safelySendToParent({ event: event.test.started, workerIndex, data: test.simplify() }))
|
|
@@ -107,19 +372,24 @@ function initializeListeners() {
|
|
|
107
372
|
event.dispatcher.on(event.step.passed, step => safelySendToParent({ event: event.step.passed, workerIndex, data: step.simplify() }))
|
|
108
373
|
event.dispatcher.on(event.step.failed, step => safelySendToParent({ event: event.step.failed, workerIndex, data: step.simplify() }))
|
|
109
374
|
|
|
110
|
-
event.dispatcher.on(event.hook.failed, (hook, err) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
375
|
+
event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } }))
|
|
376
|
+
event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
|
|
377
|
+
event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() }))
|
|
378
|
+
|
|
379
|
+
if (!poolMode) {
|
|
380
|
+
// In regular mode, close worker after all tests are complete
|
|
381
|
+
event.dispatcher.once(event.all.after, () => {
|
|
382
|
+
sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
|
|
383
|
+
})
|
|
384
|
+
// all
|
|
385
|
+
event.dispatcher.once(event.all.result, () => {
|
|
386
|
+
sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
|
|
387
|
+
parentPort?.close()
|
|
388
|
+
})
|
|
389
|
+
} else {
|
|
390
|
+
// In pool mode, don't send result events for individual tests
|
|
391
|
+
// Results will be sent once when the worker completes all tests
|
|
392
|
+
}
|
|
123
393
|
}
|
|
124
394
|
|
|
125
395
|
function disablePause() {
|
|
@@ -175,7 +445,10 @@ function sendToParentThread(data) {
|
|
|
175
445
|
}
|
|
176
446
|
|
|
177
447
|
function listenToParentThread() {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
448
|
+
if (!poolMode) {
|
|
449
|
+
parentPort?.on('message', eventData => {
|
|
450
|
+
container.append({ support: eventData.data })
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
// In pool mode, message handling is done in runPoolTests()
|
|
181
454
|
}
|
package/lib/config.js
CHANGED
|
@@ -34,7 +34,7 @@ const defaultConfig = {
|
|
|
34
34
|
let hooks = []
|
|
35
35
|
let config = {}
|
|
36
36
|
|
|
37
|
-
const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.ts', 'codecept.conf.ts']
|
|
37
|
+
const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Current configuration
|
|
@@ -69,9 +69,20 @@ class Config {
|
|
|
69
69
|
configFile = path.resolve(configFile || '.')
|
|
70
70
|
|
|
71
71
|
if (!fileExists(configFile)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// Try different extensions if the file doesn't exist
|
|
73
|
+
const extensions = ['.ts', '.cjs', '.mjs']
|
|
74
|
+
let found = false
|
|
75
|
+
|
|
76
|
+
for (const ext of extensions) {
|
|
77
|
+
const altConfig = configFile.replace(/\.js$/, ext)
|
|
78
|
+
if (fileExists(altConfig)) {
|
|
79
|
+
configFile = altConfig
|
|
80
|
+
found = true
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!found) {
|
|
75
86
|
throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`)
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -198,7 +209,7 @@ async function loadConfigFile(configFile) {
|
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
const rawConfig = configModule.config || configModule.default?.config || configModule
|
|
212
|
+
const rawConfig = configModule.config || configModule.default?.config || configModule.default || configModule
|
|
202
213
|
|
|
203
214
|
// Process helpers to extract imported classes
|
|
204
215
|
if (rawConfig.helpers) {
|