codeceptjs 4.0.1-beta.1 → 4.0.1-beta.11
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/lib/actor.js +12 -8
- package/lib/container.js +68 -33
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +19 -35
- package/lib/helper/REST.js +15 -9
- package/lib/listener/config.js +11 -3
- package/lib/step/meta.js +18 -1
- package/lib/utils/typescript.js +15 -0
- package/lib/workers.js +36 -41
- package/package.json +1 -1
package/lib/actor.js
CHANGED
|
@@ -75,7 +75,8 @@ export default function (obj = {}, container) {
|
|
|
75
75
|
if (!container) {
|
|
76
76
|
container = Container
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
// Get existing actor or create a new one
|
|
79
80
|
const actor = container.actor() || new Actor()
|
|
80
81
|
|
|
81
82
|
// load all helpers once container initialized
|
|
@@ -111,14 +112,17 @@ export default function (obj = {}, container) {
|
|
|
111
112
|
}
|
|
112
113
|
})
|
|
113
114
|
|
|
114
|
-
container.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// Update container.support.I to ensure it has the latest actor reference
|
|
116
|
+
if (!container.actor() || container.actor() !== actor) {
|
|
117
|
+
container.append({
|
|
118
|
+
support: {
|
|
119
|
+
I: actor,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
119
123
|
})
|
|
120
|
-
|
|
121
|
-
// add custom steps from actor
|
|
124
|
+
|
|
125
|
+
// add custom steps from actor immediately
|
|
122
126
|
Object.keys(obj).forEach(key => {
|
|
123
127
|
const ms = new MetaStep('I', key)
|
|
124
128
|
ms.setContext(actor)
|
package/lib/container.js
CHANGED
|
@@ -22,6 +22,7 @@ let container = {
|
|
|
22
22
|
helpers: {},
|
|
23
23
|
support: {},
|
|
24
24
|
proxySupport: {},
|
|
25
|
+
proxySupportConfig: {}, // Track config used to create proxySupport
|
|
25
26
|
plugins: {},
|
|
26
27
|
actor: null,
|
|
27
28
|
/**
|
|
@@ -67,14 +68,15 @@ class Container {
|
|
|
67
68
|
container.support = {}
|
|
68
69
|
container.helpers = await createHelpers(config.helpers || {})
|
|
69
70
|
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
|
|
70
|
-
container.
|
|
71
|
+
container.proxySupportConfig = config.include || {}
|
|
72
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
71
73
|
container.plugins = await createPlugins(config.plugins || {}, opts)
|
|
72
74
|
container.result = new Result()
|
|
73
75
|
|
|
74
76
|
// Preload includes (so proxies can expose real objects synchronously)
|
|
75
77
|
const includes = config.include || {}
|
|
76
78
|
|
|
77
|
-
//
|
|
79
|
+
// Check if custom I is provided
|
|
78
80
|
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
|
|
79
81
|
try {
|
|
80
82
|
const mod = includes.I
|
|
@@ -89,7 +91,7 @@ class Container {
|
|
|
89
91
|
throw new Error(`Could not include object I: ${e.message}`)
|
|
90
92
|
}
|
|
91
93
|
} else {
|
|
92
|
-
// Create default actor
|
|
94
|
+
// Create default actor - this sets up the callback in asyncHelperPromise
|
|
93
95
|
createActor()
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -110,6 +112,9 @@ class Container {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// Wait for all async helpers to finish loading and populate the actor
|
|
116
|
+
await asyncHelperPromise
|
|
117
|
+
|
|
113
118
|
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
114
119
|
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
115
120
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
@@ -202,19 +207,12 @@ class Container {
|
|
|
202
207
|
static append(newContainer) {
|
|
203
208
|
container = deepMerge(container, newContainer)
|
|
204
209
|
|
|
205
|
-
// If new helpers are added, set the helpers property on them
|
|
206
|
-
if (newContainer.helpers) {
|
|
207
|
-
for (const name in newContainer.helpers) {
|
|
208
|
-
if (container.helpers[name] && typeof container.helpers[name] === 'object') {
|
|
209
|
-
container.helpers[name].helpers = container.helpers
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
210
|
// If new support objects are added, update the proxy support
|
|
215
211
|
if (newContainer.support) {
|
|
216
|
-
|
|
217
|
-
container.
|
|
212
|
+
// Merge the new support config with existing config
|
|
213
|
+
container.proxySupportConfig = { ...container.proxySupportConfig, ...newContainer.support }
|
|
214
|
+
// Recreate the proxy with merged config
|
|
215
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
218
216
|
}
|
|
219
217
|
|
|
220
218
|
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
@@ -230,6 +228,7 @@ class Container {
|
|
|
230
228
|
static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
|
|
231
229
|
container.helpers = newHelpers
|
|
232
230
|
container.translation = await loadTranslation()
|
|
231
|
+
container.proxySupportConfig = newSupport
|
|
233
232
|
container.proxySupport = createSupportObjects(newSupport)
|
|
234
233
|
container.plugins = newPlugins
|
|
235
234
|
container.sharedKeys = new Set() // Clear shared keys
|
|
@@ -301,7 +300,7 @@ async function createHelpers(config) {
|
|
|
301
300
|
if (!HelperClass) {
|
|
302
301
|
const helperResult = requireHelperFromModule(helperName, config)
|
|
303
302
|
if (helperResult instanceof Promise) {
|
|
304
|
-
// Handle async ESM loading
|
|
303
|
+
// Handle async ESM loading - create placeholder
|
|
305
304
|
helpers[helperName] = {}
|
|
306
305
|
asyncHelperPromise = asyncHelperPromise
|
|
307
306
|
.then(() => helperResult)
|
|
@@ -320,8 +319,7 @@ async function createHelpers(config) {
|
|
|
320
319
|
|
|
321
320
|
checkHelperRequirements(ResolvedHelperClass)
|
|
322
321
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
323
|
-
|
|
324
|
-
debug(`helper ${helperName} async initialized`)
|
|
322
|
+
debug(`helper ${helperName} async loaded`)
|
|
325
323
|
})
|
|
326
324
|
continue
|
|
327
325
|
} else {
|
|
@@ -341,9 +339,8 @@ async function createHelpers(config) {
|
|
|
341
339
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
342
340
|
}
|
|
343
341
|
|
|
344
|
-
debug(`helper ${helperName} async initialized`)
|
|
345
|
-
|
|
346
342
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
343
|
+
debug(`helper ${helperName} async CJS loaded`)
|
|
347
344
|
})
|
|
348
345
|
|
|
349
346
|
continue
|
|
@@ -358,19 +355,18 @@ async function createHelpers(config) {
|
|
|
358
355
|
}
|
|
359
356
|
}
|
|
360
357
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
358
|
+
// Don't await here - let Container.create() handle the await
|
|
359
|
+
// This allows actor callbacks to be registered before resolution
|
|
360
|
+
asyncHelperPromise = asyncHelperPromise.then(async () => {
|
|
361
|
+
// Call _init on all helpers after they're all loaded
|
|
362
|
+
for (const name in helpers) {
|
|
363
|
+
if (helpers[name]._init) {
|
|
364
|
+
await helpers[name]._init()
|
|
365
|
+
debug(`helper ${name} _init() called`)
|
|
366
|
+
}
|
|
365
367
|
}
|
|
366
|
-
}
|
|
368
|
+
})
|
|
367
369
|
|
|
368
|
-
// Wait for async helpers and call _init
|
|
369
|
-
await asyncHelperPromise
|
|
370
|
-
|
|
371
|
-
for (const name in helpers) {
|
|
372
|
-
if (helpers[name]._init) await helpers[name]._init()
|
|
373
|
-
}
|
|
374
370
|
return helpers
|
|
375
371
|
}
|
|
376
372
|
|
|
@@ -402,20 +398,52 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
402
398
|
throw err
|
|
403
399
|
}
|
|
404
400
|
} else {
|
|
401
|
+
// Handle TypeScript files
|
|
402
|
+
let importPath = moduleName
|
|
403
|
+
let tempJsFile = null
|
|
404
|
+
const ext = path.extname(moduleName)
|
|
405
|
+
|
|
406
|
+
if (ext === '.ts') {
|
|
407
|
+
try {
|
|
408
|
+
// Use the TypeScript transpilation utility
|
|
409
|
+
const typescript = await import('typescript')
|
|
410
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
411
|
+
|
|
412
|
+
debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
|
|
413
|
+
|
|
414
|
+
importPath = tempFile
|
|
415
|
+
tempJsFile = allTempFiles
|
|
416
|
+
} catch (tsError) {
|
|
417
|
+
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
405
421
|
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
|
|
406
422
|
try {
|
|
407
423
|
// Try dynamic import for both CommonJS and ESM modules
|
|
408
|
-
const mod = await import(
|
|
424
|
+
const mod = await import(importPath)
|
|
409
425
|
if (!mod && !mod.default) {
|
|
410
426
|
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
|
|
411
427
|
}
|
|
412
428
|
HelperClass = mod.default || mod
|
|
429
|
+
|
|
430
|
+
// Clean up temp files if created
|
|
431
|
+
if (tempJsFile) {
|
|
432
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
433
|
+
cleanupTempFiles(filesToClean)
|
|
434
|
+
}
|
|
413
435
|
} catch (err) {
|
|
436
|
+
// Clean up temp files before rethrowing
|
|
437
|
+
if (tempJsFile) {
|
|
438
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
439
|
+
cleanupTempFiles(filesToClean)
|
|
440
|
+
}
|
|
441
|
+
|
|
414
442
|
if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
|
|
415
443
|
// This is an ESM module, use dynamic import
|
|
416
444
|
try {
|
|
417
445
|
const pathModule = await import('path')
|
|
418
|
-
const absolutePath = pathModule.default.resolve(
|
|
446
|
+
const absolutePath = pathModule.default.resolve(importPath)
|
|
419
447
|
const mod = await import(absolutePath)
|
|
420
448
|
HelperClass = mod.default || mod
|
|
421
449
|
debug(`helper ${helperName} loaded via ESM import`)
|
|
@@ -544,10 +572,17 @@ function createSupportObjects(config) {
|
|
|
544
572
|
return [...new Set([...keys, ...container.sharedKeys])]
|
|
545
573
|
},
|
|
546
574
|
getOwnPropertyDescriptor(target, prop) {
|
|
575
|
+
// For destructuring to work, we need to return the actual value from the getter
|
|
576
|
+
let value
|
|
577
|
+
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
578
|
+
value = container.support[prop]
|
|
579
|
+
} else {
|
|
580
|
+
value = lazyLoad(prop)
|
|
581
|
+
}
|
|
547
582
|
return {
|
|
548
583
|
enumerable: true,
|
|
549
584
|
configurable: true,
|
|
550
|
-
value:
|
|
585
|
+
value: value,
|
|
551
586
|
}
|
|
552
587
|
},
|
|
553
588
|
get(target, key) {
|
package/lib/helper/GraphQL.js
CHANGED
|
@@ -45,6 +45,8 @@ class GraphQL extends Helper {
|
|
|
45
45
|
timeout: 10000,
|
|
46
46
|
defaultHeaders: {},
|
|
47
47
|
endpoint: '',
|
|
48
|
+
onRequest: null,
|
|
49
|
+
onResponse: null,
|
|
48
50
|
}
|
|
49
51
|
this.options = Object.assign(this.options, config)
|
|
50
52
|
this.headers = { ...this.options.defaultHeaders }
|
|
@@ -87,8 +89,8 @@ class GraphQL extends Helper {
|
|
|
87
89
|
|
|
88
90
|
request.headers = { ...this.headers, ...request.headers }
|
|
89
91
|
|
|
90
|
-
if (this.
|
|
91
|
-
await this.
|
|
92
|
+
if (this.options.onRequest) {
|
|
93
|
+
await this.options.onRequest(request)
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
this.debugSection('Request', JSON.stringify(request))
|
|
@@ -102,8 +104,8 @@ class GraphQL extends Helper {
|
|
|
102
104
|
response = err.response
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
if (this.
|
|
106
|
-
await this.
|
|
107
|
+
if (this.options.onResponse) {
|
|
108
|
+
await this.options.onResponse(response)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
this.debugSection('Response', JSON.stringify(response.data))
|
|
@@ -72,8 +72,8 @@ class JSONResponse extends Helper {
|
|
|
72
72
|
if (!this.helpers[this.options.requestHelper]) {
|
|
73
73
|
throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
|
|
74
74
|
}
|
|
75
|
-
const origOnResponse = this.helpers[this.options.requestHelper].
|
|
76
|
-
this.helpers[this.options.requestHelper].
|
|
75
|
+
const origOnResponse = this.helpers[this.options.requestHelper].options.onResponse
|
|
76
|
+
this.helpers[this.options.requestHelper].options.onResponse = response => {
|
|
77
77
|
this.response = response
|
|
78
78
|
if (typeof origOnResponse === 'function') origOnResponse(response)
|
|
79
79
|
}
|
|
@@ -83,7 +83,6 @@ class JSONResponse extends Helper {
|
|
|
83
83
|
this.response = null
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
87
86
|
/**
|
|
88
87
|
* Checks that response code is equal to the provided one
|
|
89
88
|
*
|
|
@@ -372,4 +371,4 @@ class JSONResponse extends Helper {
|
|
|
372
371
|
}
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
export { JSONResponse as default }
|
|
374
|
+
export { JSONResponse, JSONResponse as default }
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -355,7 +355,7 @@ class Playwright extends Helper {
|
|
|
355
355
|
this.recordingWebSocketMessages = false
|
|
356
356
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
357
357
|
this.cdpSession = null
|
|
358
|
-
|
|
358
|
+
|
|
359
359
|
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
360
360
|
// This can happen in worker threads where config is serialized/deserialized
|
|
361
361
|
let validCustomLocators = null
|
|
@@ -367,7 +367,7 @@ class Playwright extends Helper {
|
|
|
367
367
|
validCustomLocators = config.customLocatorStrategies
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
-
|
|
370
|
+
|
|
371
371
|
this.customLocatorStrategies = validCustomLocators
|
|
372
372
|
this._customLocatorsRegistered = false
|
|
373
373
|
|
|
@@ -416,6 +416,7 @@ class Playwright extends Helper {
|
|
|
416
416
|
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
417
417
|
highlightElement: false,
|
|
418
418
|
storageState: undefined,
|
|
419
|
+
onResponse: null,
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -794,10 +795,7 @@ class Playwright extends Helper {
|
|
|
794
795
|
await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
|
|
795
796
|
}
|
|
796
797
|
// Use timeout to prevent hanging (10s should be enough for browser cleanup)
|
|
797
|
-
await Promise.race([
|
|
798
|
-
this._stopBrowser(),
|
|
799
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
|
|
800
|
-
])
|
|
798
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
|
|
801
799
|
} catch (e) {
|
|
802
800
|
console.warn('Warning during browser restart in _after:', e.message)
|
|
803
801
|
// Force cleanup even on timeout
|
|
@@ -840,10 +838,7 @@ class Playwright extends Helper {
|
|
|
840
838
|
if (this.isRunning) {
|
|
841
839
|
try {
|
|
842
840
|
// Add timeout protection to prevent hanging
|
|
843
|
-
await Promise.race([
|
|
844
|
-
this._stopBrowser(),
|
|
845
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
|
|
846
|
-
])
|
|
841
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
|
|
847
842
|
} catch (e) {
|
|
848
843
|
console.warn('Warning during suite cleanup:', e.message)
|
|
849
844
|
// Track suite cleanup failures
|
|
@@ -954,10 +949,7 @@ class Playwright extends Helper {
|
|
|
954
949
|
if (this.isRunning) {
|
|
955
950
|
try {
|
|
956
951
|
// Add timeout protection to prevent hanging
|
|
957
|
-
await Promise.race([
|
|
958
|
-
this._stopBrowser(),
|
|
959
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
|
|
960
|
-
])
|
|
952
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
|
|
961
953
|
} catch (e) {
|
|
962
954
|
console.warn('Warning during final cleanup:', e.message)
|
|
963
955
|
// Force cleanup on timeout
|
|
@@ -970,10 +962,7 @@ class Playwright extends Helper {
|
|
|
970
962
|
if (this.browser) {
|
|
971
963
|
try {
|
|
972
964
|
// Add timeout protection to prevent hanging
|
|
973
|
-
await Promise.race([
|
|
974
|
-
this._stopBrowser(),
|
|
975
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
|
|
976
|
-
])
|
|
965
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
|
|
977
966
|
} catch (e) {
|
|
978
967
|
console.warn('Warning during forced cleanup:', e.message)
|
|
979
968
|
// Force cleanup on timeout
|
|
@@ -1390,7 +1379,7 @@ class Playwright extends Helper {
|
|
|
1390
1379
|
this.context = null
|
|
1391
1380
|
this.frame = null
|
|
1392
1381
|
popupStore.clear()
|
|
1393
|
-
|
|
1382
|
+
|
|
1394
1383
|
// Remove all event listeners to prevent hanging
|
|
1395
1384
|
if (this.browser) {
|
|
1396
1385
|
try {
|
|
@@ -1399,7 +1388,7 @@ class Playwright extends Helper {
|
|
|
1399
1388
|
// Ignore errors if browser is already closed
|
|
1400
1389
|
}
|
|
1401
1390
|
}
|
|
1402
|
-
|
|
1391
|
+
|
|
1403
1392
|
if (this.options.recordHar && this.browserContext) {
|
|
1404
1393
|
try {
|
|
1405
1394
|
await this.browserContext.close()
|
|
@@ -1408,16 +1397,11 @@ class Playwright extends Helper {
|
|
|
1408
1397
|
}
|
|
1409
1398
|
}
|
|
1410
1399
|
this.browserContext = null
|
|
1411
|
-
|
|
1400
|
+
|
|
1412
1401
|
if (this.browser) {
|
|
1413
1402
|
try {
|
|
1414
1403
|
// Add timeout to prevent browser.close() from hanging indefinitely
|
|
1415
|
-
await Promise.race([
|
|
1416
|
-
this.browser.close(),
|
|
1417
|
-
new Promise((_, reject) =>
|
|
1418
|
-
setTimeout(() => reject(new Error('Browser close timeout')), 5000)
|
|
1419
|
-
)
|
|
1420
|
-
])
|
|
1404
|
+
await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))])
|
|
1421
1405
|
} catch (e) {
|
|
1422
1406
|
// Ignore errors if browser is already closed or timeout
|
|
1423
1407
|
if (!e.message?.includes('Browser close timeout')) {
|
|
@@ -1539,7 +1523,7 @@ class Playwright extends Helper {
|
|
|
1539
1523
|
acceptDownloads: true,
|
|
1540
1524
|
...this.options.emulate,
|
|
1541
1525
|
}
|
|
1542
|
-
|
|
1526
|
+
|
|
1543
1527
|
try {
|
|
1544
1528
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1545
1529
|
} catch (err) {
|
|
@@ -3183,14 +3167,14 @@ class Playwright extends Helper {
|
|
|
3183
3167
|
this.debugSection('Response', await response.text())
|
|
3184
3168
|
|
|
3185
3169
|
// hook to allow JSON response handle this
|
|
3186
|
-
if (this.
|
|
3170
|
+
if (this.options.onResponse) {
|
|
3187
3171
|
const axiosResponse = {
|
|
3188
3172
|
data: await response.json(),
|
|
3189
3173
|
status: response.status(),
|
|
3190
3174
|
statusText: response.statusText(),
|
|
3191
3175
|
headers: response.headers(),
|
|
3192
3176
|
}
|
|
3193
|
-
this.
|
|
3177
|
+
this.options.onResponse(axiosResponse)
|
|
3194
3178
|
}
|
|
3195
3179
|
|
|
3196
3180
|
return response
|
|
@@ -4337,11 +4321,11 @@ function isRoleLocatorObject(locator) {
|
|
|
4337
4321
|
*/
|
|
4338
4322
|
async function handleRoleLocator(context, locator) {
|
|
4339
4323
|
if (!isRoleLocatorObject(locator)) return null
|
|
4340
|
-
|
|
4324
|
+
|
|
4341
4325
|
const options = {}
|
|
4342
4326
|
if (locator.text) options.name = locator.text
|
|
4343
4327
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4344
|
-
|
|
4328
|
+
|
|
4345
4329
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4346
4330
|
}
|
|
4347
4331
|
|
|
@@ -4350,7 +4334,7 @@ async function findElements(matcher, locator) {
|
|
|
4350
4334
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4351
4335
|
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4352
4336
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4353
|
-
|
|
4337
|
+
|
|
4354
4338
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4355
4339
|
if (isVueLocator) return findVue(matcher, locator)
|
|
4356
4340
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
@@ -4391,7 +4375,7 @@ async function findCustomElements(matcher, locator) {
|
|
|
4391
4375
|
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4392
4376
|
// and persists in every worker thread instance
|
|
4393
4377
|
let strategyFunction = null
|
|
4394
|
-
|
|
4378
|
+
|
|
4395
4379
|
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4396
4380
|
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4397
4381
|
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
@@ -4967,7 +4951,7 @@ async function refreshContextSession() {
|
|
|
4967
4951
|
this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
|
|
4968
4952
|
return
|
|
4969
4953
|
}
|
|
4970
|
-
|
|
4954
|
+
|
|
4971
4955
|
const currentUrl = await this.grabCurrentUrl()
|
|
4972
4956
|
|
|
4973
4957
|
if (currentUrl.startsWith('http')) {
|
package/lib/helper/REST.js
CHANGED
|
@@ -94,7 +94,9 @@ const config = {}
|
|
|
94
94
|
class REST extends Helper {
|
|
95
95
|
constructor(config) {
|
|
96
96
|
super(config)
|
|
97
|
-
|
|
97
|
+
|
|
98
|
+
// Set defaults first
|
|
99
|
+
const defaults = {
|
|
98
100
|
timeout: 10000,
|
|
99
101
|
defaultHeaders: {},
|
|
100
102
|
endpoint: '',
|
|
@@ -103,15 +105,16 @@ class REST extends Helper {
|
|
|
103
105
|
onResponse: null,
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// Merge config with defaults
|
|
109
|
+
this._setConfig(config)
|
|
110
|
+
this.options = { ...defaults, ...this.options }
|
|
111
|
+
|
|
106
112
|
if (this.options.maxContentLength) {
|
|
107
113
|
const maxContentLength = this.options.maxUploadFileSize * 1024 * 1024
|
|
108
114
|
this.options.maxContentLength = maxContentLength
|
|
109
115
|
this.options.maxBodyLength = maxContentLength
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
// override defaults with config
|
|
113
|
-
this._setConfig(config)
|
|
114
|
-
|
|
115
118
|
this.headers = { ...this.options.defaultHeaders }
|
|
116
119
|
|
|
117
120
|
// Create an agent with SSL certificate
|
|
@@ -215,8 +218,9 @@ class REST extends Helper {
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
const onRequest = this.options.onRequest || this.config.onRequest
|
|
222
|
+
if (onRequest) {
|
|
223
|
+
await onRequest(request)
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
try {
|
|
@@ -245,8 +249,9 @@ class REST extends Helper {
|
|
|
245
249
|
}
|
|
246
250
|
response = err.response
|
|
247
251
|
}
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
const onResponse = this.options.onResponse || this.config.onResponse
|
|
253
|
+
if (onResponse) {
|
|
254
|
+
await onResponse(response)
|
|
250
255
|
}
|
|
251
256
|
try {
|
|
252
257
|
this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data))
|
|
@@ -468,7 +473,8 @@ class REST extends Helper {
|
|
|
468
473
|
export { REST as default }
|
|
469
474
|
|
|
470
475
|
function curlize(request) {
|
|
471
|
-
|
|
476
|
+
// Guard access to nested properties safely in case request.data is undefined
|
|
477
|
+
if ((request.data?.constructor?.name || '').toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'
|
|
472
478
|
let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", '')
|
|
473
479
|
|
|
474
480
|
if (request.headers) {
|
package/lib/listener/config.js
CHANGED
|
@@ -12,15 +12,23 @@ export default function () {
|
|
|
12
12
|
return
|
|
13
13
|
}
|
|
14
14
|
global.__codeceptConfigListenerInitialized = true
|
|
15
|
-
|
|
16
|
-
const helpers = global.container.helpers()
|
|
17
15
|
|
|
18
16
|
enableDynamicConfigFor('suite')
|
|
19
17
|
enableDynamicConfigFor('test')
|
|
20
18
|
|
|
21
19
|
function enableDynamicConfigFor(type) {
|
|
22
20
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
|
+
// Get helpers dynamically at runtime, not at initialization time
|
|
22
|
+
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
+
const helpers = global.container.helpers()
|
|
24
|
+
|
|
23
25
|
function updateHelperConfig(helper, config) {
|
|
26
|
+
// Guard against undefined or invalid helpers
|
|
27
|
+
if (!helper || !helper.constructor) {
|
|
28
|
+
output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
const oldConfig = deepClone(helper.options)
|
|
25
33
|
try {
|
|
26
34
|
helper._setConfig(deepMerge(deepClone(oldConfig), config))
|
|
@@ -41,7 +49,7 @@ export default function () {
|
|
|
41
49
|
for (let name in context.config) {
|
|
42
50
|
const config = context.config[name]
|
|
43
51
|
if (name === '0') {
|
|
44
|
-
// first helper
|
|
52
|
+
// first helper - get dynamically
|
|
45
53
|
name = Object.keys(helpers)[0]
|
|
46
54
|
}
|
|
47
55
|
const helper = helpers[name]
|
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/typescript.js
CHANGED
|
@@ -168,6 +168,7 @@ const __dirname = __dirname_fn(__filename);
|
|
|
168
168
|
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
|
|
169
169
|
(match, importPath) => {
|
|
170
170
|
let resolvedPath = path.resolve(fileBaseDir, importPath)
|
|
171
|
+
const originalExt = path.extname(importPath)
|
|
171
172
|
|
|
172
173
|
// Handle .js extension that might be .ts
|
|
173
174
|
if (resolvedPath.endsWith('.js')) {
|
|
@@ -181,6 +182,8 @@ const __dirname = __dirname_fn(__filename);
|
|
|
181
182
|
}
|
|
182
183
|
return `from '${relPath}'`
|
|
183
184
|
}
|
|
185
|
+
// Keep .js extension as-is (might be a real .js file)
|
|
186
|
+
return match
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// Try with .ts extension
|
|
@@ -197,6 +200,18 @@ const __dirname = __dirname_fn(__filename);
|
|
|
197
200
|
return `from '${relPath}'`
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
// If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json)
|
|
204
|
+
// add .js for ESM compatibility
|
|
205
|
+
// This handles cases where:
|
|
206
|
+
// 1. Import has no real extension (e.g., "./utils" or "./helper")
|
|
207
|
+
// 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper")
|
|
208
|
+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
|
|
209
|
+
const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
|
|
210
|
+
|
|
211
|
+
if (!hasStandardExtension) {
|
|
212
|
+
return match.replace(importPath, importPath + '.js')
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
// Otherwise, keep the import as-is
|
|
201
216
|
return match
|
|
202
217
|
}
|
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) {
|