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/helper/Playwright.js
CHANGED
|
@@ -44,6 +44,36 @@ if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
|
44
44
|
global.__playwrightSelectorsRegistered = false
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Playwright selector engine factory for a custom locator strategy.
|
|
49
|
+
* @param {string} name - Strategy name for error messages
|
|
50
|
+
* @param {Function} func - The locator function (selector, root) => Element|Element[]
|
|
51
|
+
* @returns {Function} Selector engine factory
|
|
52
|
+
*/
|
|
53
|
+
function createCustomSelectorEngine(name, func) {
|
|
54
|
+
return () => ({
|
|
55
|
+
create: () => null,
|
|
56
|
+
query(root, selector) {
|
|
57
|
+
if (!root) return null
|
|
58
|
+
try {
|
|
59
|
+
const result = func(selector, root)
|
|
60
|
+
return Array.isArray(result) ? result[0] : result
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
queryAll(root, selector) {
|
|
66
|
+
if (!root) return []
|
|
67
|
+
try {
|
|
68
|
+
const result = func(selector, root)
|
|
69
|
+
return Array.isArray(result) ? result : result ? [result] : []
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
const popupStore = new Popup()
|
|
48
78
|
const consoleLogStore = new Console()
|
|
49
79
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
@@ -355,26 +385,16 @@ class Playwright extends Helper {
|
|
|
355
385
|
this.recordingWebSocketMessages = false
|
|
356
386
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
357
387
|
this.cdpSession = null
|
|
358
|
-
|
|
388
|
+
|
|
359
389
|
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
360
390
|
// This can happen in worker threads where config is serialized/deserialized
|
|
361
|
-
|
|
362
|
-
if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
|
|
363
|
-
// Check if it's an empty array or object with no function properties
|
|
364
|
-
const entries = Object.entries(config.customLocatorStrategies)
|
|
365
|
-
const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
|
|
366
|
-
if (hasFunctions) {
|
|
367
|
-
validCustomLocators = config.customLocatorStrategies
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this.customLocatorStrategies = validCustomLocators
|
|
391
|
+
this.customLocatorStrategies = this._parseCustomLocatorStrategies(config.customLocatorStrategies)
|
|
372
392
|
this._customLocatorsRegistered = false
|
|
373
393
|
|
|
374
394
|
// Add custom locator strategies to global registry for early registration
|
|
375
395
|
if (this.customLocatorStrategies) {
|
|
376
|
-
for (const [
|
|
377
|
-
globalCustomLocatorStrategies.set(
|
|
396
|
+
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
397
|
+
globalCustomLocatorStrategies.set(name, func)
|
|
378
398
|
}
|
|
379
399
|
}
|
|
380
400
|
|
|
@@ -416,6 +436,7 @@ class Playwright extends Helper {
|
|
|
416
436
|
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
417
437
|
highlightElement: false,
|
|
418
438
|
storageState: undefined,
|
|
439
|
+
onResponse: null,
|
|
419
440
|
}
|
|
420
441
|
|
|
421
442
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -564,54 +585,23 @@ class Playwright extends Helper {
|
|
|
564
585
|
}
|
|
565
586
|
|
|
566
587
|
// Register all custom locator strategies from the global registry
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const createCustomEngine = ((name, func) => {
|
|
573
|
-
return () => {
|
|
574
|
-
return {
|
|
575
|
-
create() {
|
|
576
|
-
return null
|
|
577
|
-
},
|
|
578
|
-
query(root, selector) {
|
|
579
|
-
try {
|
|
580
|
-
if (!root) return null
|
|
581
|
-
const result = func(selector, root)
|
|
582
|
-
return Array.isArray(result) ? result[0] : result
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
585
|
-
return null
|
|
586
|
-
}
|
|
587
|
-
},
|
|
588
|
-
queryAll(root, selector) {
|
|
589
|
-
try {
|
|
590
|
-
if (!root) return []
|
|
591
|
-
const result = func(selector, root)
|
|
592
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
593
|
-
} catch (error) {
|
|
594
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
595
|
-
return []
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
})(strategyName, strategyFunction)
|
|
588
|
+
await this._registerGlobalCustomLocators()
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.warn(e)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
601
593
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
594
|
+
async _registerGlobalCustomLocators() {
|
|
595
|
+
for (const [name, func] of globalCustomLocatorStrategies.entries()) {
|
|
596
|
+
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
597
|
+
try {
|
|
598
|
+
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
599
|
+
registeredCustomLocatorStrategies.add(name)
|
|
600
|
+
} catch (e) {
|
|
601
|
+
if (!e.message.includes('already registered')) {
|
|
602
|
+
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
611
603
|
}
|
|
612
604
|
}
|
|
613
|
-
} catch (e) {
|
|
614
|
-
console.warn(e)
|
|
615
605
|
}
|
|
616
606
|
}
|
|
617
607
|
|
|
@@ -794,10 +784,7 @@ class Playwright extends Helper {
|
|
|
794
784
|
await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
|
|
795
785
|
}
|
|
796
786
|
// 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
|
-
])
|
|
787
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
|
|
801
788
|
} catch (e) {
|
|
802
789
|
console.warn('Warning during browser restart in _after:', e.message)
|
|
803
790
|
// Force cleanup even on timeout
|
|
@@ -840,10 +827,7 @@ class Playwright extends Helper {
|
|
|
840
827
|
if (this.isRunning) {
|
|
841
828
|
try {
|
|
842
829
|
// 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
|
-
])
|
|
830
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
|
|
847
831
|
} catch (e) {
|
|
848
832
|
console.warn('Warning during suite cleanup:', e.message)
|
|
849
833
|
// Track suite cleanup failures
|
|
@@ -954,10 +938,7 @@ class Playwright extends Helper {
|
|
|
954
938
|
if (this.isRunning) {
|
|
955
939
|
try {
|
|
956
940
|
// 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
|
-
])
|
|
941
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
|
|
961
942
|
} catch (e) {
|
|
962
943
|
console.warn('Warning during final cleanup:', e.message)
|
|
963
944
|
// Force cleanup on timeout
|
|
@@ -970,10 +951,7 @@ class Playwright extends Helper {
|
|
|
970
951
|
if (this.browser) {
|
|
971
952
|
try {
|
|
972
953
|
// 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
|
-
])
|
|
954
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
|
|
977
955
|
} catch (e) {
|
|
978
956
|
console.warn('Warning during forced cleanup:', e.message)
|
|
979
957
|
// Force cleanup on timeout
|
|
@@ -1288,28 +1266,31 @@ class Playwright extends Helper {
|
|
|
1288
1266
|
return this.browser
|
|
1289
1267
|
}
|
|
1290
1268
|
|
|
1269
|
+
_hasCustomLocatorStrategies() {
|
|
1270
|
+
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
_parseCustomLocatorStrategies(strategies) {
|
|
1274
|
+
if (typeof strategies !== 'object' || strategies === null) return null
|
|
1275
|
+
const hasValidFunctions = Object.values(strategies).some(v => typeof v === 'function')
|
|
1276
|
+
return hasValidFunctions ? strategies : null
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1291
1279
|
_lookupCustomLocator(customStrategy) {
|
|
1292
|
-
if (
|
|
1293
|
-
return null
|
|
1294
|
-
}
|
|
1280
|
+
if (!this._hasCustomLocatorStrategies()) return null
|
|
1295
1281
|
const strategy = this.customLocatorStrategies[customStrategy]
|
|
1296
1282
|
return typeof strategy === 'function' ? strategy : null
|
|
1297
1283
|
}
|
|
1298
1284
|
|
|
1299
1285
|
_isCustomLocator(locator) {
|
|
1300
1286
|
const locatorObj = new Locator(locator)
|
|
1301
|
-
if (locatorObj.isCustom())
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return true
|
|
1305
|
-
}
|
|
1306
|
-
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1307
|
-
}
|
|
1308
|
-
return false
|
|
1287
|
+
if (!locatorObj.isCustom()) return false
|
|
1288
|
+
if (this._lookupCustomLocator(locatorObj.type)) return true
|
|
1289
|
+
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1309
1290
|
}
|
|
1310
1291
|
|
|
1311
1292
|
_isCustomLocatorStrategyDefined() {
|
|
1312
|
-
return
|
|
1293
|
+
return this._hasCustomLocatorStrategies()
|
|
1313
1294
|
}
|
|
1314
1295
|
|
|
1315
1296
|
/**
|
|
@@ -1332,49 +1313,16 @@ class Playwright extends Helper {
|
|
|
1332
1313
|
}
|
|
1333
1314
|
|
|
1334
1315
|
async _registerCustomLocatorStrategies() {
|
|
1335
|
-
if (!this.
|
|
1336
|
-
|
|
1337
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
1338
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
1339
|
-
try {
|
|
1340
|
-
const createCustomEngine = ((name, func) => {
|
|
1341
|
-
return () => {
|
|
1342
|
-
return {
|
|
1343
|
-
create(root, target) {
|
|
1344
|
-
return null
|
|
1345
|
-
},
|
|
1346
|
-
query(root, selector) {
|
|
1347
|
-
try {
|
|
1348
|
-
if (!root) return null
|
|
1349
|
-
const result = func(selector, root)
|
|
1350
|
-
return Array.isArray(result) ? result[0] : result
|
|
1351
|
-
} catch (error) {
|
|
1352
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1353
|
-
return null
|
|
1354
|
-
}
|
|
1355
|
-
},
|
|
1356
|
-
queryAll(root, selector) {
|
|
1357
|
-
try {
|
|
1358
|
-
if (!root) return []
|
|
1359
|
-
const result = func(selector, root)
|
|
1360
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1363
|
-
return []
|
|
1364
|
-
}
|
|
1365
|
-
},
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
})(strategyName, strategyFunction)
|
|
1316
|
+
if (!this._hasCustomLocatorStrategies()) return
|
|
1369
1317
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1318
|
+
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
1319
|
+
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
1320
|
+
try {
|
|
1321
|
+
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
1322
|
+
registeredCustomLocatorStrategies.add(name)
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
if (!e.message.includes('already registered')) {
|
|
1325
|
+
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
1378
1326
|
}
|
|
1379
1327
|
}
|
|
1380
1328
|
}
|
|
@@ -1390,7 +1338,7 @@ class Playwright extends Helper {
|
|
|
1390
1338
|
this.context = null
|
|
1391
1339
|
this.frame = null
|
|
1392
1340
|
popupStore.clear()
|
|
1393
|
-
|
|
1341
|
+
|
|
1394
1342
|
// Remove all event listeners to prevent hanging
|
|
1395
1343
|
if (this.browser) {
|
|
1396
1344
|
try {
|
|
@@ -1399,7 +1347,8 @@ class Playwright extends Helper {
|
|
|
1399
1347
|
// Ignore errors if browser is already closed
|
|
1400
1348
|
}
|
|
1401
1349
|
}
|
|
1402
|
-
|
|
1350
|
+
|
|
1351
|
+
// Close browserContext if recordHar is enabled
|
|
1403
1352
|
if (this.options.recordHar && this.browserContext) {
|
|
1404
1353
|
try {
|
|
1405
1354
|
await this.browserContext.close()
|
|
@@ -1408,22 +1357,17 @@ class Playwright extends Helper {
|
|
|
1408
1357
|
}
|
|
1409
1358
|
}
|
|
1410
1359
|
this.browserContext = null
|
|
1411
|
-
|
|
1360
|
+
|
|
1361
|
+
// Initiate browser close without waiting for it to complete
|
|
1362
|
+
// The browser process will be cleaned up when the Node process exits
|
|
1412
1363
|
if (this.browser) {
|
|
1413
1364
|
try {
|
|
1414
|
-
//
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
setTimeout(() => reject(new Error('Browser close timeout')), 5000)
|
|
1419
|
-
)
|
|
1420
|
-
])
|
|
1365
|
+
// Fire and forget - don't wait for close to complete
|
|
1366
|
+
this.browser.close().catch(() => {
|
|
1367
|
+
// Silently ignore any errors during async close
|
|
1368
|
+
})
|
|
1421
1369
|
} catch (e) {
|
|
1422
|
-
// Ignore
|
|
1423
|
-
if (!e.message?.includes('Browser close timeout')) {
|
|
1424
|
-
// Non-timeout error, can be ignored as well
|
|
1425
|
-
}
|
|
1426
|
-
// Force cleanup even on error
|
|
1370
|
+
// Ignore any synchronous errors
|
|
1427
1371
|
}
|
|
1428
1372
|
}
|
|
1429
1373
|
this.browser = null
|
|
@@ -1539,7 +1483,7 @@ class Playwright extends Helper {
|
|
|
1539
1483
|
acceptDownloads: true,
|
|
1540
1484
|
...this.options.emulate,
|
|
1541
1485
|
}
|
|
1542
|
-
|
|
1486
|
+
|
|
1543
1487
|
try {
|
|
1544
1488
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1545
1489
|
} catch (err) {
|
|
@@ -3183,14 +3127,14 @@ class Playwright extends Helper {
|
|
|
3183
3127
|
this.debugSection('Response', await response.text())
|
|
3184
3128
|
|
|
3185
3129
|
// hook to allow JSON response handle this
|
|
3186
|
-
if (this.
|
|
3130
|
+
if (this.options.onResponse) {
|
|
3187
3131
|
const axiosResponse = {
|
|
3188
3132
|
data: await response.json(),
|
|
3189
3133
|
status: response.status(),
|
|
3190
3134
|
statusText: response.statusText(),
|
|
3191
3135
|
headers: response.headers(),
|
|
3192
3136
|
}
|
|
3193
|
-
this.
|
|
3137
|
+
this.options.onResponse(axiosResponse)
|
|
3194
3138
|
}
|
|
3195
3139
|
|
|
3196
3140
|
return response
|
|
@@ -4337,11 +4281,11 @@ function isRoleLocatorObject(locator) {
|
|
|
4337
4281
|
*/
|
|
4338
4282
|
async function handleRoleLocator(context, locator) {
|
|
4339
4283
|
if (!isRoleLocatorObject(locator)) return null
|
|
4340
|
-
|
|
4284
|
+
|
|
4341
4285
|
const options = {}
|
|
4342
4286
|
if (locator.text) options.name = locator.text
|
|
4343
4287
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4344
|
-
|
|
4288
|
+
|
|
4345
4289
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4346
4290
|
}
|
|
4347
4291
|
|
|
@@ -4350,7 +4294,7 @@ async function findElements(matcher, locator) {
|
|
|
4350
4294
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4351
4295
|
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4352
4296
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4353
|
-
|
|
4297
|
+
|
|
4354
4298
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4355
4299
|
if (isVueLocator) return findVue(matcher, locator)
|
|
4356
4300
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
@@ -4391,7 +4335,7 @@ async function findCustomElements(matcher, locator) {
|
|
|
4391
4335
|
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4392
4336
|
// and persists in every worker thread instance
|
|
4393
4337
|
let strategyFunction = null
|
|
4394
|
-
|
|
4338
|
+
|
|
4395
4339
|
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4396
4340
|
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4397
4341
|
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
@@ -4967,7 +4911,7 @@ async function refreshContextSession() {
|
|
|
4967
4911
|
this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
|
|
4968
4912
|
return
|
|
4969
4913
|
}
|
|
4970
|
-
|
|
4914
|
+
|
|
4971
4915
|
const currentUrl = await this.grabCurrentUrl()
|
|
4972
4916
|
|
|
4973
4917
|
if (currentUrl.startsWith('http')) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -2955,7 +2955,7 @@ async function findElements(matcher, locator) {
|
|
|
2955
2955
|
async function findElement(matcher, locator) {
|
|
2956
2956
|
if (locator.react) return findReactElements.call(this, locator)
|
|
2957
2957
|
locator = new Locator(locator, 'css')
|
|
2958
|
-
|
|
2958
|
+
|
|
2959
2959
|
// Check if locator is a role locator and call findByRole
|
|
2960
2960
|
if (locator.isRole()) {
|
|
2961
2961
|
const elements = await findByRole.call(this, matcher, locator)
|
|
@@ -2967,10 +2967,13 @@ async function findElement(matcher, locator) {
|
|
|
2967
2967
|
const elements = await matcher.$$(locator.simplify())
|
|
2968
2968
|
return elements[0]
|
|
2969
2969
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2970
|
+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2971
|
+
if (puppeteer.default?.defaultBrowserRevision) {
|
|
2972
|
+
const elements = await matcher.$$(`xpath/${locator.value}`)
|
|
2973
|
+
return elements[0]
|
|
2974
|
+
}
|
|
2975
|
+
// For Puppeteer 24.x+, $x method was removed - use ::-p-xpath() selector
|
|
2976
|
+
const elements = await matcher.$$(`::-p-xpath(${locator.value})`)
|
|
2974
2977
|
return elements[0]
|
|
2975
2978
|
}
|
|
2976
2979
|
|
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))
|
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/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/mocha/test.js
CHANGED
|
@@ -154,14 +154,16 @@ function cloneTest(test) {
|
|
|
154
154
|
function testToFileName(test, { suffix = '', unique = false } = {}) {
|
|
155
155
|
let fileName = test.title
|
|
156
156
|
|
|
157
|
-
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
|
|
158
|
-
if (suffix) fileName = `${fileName}_${suffix}`
|
|
159
157
|
// remove tags with empty string (disable for now)
|
|
160
158
|
// fileName = fileName.replace(/\@\w+/g, '')
|
|
161
159
|
fileName = fileName.slice(0, 100)
|
|
162
160
|
if (fileName.indexOf('{') !== -1) {
|
|
163
161
|
fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
|
|
164
162
|
}
|
|
163
|
+
|
|
164
|
+
// Apply unique suffix AFTER removing data part to ensure uniqueness
|
|
165
|
+
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime())}`
|
|
166
|
+
if (suffix) fileName = `${fileName}_${suffix}`
|
|
165
167
|
if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
|
|
166
168
|
// TODO: add suite title to file name
|
|
167
169
|
// if (test.parent && test.parent.title) {
|
package/lib/output.js
CHANGED
|
@@ -222,12 +222,10 @@ const output = {
|
|
|
222
222
|
/**
|
|
223
223
|
* @param {Mocha.Test} test
|
|
224
224
|
*/
|
|
225
|
-
|
|
226
225
|
started(test) {
|
|
227
226
|
if (outputLevel < 1) return
|
|
228
227
|
print(` ${colors.dim.bold('Scenario()')}`)
|
|
229
228
|
},
|
|
230
|
-
|
|
231
229
|
/**
|
|
232
230
|
* @param {Mocha.Test} test
|
|
233
231
|
*/
|
|
@@ -273,10 +271,12 @@ const output = {
|
|
|
273
271
|
},
|
|
274
272
|
|
|
275
273
|
/**
|
|
274
|
+
* Prints the stats of a test run to the console.
|
|
276
275
|
* @param {number} passed
|
|
277
276
|
* @param {number} failed
|
|
278
277
|
* @param {number} skipped
|
|
279
278
|
* @param {number|string} duration
|
|
279
|
+
* @param {number} [failedHooks]
|
|
280
280
|
*/
|
|
281
281
|
result(passed, failed, skipped, duration, failedHooks = 0) {
|
|
282
282
|
let style = colors.bgGreen
|
package/lib/step/base.js
CHANGED
|
@@ -147,9 +147,22 @@ class Step {
|
|
|
147
147
|
line() {
|
|
148
148
|
const lines = this.stack.split('\n')
|
|
149
149
|
if (lines[STACK_LINE]) {
|
|
150
|
-
|
|
150
|
+
let line = lines[STACK_LINE].trim()
|
|
151
151
|
.replace(global.codecept_dir || '', '.')
|
|
152
152
|
.trim()
|
|
153
|
+
|
|
154
|
+
// Map .temp.mjs back to original .ts files using container's tsFileMapping
|
|
155
|
+
const fileMapping = global.container?.tsFileMapping?.()
|
|
156
|
+
if (line.includes('.temp.mjs') && fileMapping) {
|
|
157
|
+
for (const [tsFile, mjsFile] of fileMapping.entries()) {
|
|
158
|
+
if (line.includes(mjsFile)) {
|
|
159
|
+
line = line.replace(mjsFile, tsFile)
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return line
|
|
153
166
|
}
|
|
154
167
|
return ''
|
|
155
168
|
}
|