codeceptjs 4.0.1-beta.2 → 4.0.1-beta.21
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/container.js +65 -17
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +97 -113
- package/lib/helper/REST.js +13 -8
- package/lib/helper/extras/PlaywrightLocator.js +13 -34
- package/lib/listener/config.js +11 -3
- package/lib/locator.js +74 -30
- package/lib/mocha/factory.js +2 -27
- package/lib/step/meta.js +18 -1
- package/lib/utils/loaderCheck.js +13 -3
- package/lib/utils/typescript.js +22 -2
- package/lib/workers.js +36 -41
- package/package.json +16 -16
- package/typings/promiseBasedTypes.d.ts +3974 -5520
- package/typings/types.d.ts +4146 -5821
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
package/lib/locator.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createRequire } from 'module'
|
|
|
5
5
|
const require = createRequire(import.meta.url)
|
|
6
6
|
let cssToXPath
|
|
7
7
|
|
|
8
|
-
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', '
|
|
8
|
+
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'role']
|
|
9
9
|
/** @class */
|
|
10
10
|
class Locator {
|
|
11
11
|
/**
|
|
@@ -24,19 +24,16 @@ class Locator {
|
|
|
24
24
|
*/
|
|
25
25
|
this.strict = false
|
|
26
26
|
|
|
27
|
+
if (typeof locator === 'string' && this.parsedJsonAsString(locator)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
if (typeof locator === 'object') {
|
|
28
32
|
if (locator.constructor.name === 'Locator') {
|
|
29
33
|
Object.assign(this, locator)
|
|
30
34
|
return
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
this.locator = locator
|
|
34
|
-
this.type = Object.keys(locator)[0]
|
|
35
|
-
this.value = locator[this.type]
|
|
36
|
-
this.strict = true
|
|
37
|
-
|
|
38
|
-
Locator.filters.forEach(f => f(locator, this))
|
|
39
|
-
|
|
36
|
+
this._applyObjectLocator(locator)
|
|
40
37
|
return
|
|
41
38
|
}
|
|
42
39
|
|
|
@@ -53,8 +50,9 @@ class Locator {
|
|
|
53
50
|
if (isShadow(locator)) {
|
|
54
51
|
this.type = 'shadow'
|
|
55
52
|
}
|
|
56
|
-
if (
|
|
57
|
-
|
|
53
|
+
if (isReactVueLocator(locator)) {
|
|
54
|
+
// React/Vue locators - keep as fuzzy type, helpers will handle them specially
|
|
55
|
+
this.type = 'fuzzy'
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
Locator.filters.forEach(f => f(locator, this))
|
|
@@ -76,8 +74,6 @@ class Locator {
|
|
|
76
74
|
return this.value
|
|
77
75
|
case 'shadow':
|
|
78
76
|
return { shadow: this.value }
|
|
79
|
-
case 'pw':
|
|
80
|
-
return { pw: this.value }
|
|
81
77
|
case 'role':
|
|
82
78
|
return `[role="${this.value}"]`
|
|
83
79
|
}
|
|
@@ -86,14 +82,52 @@ class Locator {
|
|
|
86
82
|
|
|
87
83
|
toStrict() {
|
|
88
84
|
if (!this.type) return null
|
|
85
|
+
if (this.type === 'role' && this.locator) {
|
|
86
|
+
return this.locator
|
|
87
|
+
}
|
|
89
88
|
return { [this.type]: this.value }
|
|
90
89
|
}
|
|
91
90
|
|
|
91
|
+
parsedJsonAsString(locator) {
|
|
92
|
+
if (typeof locator !== 'string') {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const trimmed = locator.trim()
|
|
97
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(trimmed)
|
|
103
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
104
|
+
this._applyObjectLocator(parsed)
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_applyObjectLocator(locator) {
|
|
113
|
+
this.strict = true
|
|
114
|
+
this.locator = locator
|
|
115
|
+
const keys = Object.keys(locator)
|
|
116
|
+
const [type] = keys
|
|
117
|
+
this.type = type
|
|
118
|
+
this.value = keys.length > 1 ? locator : locator[type]
|
|
119
|
+
Locator.filters.forEach(f => f(locator, this))
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
/**
|
|
93
123
|
* @returns {string}
|
|
94
124
|
*/
|
|
95
125
|
toString() {
|
|
96
|
-
|
|
126
|
+
if (this.output) return this.output
|
|
127
|
+
if (this.locator && this.value === this.locator) {
|
|
128
|
+
return JSON.stringify(this.locator)
|
|
129
|
+
}
|
|
130
|
+
return `{${this.type}: ${this.value}}`
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
/**
|
|
@@ -127,17 +161,27 @@ class Locator {
|
|
|
127
161
|
/**
|
|
128
162
|
* @returns {boolean}
|
|
129
163
|
*/
|
|
130
|
-
|
|
131
|
-
return this.type === '
|
|
164
|
+
isRole() {
|
|
165
|
+
return this.type === 'role'
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
/**
|
|
135
|
-
* @returns {
|
|
169
|
+
* @returns {{role: string, options: object}|null}
|
|
136
170
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
getRoleOptions() {
|
|
172
|
+
if (!this.isRole()) return null
|
|
173
|
+
const data = this.locator && typeof this.locator === 'object' ? this.locator : { role: this.value }
|
|
174
|
+
const { role, text, name, exact, includeHidden, ...rest } = data
|
|
175
|
+
let options = { ...rest }
|
|
176
|
+
const accessibleName = name ?? text
|
|
177
|
+
if (accessibleName !== undefined) options.name = accessibleName
|
|
178
|
+
if (exact !== undefined) options.exact = exact
|
|
179
|
+
if (includeHidden !== undefined) options.includeHidden = includeHidden
|
|
180
|
+
if (Object.keys(options).length === 0) options = undefined
|
|
181
|
+
return { role, options }
|
|
139
182
|
}
|
|
140
183
|
|
|
184
|
+
|
|
141
185
|
/**
|
|
142
186
|
* @returns {boolean}
|
|
143
187
|
*/
|
|
@@ -404,6 +448,16 @@ Locator.build = locator => {
|
|
|
404
448
|
return new Locator(locator, 'css')
|
|
405
449
|
}
|
|
406
450
|
|
|
451
|
+
/**
|
|
452
|
+
* @param {CodeceptJS.LocatorOrString|Locator} locator
|
|
453
|
+
* @param {string} [defaultType]
|
|
454
|
+
* @returns {Locator}
|
|
455
|
+
*/
|
|
456
|
+
Locator.from = (locator, defaultType = '') => {
|
|
457
|
+
if (locator instanceof Locator) return locator
|
|
458
|
+
return new Locator(locator, defaultType)
|
|
459
|
+
}
|
|
460
|
+
|
|
407
461
|
/**
|
|
408
462
|
* Filters to modify locators
|
|
409
463
|
* @type {Array<function(CodeceptJS.LocatorOrString, Locator): void>}
|
|
@@ -604,20 +658,10 @@ function removePrefix(xpath) {
|
|
|
604
658
|
* @param {string} locator
|
|
605
659
|
* @returns {boolean}
|
|
606
660
|
*/
|
|
607
|
-
function
|
|
661
|
+
function isReactVueLocator(locator) {
|
|
608
662
|
return locator.includes('_react') || locator.includes('_vue')
|
|
609
663
|
}
|
|
610
664
|
|
|
611
|
-
/**
|
|
612
|
-
* @private
|
|
613
|
-
* check if the locator is a role locator
|
|
614
|
-
* @param {{role: string}} locator
|
|
615
|
-
* @returns {boolean}
|
|
616
|
-
*/
|
|
617
|
-
function isRoleLocator(locator) {
|
|
618
|
-
return locator.role !== undefined && typeof locator.role === 'string' && Object.keys(locator).length >= 1
|
|
619
|
-
}
|
|
620
|
-
|
|
621
665
|
/**
|
|
622
666
|
* @private
|
|
623
667
|
* @param {CodeceptJS.LocatorOrString} locator
|
package/lib/mocha/factory.js
CHANGED
|
@@ -62,34 +62,9 @@ class MochaFactory {
|
|
|
62
62
|
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
|
|
63
63
|
this.files = this.files.filter(file => !file.match(/\.feature$/))
|
|
64
64
|
|
|
65
|
-
// Load JavaScript test files using
|
|
65
|
+
// Load JavaScript test files using original loadFiles
|
|
66
66
|
if (jsFiles.length > 0) {
|
|
67
|
-
|
|
68
|
-
// Try original loadFiles first for compatibility
|
|
69
|
-
originalLoadFiles.call(this, fn)
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// If original loadFiles fails, load ESM files manually
|
|
72
|
-
if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
|
|
73
|
-
// Load ESM files by importing them synchronously using top-level await workaround
|
|
74
|
-
for (const file of jsFiles) {
|
|
75
|
-
try {
|
|
76
|
-
// Convert file path to file:// URL for dynamic import
|
|
77
|
-
const fileUrl = `file://${file}`
|
|
78
|
-
// Use import() but don't await it - let it load in the background
|
|
79
|
-
import(fileUrl).catch(importErr => {
|
|
80
|
-
// If dynamic import fails, the file may have syntax errors or other issues
|
|
81
|
-
console.error(`Failed to load test file ${file}:`, importErr.message)
|
|
82
|
-
})
|
|
83
|
-
if (fn) fn()
|
|
84
|
-
} catch (fileErr) {
|
|
85
|
-
console.error(`Error processing test file ${file}:`, fileErr.message)
|
|
86
|
-
if (fn) fn(fileErr)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
throw e
|
|
91
|
-
}
|
|
92
|
-
}
|
|
67
|
+
originalLoadFiles.call(this, fn)
|
|
93
68
|
}
|
|
94
69
|
|
|
95
70
|
// add ids for each test and check uniqueness
|
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/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
|
@@ -142,8 +142,13 @@ const __dirname = __dirname_fn(__filename);
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
146
|
-
|
|
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
|
|
@@ -168,6 +173,7 @@ const __dirname = __dirname_fn(__filename);
|
|
|
168
173
|
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
|
|
169
174
|
(match, importPath) => {
|
|
170
175
|
let resolvedPath = path.resolve(fileBaseDir, importPath)
|
|
176
|
+
const originalExt = path.extname(importPath)
|
|
171
177
|
|
|
172
178
|
// Handle .js extension that might be .ts
|
|
173
179
|
if (resolvedPath.endsWith('.js')) {
|
|
@@ -181,6 +187,8 @@ 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
|
|
@@ -197,6 +205,18 @@ const __dirname = __dirname_fn(__filename);
|
|
|
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
|
}
|
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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "4.0.1-beta.
|
|
3
|
+
"version": "4.0.1-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Supercharged End 2 End Testing Framework for NodeJS",
|
|
6
6
|
"keywords": [
|
|
@@ -88,17 +88,17 @@
|
|
|
88
88
|
"@codeceptjs/configure": "1.0.6",
|
|
89
89
|
"@codeceptjs/helper": "2.0.4",
|
|
90
90
|
"@cucumber/cucumber-expressions": "18",
|
|
91
|
-
"@cucumber/gherkin": "
|
|
92
|
-
"@cucumber/messages": "
|
|
91
|
+
"@cucumber/gherkin": "37.0.0",
|
|
92
|
+
"@cucumber/messages": "31.0.0",
|
|
93
93
|
"@xmldom/xmldom": "0.9.8",
|
|
94
94
|
"acorn": "8.15.0",
|
|
95
95
|
"ai": "^5.0.60",
|
|
96
96
|
"arrify": "3.0.0",
|
|
97
|
-
"axios": "1.
|
|
97
|
+
"axios": "1.13.2",
|
|
98
98
|
"chalk": "4.1.2",
|
|
99
99
|
"cheerio": "^1.0.0",
|
|
100
100
|
"chokidar": "^4.0.3",
|
|
101
|
-
"commander": "
|
|
101
|
+
"commander": "14.0.2",
|
|
102
102
|
"cross-spawn": "7.0.6",
|
|
103
103
|
"css-to-xpath": "0.1.0",
|
|
104
104
|
"csstoxpath": "1.6.0",
|
|
@@ -108,11 +108,11 @@
|
|
|
108
108
|
"fn-args": "4.0.0",
|
|
109
109
|
"fs-extra": "11.3.2",
|
|
110
110
|
"fuse.js": "^7.0.0",
|
|
111
|
-
"glob": ">=9.0.0 <
|
|
111
|
+
"glob": ">=9.0.0 <14",
|
|
112
112
|
"html-minifier-terser": "7.2.0",
|
|
113
113
|
"inquirer": "^8.2.7",
|
|
114
114
|
"invisi-data": "^1.0.0",
|
|
115
|
-
"joi": "18.0.
|
|
115
|
+
"joi": "18.0.2",
|
|
116
116
|
"js-beautify": "1.15.4",
|
|
117
117
|
"lodash.clonedeep": "4.5.0",
|
|
118
118
|
"lodash.merge": "4.6.2",
|
|
@@ -144,16 +144,16 @@
|
|
|
144
144
|
"@pollyjs/adapter-puppeteer": "6.0.6",
|
|
145
145
|
"@pollyjs/core": "6.0.6",
|
|
146
146
|
"@testomatio/reporter": "^2.3.1",
|
|
147
|
-
"@types/chai": "5.2.
|
|
147
|
+
"@types/chai": "5.2.3",
|
|
148
148
|
"@types/inquirer": "9.0.9",
|
|
149
149
|
"@types/node": "^24.9.2",
|
|
150
150
|
"@wdio/sauce-service": "9.12.5",
|
|
151
151
|
"@wdio/selenium-standalone-service": "8.15.0",
|
|
152
|
-
"@wdio/utils": "9.
|
|
152
|
+
"@wdio/utils": "9.21.0",
|
|
153
153
|
"@xmldom/xmldom": "0.9.8",
|
|
154
154
|
"bunosh": "latest",
|
|
155
|
-
"chai": "^
|
|
156
|
-
"chai-as-promised": "
|
|
155
|
+
"chai": "^6.2.1",
|
|
156
|
+
"chai-as-promised": "^8.0.2",
|
|
157
157
|
"chai-subset": "1.6.0",
|
|
158
158
|
"documentation": "14.0.3",
|
|
159
159
|
"electron": "38.2.0",
|
|
@@ -162,8 +162,8 @@
|
|
|
162
162
|
"eslint-plugin-mocha": "11.1.0",
|
|
163
163
|
"expect": "30.2.0",
|
|
164
164
|
"express": "^5.1.0",
|
|
165
|
-
"globals": "16.
|
|
166
|
-
"graphql": "16.
|
|
165
|
+
"globals": "16.5.0",
|
|
166
|
+
"graphql": "16.12.0",
|
|
167
167
|
"graphql-tag": "^2.12.6",
|
|
168
168
|
"husky": "9.1.7",
|
|
169
169
|
"jsdoc": "^3.6.11",
|
|
@@ -172,19 +172,19 @@
|
|
|
172
172
|
"mochawesome": "^7.1.3",
|
|
173
173
|
"playwright": "1.55.1",
|
|
174
174
|
"prettier": "^3.3.2",
|
|
175
|
-
"puppeteer": "24.
|
|
175
|
+
"puppeteer": "24.33.0",
|
|
176
176
|
"qrcode-terminal": "0.12.0",
|
|
177
177
|
"rosie": "2.1.1",
|
|
178
178
|
"runok": "^0.9.3",
|
|
179
179
|
"semver": "7.7.3",
|
|
180
180
|
"sinon": "21.0.0",
|
|
181
|
-
"sinon-chai": "
|
|
181
|
+
"sinon-chai": "^4.0.1",
|
|
182
182
|
"ts-morph": "27.0.2",
|
|
183
183
|
"ts-node": "10.9.2",
|
|
184
184
|
"tsd": "^0.33.0",
|
|
185
185
|
"tsd-jsdoc": "2.5.0",
|
|
186
186
|
"tsx": "^4.19.2",
|
|
187
|
-
"typedoc": "0.28.
|
|
187
|
+
"typedoc": "0.28.15",
|
|
188
188
|
"typedoc-plugin-markdown": "4.9.0",
|
|
189
189
|
"typescript": "5.8.3",
|
|
190
190
|
"wdio-docker-service": "3.2.1",
|