codeceptjs 4.0.1-beta.3 → 4.0.1-beta.31
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/bin/codecept.js +2 -2
- package/lib/actor.js +12 -8
- package/lib/command/definitions.js +8 -3
- package/lib/command/workers/runTests.js +3 -1
- package/lib/config.js +3 -2
- package/lib/container.js +107 -22
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +96 -152
- package/lib/helper/Puppeteer.js +8 -5
- package/lib/helper/REST.js +13 -8
- package/lib/listener/config.js +11 -3
- package/lib/mocha/factory.js +2 -27
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/step/base.js +14 -1
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +8 -0
- package/lib/utils/loaderCheck.js +13 -3
- package/lib/utils/typescript.js +80 -33
- package/lib/workers.js +36 -41
- package/package.json +21 -21
- package/typings/index.d.ts +1 -1
- package/typings/promiseBasedTypes.d.ts +136 -43
- package/typings/types.d.ts +150 -74
package/lib/step/meta.js
CHANGED
|
@@ -58,17 +58,24 @@ class MetaStep extends Step {
|
|
|
58
58
|
this.status = 'queued'
|
|
59
59
|
this.setArguments(Array.from(arguments).slice(1))
|
|
60
60
|
let result
|
|
61
|
+
let hasChildSteps = false
|
|
61
62
|
|
|
62
63
|
const registerStep = step => {
|
|
63
64
|
this.setMetaStep(null)
|
|
64
65
|
step.setMetaStep(this)
|
|
66
|
+
hasChildSteps = true
|
|
65
67
|
}
|
|
66
68
|
event.dispatcher.prependListener(event.step.before, registerStep)
|
|
69
|
+
|
|
70
|
+
// Start timing
|
|
71
|
+
this.startTime = Date.now()
|
|
72
|
+
|
|
67
73
|
// Handle async and sync methods.
|
|
68
74
|
if (fn.constructor.name === 'AsyncFunction') {
|
|
69
75
|
result = fn
|
|
70
76
|
.apply(this.context, this.args)
|
|
71
77
|
.then(result => {
|
|
78
|
+
this.setStatus('success')
|
|
72
79
|
return result
|
|
73
80
|
})
|
|
74
81
|
.catch(error => {
|
|
@@ -78,17 +85,27 @@ class MetaStep extends Step {
|
|
|
78
85
|
.finally(() => {
|
|
79
86
|
this.endTime = Date.now()
|
|
80
87
|
event.dispatcher.removeListener(event.step.before, registerStep)
|
|
88
|
+
// Only emit events if no child steps were registered
|
|
89
|
+
if (!hasChildSteps) {
|
|
90
|
+
event.emit(event.step.started, this)
|
|
91
|
+
event.emit(event.step.finished, this)
|
|
92
|
+
}
|
|
81
93
|
})
|
|
82
94
|
} else {
|
|
83
95
|
try {
|
|
84
|
-
this.startTime = Date.now()
|
|
85
96
|
result = fn.apply(this.context, this.args)
|
|
97
|
+
this.setStatus('success')
|
|
86
98
|
} catch (error) {
|
|
87
99
|
this.setStatus('failed')
|
|
88
100
|
throw error
|
|
89
101
|
} finally {
|
|
90
102
|
this.endTime = Date.now()
|
|
91
103
|
event.dispatcher.removeListener(event.step.before, registerStep)
|
|
104
|
+
// Only emit events if no child steps were registered
|
|
105
|
+
if (!hasChildSteps) {
|
|
106
|
+
event.emit(event.step.started, this)
|
|
107
|
+
event.emit(event.step.finished, this)
|
|
108
|
+
}
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
|
package/lib/step/record.js
CHANGED
|
@@ -5,6 +5,7 @@ import output from '../output.js'
|
|
|
5
5
|
import store from '../store.js'
|
|
6
6
|
import { TIMEOUT_ORDER } from '../timeout.js'
|
|
7
7
|
import retryStep from './retry.js'
|
|
8
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
8
9
|
function recordStep(step, args) {
|
|
9
10
|
step.status = 'queued'
|
|
10
11
|
|
|
@@ -60,6 +61,13 @@ function recordStep(step, args) {
|
|
|
60
61
|
recorder.catch(err => {
|
|
61
62
|
step.status = 'failed'
|
|
62
63
|
step.endTime = +Date.now()
|
|
64
|
+
|
|
65
|
+
// Fix error stack to point to original .ts files (lazy import to avoid circular dependency)
|
|
66
|
+
const fileMapping = global.container?.tsFileMapping?.()
|
|
67
|
+
if (fileMapping) {
|
|
68
|
+
fixErrorStack(err, fileMapping)
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
event.emit(event.step.failed, step, err)
|
|
64
72
|
event.emit(event.step.finished, step)
|
|
65
73
|
throw err
|
package/lib/utils/loaderCheck.js
CHANGED
|
@@ -65,9 +65,18 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
|
|
|
65
65
|
✅ Complete: Handles all TypeScript features
|
|
66
66
|
|
|
67
67
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
68
|
-
│ Option 2: ts-node/esm (
|
|
68
|
+
│ Option 2: ts-node/esm (Not Recommended - Has Module Resolution Issues) │
|
|
69
69
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
70
70
|
|
|
71
|
+
⚠️ ts-node/esm has significant limitations and is not recommended:
|
|
72
|
+
- Doesn't work with "type": "module" in package.json
|
|
73
|
+
- Module resolution doesn't work like standard TypeScript ESM
|
|
74
|
+
- Import statements must use explicit file paths
|
|
75
|
+
|
|
76
|
+
We strongly recommend using tsx/cjs instead.
|
|
77
|
+
|
|
78
|
+
If you still want to use ts-node/esm:
|
|
79
|
+
|
|
71
80
|
Installation:
|
|
72
81
|
npm install --save-dev ts-node
|
|
73
82
|
|
|
@@ -84,11 +93,12 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
|
|
|
84
93
|
"esModuleInterop": true
|
|
85
94
|
},
|
|
86
95
|
"ts-node": {
|
|
87
|
-
"esm": true
|
|
88
|
-
"experimentalSpecifierResolution": "node"
|
|
96
|
+
"esm": true
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
3. Do NOT use "type": "module" in package.json
|
|
101
|
+
|
|
92
102
|
📚 Documentation: https://codecept.io/typescript
|
|
93
103
|
|
|
94
104
|
Note: TypeScript config files (codecept.conf.ts) and helpers are automatically
|
package/lib/utils/typescript.js
CHANGED
|
@@ -4,10 +4,10 @@ import path from 'path'
|
|
|
4
4
|
/**
|
|
5
5
|
* Transpile TypeScript files to ES modules with CommonJS shim support
|
|
6
6
|
* Handles recursive transpilation of imported TypeScript files
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param {string} mainFilePath - Path to the main TypeScript file to transpile
|
|
9
9
|
* @param {object} typescript - TypeScript compiler instance
|
|
10
|
-
* @returns {Promise<{tempFile: string, allTempFiles: string[]}>} - Main temp file and all temp files created
|
|
10
|
+
* @returns {Promise<{tempFile: string, allTempFiles: string[], fileMapping: any}>} - Main temp file and all temp files created
|
|
11
11
|
*/
|
|
12
12
|
export async function transpileTypeScript(mainFilePath, typescript) {
|
|
13
13
|
const { transpile } = typescript
|
|
@@ -18,7 +18,7 @@ export async function transpileTypeScript(mainFilePath, typescript) {
|
|
|
18
18
|
*/
|
|
19
19
|
const transpileTS = (filePath) => {
|
|
20
20
|
const tsContent = fs.readFileSync(filePath, 'utf8')
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
// Transpile TypeScript to JavaScript with ES module output
|
|
23
23
|
let jsContent = transpile(tsContent, {
|
|
24
24
|
module: 99, // ModuleKind.ESNext
|
|
@@ -29,16 +29,16 @@ export async function transpileTypeScript(mainFilePath, typescript) {
|
|
|
29
29
|
suppressOutputPathCheck: true,
|
|
30
30
|
skipLibCheck: true,
|
|
31
31
|
})
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
// Check if the code uses CommonJS globals
|
|
34
34
|
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
|
|
35
35
|
const usesRequire = /\brequire\s*\(/.test(jsContent)
|
|
36
36
|
const usesModuleExports = /\b(module\.exports|exports\.)/.test(jsContent)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
if (usesCommonJSGlobals || usesRequire || usesModuleExports) {
|
|
39
39
|
// Inject ESM equivalents at the top of the file
|
|
40
40
|
let esmGlobals = ''
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
if (usesRequire || usesModuleExports) {
|
|
43
43
|
// IMPORTANT: Use the original .ts file path as the base for require()
|
|
44
44
|
// This ensures dynamic require() calls work with relative paths from the original file location
|
|
@@ -81,7 +81,7 @@ const exports = module.exports;
|
|
|
81
81
|
|
|
82
82
|
`
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
if (usesCommonJSGlobals) {
|
|
86
86
|
// For __dirname and __filename, also use the original file path
|
|
87
87
|
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
|
|
@@ -92,48 +92,48 @@ const __dirname = __dirname_fn(__filename);
|
|
|
92
92
|
|
|
93
93
|
`
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
jsContent = esmGlobals + jsContent
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
// If module.exports is used, we need to export it as default
|
|
99
99
|
if (usesModuleExports) {
|
|
100
100
|
jsContent += `\nexport default module.exports;\n`
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
return jsContent
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
// Create a map to track transpiled files
|
|
108
108
|
const transpiledFiles = new Map()
|
|
109
109
|
const baseDir = path.dirname(mainFilePath)
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
// Recursive function to transpile a file and all its TypeScript dependencies
|
|
112
112
|
const transpileFileAndDeps = (filePath) => {
|
|
113
113
|
// Already transpiled, skip
|
|
114
114
|
if (transpiledFiles.has(filePath)) {
|
|
115
115
|
return
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Transpile this file
|
|
119
119
|
let jsContent = transpileTS(filePath)
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// Find all relative TypeScript imports in this file
|
|
122
122
|
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
|
|
123
123
|
let match
|
|
124
124
|
const imports = []
|
|
125
|
-
|
|
125
|
+
|
|
126
126
|
while ((match = importRegex.exec(jsContent)) !== null) {
|
|
127
127
|
imports.push(match[1])
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
// Get the base directory for this file
|
|
131
131
|
const fileBaseDir = path.dirname(filePath)
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
// Recursively transpile each imported TypeScript file
|
|
134
134
|
for (const relativeImport of imports) {
|
|
135
135
|
let importedPath = path.resolve(fileBaseDir, relativeImport)
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
// Handle .js extensions that might actually be .ts files
|
|
138
138
|
if (importedPath.endsWith('.js')) {
|
|
139
139
|
const tsVersion = importedPath.replace(/\.js$/, '.ts')
|
|
@@ -141,9 +141,14 @@ const __dirname = __dirname_fn(__filename);
|
|
|
141
141
|
importedPath = tsVersion
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
144
|
+
|
|
145
|
+
// Check for standard module extensions to determine if we should try adding .ts
|
|
146
|
+
const ext = path.extname(importedPath)
|
|
147
|
+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
|
|
148
|
+
const hasStandardExtension = standardExtensions.includes(ext.toLowerCase())
|
|
149
|
+
|
|
150
|
+
// If it doesn't end with .ts and doesn't have a standard extension, try adding .ts
|
|
151
|
+
if (!importedPath.endsWith('.ts') && !hasStandardExtension) {
|
|
147
152
|
const tsPath = importedPath + '.ts'
|
|
148
153
|
if (fs.existsSync(tsPath)) {
|
|
149
154
|
importedPath = tsPath
|
|
@@ -156,19 +161,20 @@ const __dirname = __dirname_fn(__filename);
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
|
-
|
|
164
|
+
|
|
160
165
|
// If it's a TypeScript file, recursively transpile it and its dependencies
|
|
161
166
|
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
|
|
162
167
|
transpileFileAndDeps(importedPath)
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
// After all dependencies are transpiled, rewrite imports in this file
|
|
167
172
|
jsContent = jsContent.replace(
|
|
168
173
|
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
|
|
169
174
|
(match, importPath) => {
|
|
170
175
|
let resolvedPath = path.resolve(fileBaseDir, importPath)
|
|
171
|
-
|
|
176
|
+
const originalExt = path.extname(importPath)
|
|
177
|
+
|
|
172
178
|
// Handle .js extension that might be .ts
|
|
173
179
|
if (resolvedPath.endsWith('.js')) {
|
|
174
180
|
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
|
|
@@ -181,11 +187,13 @@ const __dirname = __dirname_fn(__filename);
|
|
|
181
187
|
}
|
|
182
188
|
return `from '${relPath}'`
|
|
183
189
|
}
|
|
190
|
+
// Keep .js extension as-is (might be a real .js file)
|
|
191
|
+
return match
|
|
184
192
|
}
|
|
185
|
-
|
|
193
|
+
|
|
186
194
|
// Try with .ts extension
|
|
187
195
|
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
|
|
188
|
-
|
|
196
|
+
|
|
189
197
|
// If we transpiled this file, use the temp file
|
|
190
198
|
if (transpiledFiles.has(tsPath)) {
|
|
191
199
|
const tempFile = transpiledFiles.get(tsPath)
|
|
@@ -196,28 +204,67 @@ const __dirname = __dirname_fn(__filename);
|
|
|
196
204
|
}
|
|
197
205
|
return `from '${relPath}'`
|
|
198
206
|
}
|
|
199
|
-
|
|
207
|
+
|
|
208
|
+
// If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json)
|
|
209
|
+
// add .js for ESM compatibility
|
|
210
|
+
// This handles cases where:
|
|
211
|
+
// 1. Import has no real extension (e.g., "./utils" or "./helper")
|
|
212
|
+
// 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper")
|
|
213
|
+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
|
|
214
|
+
const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
|
|
215
|
+
|
|
216
|
+
if (!hasStandardExtension) {
|
|
217
|
+
return match.replace(importPath, importPath + '.js')
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
// Otherwise, keep the import as-is
|
|
201
221
|
return match
|
|
202
222
|
}
|
|
203
223
|
)
|
|
204
|
-
|
|
224
|
+
|
|
205
225
|
// Write the transpiled file with updated imports
|
|
206
226
|
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
|
|
207
227
|
fs.writeFileSync(tempFile, jsContent)
|
|
208
228
|
transpiledFiles.set(filePath, tempFile)
|
|
209
229
|
}
|
|
210
|
-
|
|
230
|
+
|
|
211
231
|
// Start recursive transpilation from the main file
|
|
212
232
|
transpileFileAndDeps(mainFilePath)
|
|
213
|
-
|
|
233
|
+
|
|
214
234
|
// Get the main transpiled file
|
|
215
235
|
const tempJsFile = transpiledFiles.get(mainFilePath)
|
|
216
|
-
|
|
236
|
+
|
|
217
237
|
// Store all temp files for cleanup
|
|
218
238
|
const allTempFiles = Array.from(transpiledFiles.values())
|
|
219
|
-
|
|
220
|
-
return { tempFile: tempJsFile, allTempFiles }
|
|
239
|
+
|
|
240
|
+
return { tempFile: tempJsFile, allTempFiles, fileMapping: transpiledFiles }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Map error stack traces from temp .mjs files back to original .ts files
|
|
245
|
+
* @param {Error} error - The error object to fix
|
|
246
|
+
* @param {Map<string, string>} fileMapping - Map of original .ts files to temp .mjs files
|
|
247
|
+
* @returns {Error} - Error with fixed stack trace
|
|
248
|
+
*/
|
|
249
|
+
export function fixErrorStack(error, fileMapping) {
|
|
250
|
+
if (!error.stack || !fileMapping) return error
|
|
251
|
+
|
|
252
|
+
let stack = error.stack
|
|
253
|
+
|
|
254
|
+
// Create reverse mapping (temp.mjs -> original.ts)
|
|
255
|
+
const reverseMap = new Map()
|
|
256
|
+
for (const [tsFile, mjsFile] of fileMapping.entries()) {
|
|
257
|
+
reverseMap.set(mjsFile, tsFile)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Replace all temp.mjs references with original .ts files
|
|
261
|
+
for (const [mjsFile, tsFile] of reverseMap.entries()) {
|
|
262
|
+
const mjsPattern = mjsFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
263
|
+
stack = stack.replace(new RegExp(mjsPattern, 'g'), tsFile)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
error.stack = stack
|
|
267
|
+
return error
|
|
221
268
|
}
|
|
222
269
|
|
|
223
270
|
/**
|
package/lib/workers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
|
|
|
5
5
|
import { Worker } from 'worker_threads'
|
|
6
6
|
import { EventEmitter } from 'events'
|
|
7
7
|
import ms from 'ms'
|
|
8
|
+
import merge from 'lodash.merge'
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
11
|
const __dirname = dirname(__filename)
|
|
@@ -66,21 +67,21 @@ const createWorker = (workerObject, isPoolMode = false) => {
|
|
|
66
67
|
stdout: true,
|
|
67
68
|
stderr: true,
|
|
68
69
|
})
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
// Pipe worker stdout/stderr to main process
|
|
71
72
|
if (worker.stdout) {
|
|
72
73
|
worker.stdout.setEncoding('utf8')
|
|
73
|
-
worker.stdout.on('data',
|
|
74
|
+
worker.stdout.on('data', data => {
|
|
74
75
|
process.stdout.write(data)
|
|
75
76
|
})
|
|
76
77
|
}
|
|
77
78
|
if (worker.stderr) {
|
|
78
79
|
worker.stderr.setEncoding('utf8')
|
|
79
|
-
worker.stderr.on('data',
|
|
80
|
+
worker.stderr.on('data', data => {
|
|
80
81
|
process.stderr.write(data)
|
|
81
82
|
})
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
+
|
|
84
85
|
worker.on('error', err => {
|
|
85
86
|
console.error(`[Main] Worker Error:`, err)
|
|
86
87
|
output.error(`Worker Error: ${err.stack}`)
|
|
@@ -221,13 +222,13 @@ class WorkerObject {
|
|
|
221
222
|
|
|
222
223
|
addConfig(config) {
|
|
223
224
|
const oldConfig = JSON.parse(this.options.override || '{}')
|
|
224
|
-
|
|
225
|
+
|
|
225
226
|
// Remove customLocatorStrategies from both old and new config before JSON serialization
|
|
226
227
|
// since functions cannot be serialized and will be lost, causing workers to have empty strategies
|
|
227
228
|
const configWithoutFunctions = { ...config }
|
|
228
|
-
|
|
229
|
+
|
|
229
230
|
// Clean both old and new config
|
|
230
|
-
const cleanConfig =
|
|
231
|
+
const cleanConfig = cfg => {
|
|
231
232
|
if (cfg.helpers) {
|
|
232
233
|
cfg.helpers = { ...cfg.helpers }
|
|
233
234
|
Object.keys(cfg.helpers).forEach(helperName => {
|
|
@@ -239,14 +240,12 @@ class WorkerObject {
|
|
|
239
240
|
}
|
|
240
241
|
return cfg
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
+
|
|
243
244
|
const cleanedOldConfig = cleanConfig(oldConfig)
|
|
244
245
|
const cleanedNewConfig = cleanConfig(configWithoutFunctions)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
...cleanedNewConfig,
|
|
249
|
-
}
|
|
246
|
+
|
|
247
|
+
// Deep merge configurations to preserve all helpers from base config
|
|
248
|
+
const newConfig = merge({}, cleanedOldConfig, cleanedNewConfig)
|
|
250
249
|
this.options.override = JSON.stringify(newConfig)
|
|
251
250
|
}
|
|
252
251
|
|
|
@@ -280,8 +279,8 @@ class Workers extends EventEmitter {
|
|
|
280
279
|
this.setMaxListeners(50)
|
|
281
280
|
this.codeceptPromise = initializeCodecept(config.testConfig, config.options)
|
|
282
281
|
this.codecept = null
|
|
283
|
-
this.config = config
|
|
284
|
-
this.numberOfWorkersRequested = numberOfWorkers
|
|
282
|
+
this.config = config // Save config
|
|
283
|
+
this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
|
|
285
284
|
this.options = config.options || {}
|
|
286
285
|
this.errors = []
|
|
287
286
|
this.numberOfWorkers = 0
|
|
@@ -304,11 +303,8 @@ class Workers extends EventEmitter {
|
|
|
304
303
|
// Initialize workers in these cases:
|
|
305
304
|
// 1. Positive number requested AND no manual workers pre-spawned
|
|
306
305
|
// 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned
|
|
307
|
-
const shouldAutoInit = this.workers.length === 0 && (
|
|
308
|
-
|
|
309
|
-
(this.numberOfWorkersRequested < 0 && isFunction(this.config.by))
|
|
310
|
-
)
|
|
311
|
-
|
|
306
|
+
const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by)))
|
|
307
|
+
|
|
312
308
|
if (shouldAutoInit) {
|
|
313
309
|
this._initWorkers(this.numberOfWorkersRequested, this.config)
|
|
314
310
|
}
|
|
@@ -319,7 +315,7 @@ class Workers extends EventEmitter {
|
|
|
319
315
|
this.splitTestsByGroups(numberOfWorkers, config)
|
|
320
316
|
// For function-based grouping, use the actual number of test groups created
|
|
321
317
|
const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
|
|
322
|
-
this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
|
|
318
|
+
this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
|
|
323
319
|
this.numberOfWorkers = this.workers.length
|
|
324
320
|
}
|
|
325
321
|
|
|
@@ -371,9 +367,9 @@ class Workers extends EventEmitter {
|
|
|
371
367
|
* @param {Number} numberOfWorkers
|
|
372
368
|
*/
|
|
373
369
|
createGroupsOfTests(numberOfWorkers) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
370
|
+
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
371
|
+
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
372
|
+
const files = this.codecept.testFiles
|
|
377
373
|
const mocha = Container.mocha()
|
|
378
374
|
mocha.files = files
|
|
379
375
|
mocha.loadFiles()
|
|
@@ -430,7 +426,7 @@ class Workers extends EventEmitter {
|
|
|
430
426
|
for (const file of files) {
|
|
431
427
|
this.testPool.push(file)
|
|
432
428
|
}
|
|
433
|
-
|
|
429
|
+
|
|
434
430
|
this.testPoolInitialized = true
|
|
435
431
|
}
|
|
436
432
|
|
|
@@ -443,7 +439,7 @@ class Workers extends EventEmitter {
|
|
|
443
439
|
if (!this.testPoolInitialized) {
|
|
444
440
|
this._initializeTestPool()
|
|
445
441
|
}
|
|
446
|
-
|
|
442
|
+
|
|
447
443
|
return this.testPool.shift()
|
|
448
444
|
}
|
|
449
445
|
|
|
@@ -451,9 +447,9 @@ class Workers extends EventEmitter {
|
|
|
451
447
|
* @param {Number} numberOfWorkers
|
|
452
448
|
*/
|
|
453
449
|
createGroupsOfSuites(numberOfWorkers) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
450
|
+
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
451
|
+
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
452
|
+
const files = this.codecept.testFiles
|
|
457
453
|
const groups = populateGroups(numberOfWorkers)
|
|
458
454
|
|
|
459
455
|
const mocha = Container.mocha()
|
|
@@ -494,7 +490,7 @@ class Workers extends EventEmitter {
|
|
|
494
490
|
recorder.startUnlessRunning()
|
|
495
491
|
event.dispatcher.emit(event.workers.before)
|
|
496
492
|
process.env.RUNS_WITH_WORKERS = 'true'
|
|
497
|
-
|
|
493
|
+
|
|
498
494
|
// Create workers and set up message handlers immediately (not in recorder queue)
|
|
499
495
|
// This prevents a race condition where workers start sending messages before handlers are attached
|
|
500
496
|
const workerThreads = []
|
|
@@ -503,11 +499,11 @@ class Workers extends EventEmitter {
|
|
|
503
499
|
this._listenWorkerEvents(workerThread)
|
|
504
500
|
workerThreads.push(workerThread)
|
|
505
501
|
}
|
|
506
|
-
|
|
502
|
+
|
|
507
503
|
recorder.add('workers started', () => {
|
|
508
504
|
// Workers are already running, this is just a placeholder step
|
|
509
505
|
})
|
|
510
|
-
|
|
506
|
+
|
|
511
507
|
return new Promise(resolve => {
|
|
512
508
|
this.on('end', resolve)
|
|
513
509
|
})
|
|
@@ -591,7 +587,7 @@ class Workers extends EventEmitter {
|
|
|
591
587
|
// Otherwise skip - we'll emit based on finished state
|
|
592
588
|
break
|
|
593
589
|
case event.test.passed:
|
|
594
|
-
// Skip individual passed events - we'll emit based on finished state
|
|
590
|
+
// Skip individual passed events - we'll emit based on finished state
|
|
595
591
|
break
|
|
596
592
|
case event.test.skipped:
|
|
597
593
|
this.emit(event.test.skipped, deserializeTest(message.data))
|
|
@@ -602,15 +598,15 @@ class Workers extends EventEmitter {
|
|
|
602
598
|
const data = message.data
|
|
603
599
|
const uid = data?.uid
|
|
604
600
|
const isFailed = !!data?.err || data?.state === 'failed'
|
|
605
|
-
|
|
601
|
+
|
|
606
602
|
if (uid) {
|
|
607
603
|
// Track states for each test UID
|
|
608
604
|
if (!this._testStates) this._testStates = new Map()
|
|
609
|
-
|
|
605
|
+
|
|
610
606
|
if (!this._testStates.has(uid)) {
|
|
611
607
|
this._testStates.set(uid, { states: [], lastData: data })
|
|
612
608
|
}
|
|
613
|
-
|
|
609
|
+
|
|
614
610
|
const testState = this._testStates.get(uid)
|
|
615
611
|
testState.states.push({ isFailed, data })
|
|
616
612
|
testState.lastData = data
|
|
@@ -622,7 +618,7 @@ class Workers extends EventEmitter {
|
|
|
622
618
|
this.emit(event.test.passed, deserializeTest(data))
|
|
623
619
|
}
|
|
624
620
|
}
|
|
625
|
-
|
|
621
|
+
|
|
626
622
|
this.emit(event.test.finished, deserializeTest(data))
|
|
627
623
|
}
|
|
628
624
|
break
|
|
@@ -682,11 +678,10 @@ class Workers extends EventEmitter {
|
|
|
682
678
|
// For tests with retries configured, emit all failures + final success
|
|
683
679
|
// For tests without retries, emit only final state
|
|
684
680
|
const lastState = states[states.length - 1]
|
|
685
|
-
|
|
681
|
+
|
|
686
682
|
// Check if this test had retries by looking for failure followed by success
|
|
687
|
-
const hasRetryPattern = states.length > 1 &&
|
|
688
|
-
|
|
689
|
-
|
|
683
|
+
const hasRetryPattern = states.length > 1 && states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
|
|
684
|
+
|
|
690
685
|
if (hasRetryPattern) {
|
|
691
686
|
// Emit all intermediate failures and final success for retries
|
|
692
687
|
for (const state of states) {
|