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.
@@ -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
- let validCustomLocators = null
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 [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
377
- globalCustomLocatorStrategies.set(strategyName, strategyFunction)
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
- for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
568
- if (!registeredCustomLocatorStrategies.has(strategyName)) {
569
- try {
570
- // Create a selector engine factory function exactly like createValueEngine pattern
571
- // Capture variables in closure to avoid reference issues
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
- await playwright.selectors.register(strategyName, createCustomEngine)
603
- registeredCustomLocatorStrategies.add(strategyName)
604
- } catch (error) {
605
- if (!error.message.includes('already registered')) {
606
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
607
- } else {
608
- console.log(`Custom locator strategy '${strategyName}' already registered`)
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 (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
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
- const customLocator = this._lookupCustomLocator(locatorObj.type)
1303
- if (customLocator) {
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 !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
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.customLocatorStrategies) return
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
- await playwright.selectors.register(strategyName, createCustomEngine)
1371
- registeredCustomLocatorStrategies.add(strategyName)
1372
- } catch (error) {
1373
- if (!error.message.includes('already registered')) {
1374
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
1375
- } else {
1376
- console.log(`Custom locator strategy '${strategyName}' already registered`)
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
- // 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
- ])
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 errors if browser is already closed or timeout
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.config.onResponse) {
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.config.onResponse(axiosResponse)
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')) {
@@ -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
- // For XPath in Puppeteer 24.x+, use the same approach as findElements
2972
- // $x method was removed, so we use ::-p-xpath() or fallback
2973
- const elements = await findElements.call(this, matcher, locator)
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
 
@@ -94,7 +94,9 @@ const config = {}
94
94
  class REST extends Helper {
95
95
  constructor(config) {
96
96
  super(config)
97
- this.options = {
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
- if (this.config.onRequest) {
219
- await this.config.onRequest(request)
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
- if (this.config.onResponse) {
249
- await this.config.onResponse(response)
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))
@@ -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]
@@ -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 ESM imports
65
+ // Load JavaScript test files using original loadFiles
66
66
  if (jsFiles.length > 0) {
67
- try {
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
- return lines[STACK_LINE].trim()
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
  }