codeceptjs 4.0.1-beta.9 → 4.0.2-beta.2

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 CHANGED
@@ -174,7 +174,7 @@ program
174
174
  .option('-R, --reporter <name>', 'specify the reporter to use')
175
175
  .option('-S, --sort', 'sort test files')
176
176
  .option('-b, --bail', 'bail after first test failure')
177
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
177
+ .option('--inspec', "enable node's debugger, synonym for node --debug")
178
178
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
179
179
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
180
180
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -276,7 +276,7 @@ program
276
276
  .option('-R, --reporter <name>', 'specify the reporter to use')
277
277
  .option('-S, --sort', 'sort test files')
278
278
  .option('-b, --bail', 'bail after first test failure')
279
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
279
+ .option('--inspect', "enable node's debugger, synonym for node --debug")
280
280
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
281
281
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
282
282
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -41,7 +41,7 @@ const getDefinitionsFileContent = ({ hasCustomHelper, hasCustomStepsFile, helper
41
41
 
42
42
  const importPathsFragment = importPaths.join('\n')
43
43
  const supportObjectsTypeFragment = convertMapToType(supportObject)
44
- const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : ''
44
+ const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : 'interface Methods {}'
45
45
  const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2)
46
46
 
47
47
  return generateDefinitionsContent({
@@ -239,8 +239,13 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
239
239
  }
240
240
 
241
241
  for (const name in pathsToValue) {
242
- const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath)
243
- importStrings.push(`type ${name} = import('${relativePath}');`)
242
+ const originalPath = pathsToValue[name]
243
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
244
+ if (originalPath.endsWith('.js') || originalPath.endsWith('.ts')) {
245
+ importStrings.push(`type ${name} = InstanceType<typeof import('${relativePath}').default>;`)
246
+ } else {
247
+ importStrings.push(`type ${name} = import('${relativePath}');`)
248
+ }
244
249
  }
245
250
 
246
251
  return importStrings
@@ -104,7 +104,9 @@ initPromise = (async function () {
104
104
  // important deep merge so dynamic things e.g. functions on config are not overridden
105
105
  config = deepMerge(baseConfig, overrideConfigs)
106
106
 
107
- codecept = new Codecept(config, options)
107
+ // Pass workerIndex as child option for output.process() to display worker prefix
108
+ const optsWithChild = { ...options, child: workerIndex }
109
+ codecept = new Codecept(config, optsWithChild)
108
110
  await codecept.init(testRoot)
109
111
  codecept.loadTests()
110
112
  mocha = container.mocha()
@@ -334,8 +336,43 @@ function filterTests() {
334
336
  mocha.files = files
335
337
  mocha.loadFiles()
336
338
 
337
- for (const suite of mocha.suite.suites) {
339
+ // Debug logging to help diagnose test filtering issues
340
+ if (options.debug || options.verbose) {
341
+ const allLoadedTests = [];
342
+ mocha.suite.eachTest(test => {
343
+ if (test) {
344
+ allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
345
+ }
346
+ });
347
+
348
+ console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests, expecting ${tests.length} tests`);
349
+
350
+ const loadedUids = new Set(allLoadedTests.map(t => t.uid));
351
+ const missingTests = tests.filter(uid => !loadedUids.has(uid));
352
+
353
+ if (missingTests.length > 0) {
354
+ console.log(`[Worker ${workerIndex}] WARNING: ${missingTests.length} assigned tests not found in loaded files`);
355
+ console.log(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
356
+ }
357
+ }
358
+
359
+ // Recursively filter tests in all suites (including nested ones)
360
+ const filterSuiteTests = (suite) => {
338
361
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
362
+ for (const childSuite of suite.suites) {
363
+ filterSuiteTests(childSuite)
364
+ }
365
+ }
366
+
367
+ for (const suite of mocha.suite.suites) {
368
+ filterSuiteTests(suite)
369
+ }
370
+
371
+ // Verify final test count
372
+ if (options.debug || options.verbose) {
373
+ let finalCount = 0;
374
+ mocha.suite.eachTest(() => finalCount++);
375
+ console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests will run`);
339
376
  }
340
377
  }
341
378
 
package/lib/config.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
4
  import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
5
- import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
5
+ import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
6
6
 
7
7
  const defaultConfig = {
8
8
  output: './_output',
@@ -159,12 +159,13 @@ async function loadConfigFile(configFile) {
159
159
  try {
160
160
  // Use the TypeScript transpilation utility
161
161
  const typescript = require('typescript')
162
- const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
162
+ const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript)
163
163
 
164
164
  try {
165
165
  configModule = await import(tempFile)
166
166
  cleanupTempFiles(allTempFiles)
167
167
  } catch (err) {
168
+ fixErrorStack(err, fileMapping)
168
169
  cleanupTempFiles(allTempFiles)
169
170
  throw err
170
171
  }
package/lib/container.js CHANGED
@@ -5,7 +5,7 @@ import debugModule from 'debug'
5
5
  const debug = debugModule('codeceptjs:container')
6
6
  import { MetaStep } from './step.js'
7
7
  import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
8
- import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
8
+ import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
9
9
  import Translation from './translation.js'
10
10
  import MochaFactory from './mocha/factory.js'
11
11
  import recorder from './recorder.js'
@@ -34,6 +34,7 @@ let container = {
34
34
  /** @type {Result | null} */
35
35
  result: null,
36
36
  sharedKeys: new Set(), // Track keys shared via share() function
37
+ tsFileMapping: null, // TypeScript file mapping for error stack fixing
37
38
  }
38
39
 
39
40
  /**
@@ -88,7 +89,7 @@ class Container {
88
89
  container.support.I = mod
89
90
  }
90
91
  } catch (e) {
91
- throw new Error(`Could not include object I: ${e.message}`)
92
+ throw e
92
93
  }
93
94
  } else {
94
95
  // Create default actor - this sets up the callback in asyncHelperPromise
@@ -176,6 +177,15 @@ class Container {
176
177
  return container.translation
177
178
  }
178
179
 
180
+ /**
181
+ * Get TypeScript file mapping for error stack fixing
182
+ *
183
+ * @api
184
+ */
185
+ static tsFileMapping() {
186
+ return container.tsFileMapping
187
+ }
188
+
179
189
  /**
180
190
  * Get Mocha instance
181
191
  *
@@ -398,20 +408,66 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
398
408
  throw err
399
409
  }
400
410
  } else {
411
+ // Handle TypeScript files
412
+ let importPath = moduleName
413
+ let tempJsFile = null
414
+ let fileMapping = null
415
+ const ext = path.extname(moduleName)
416
+
417
+ if (ext === '.ts') {
418
+ try {
419
+ // Use the TypeScript transpilation utility
420
+ const typescript = await import('typescript')
421
+ const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
422
+
423
+ debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
424
+
425
+ importPath = tempFile
426
+ tempJsFile = allTempFiles
427
+ fileMapping = mapping
428
+ // Store file mapping in container for runtime error fixing (merge with existing)
429
+ if (!container.tsFileMapping) {
430
+ container.tsFileMapping = new Map()
431
+ }
432
+ for (const [key, value] of mapping.entries()) {
433
+ container.tsFileMapping.set(key, value)
434
+ }
435
+ } catch (tsError) {
436
+ throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
437
+ }
438
+ }
439
+
401
440
  // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
402
441
  try {
403
442
  // Try dynamic import for both CommonJS and ESM modules
404
- const mod = await import(moduleName)
443
+ const mod = await import(importPath)
405
444
  if (!mod && !mod.default) {
406
445
  throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
407
446
  }
408
447
  HelperClass = mod.default || mod
448
+
449
+ // Clean up temp files if created
450
+ if (tempJsFile) {
451
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
452
+ cleanupTempFiles(filesToClean)
453
+ }
409
454
  } catch (err) {
455
+ // Fix error stack to point to original .ts files
456
+ if (fileMapping) {
457
+ fixErrorStack(err, fileMapping)
458
+ }
459
+
460
+ // Clean up temp files before rethrowing
461
+ if (tempJsFile) {
462
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
463
+ cleanupTempFiles(filesToClean)
464
+ }
465
+
410
466
  if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
411
467
  // This is an ESM module, use dynamic import
412
468
  try {
413
469
  const pathModule = await import('path')
414
- const absolutePath = pathModule.default.resolve(moduleName)
470
+ const absolutePath = pathModule.default.resolve(importPath)
415
471
  const mod = await import(absolutePath)
416
472
  HelperClass = mod.default || mod
417
473
  debug(`helper ${helperName} loaded via ESM import`)
@@ -699,6 +755,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
699
755
  // Use dynamic import for both ESM and CJS modules
700
756
  let importPath = modulePath
701
757
  let tempJsFile = null
758
+ let fileMapping = null
702
759
 
703
760
  if (typeof importPath === 'string') {
704
761
  const ext = path.extname(importPath)
@@ -708,7 +765,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
708
765
  try {
709
766
  // Use the TypeScript transpilation utility
710
767
  const typescript = await import('typescript')
711
- const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
768
+ const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
712
769
 
713
770
  debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
714
771
 
@@ -716,6 +773,14 @@ async function loadSupportObject(modulePath, supportObjectName) {
716
773
  importPath = tempFile
717
774
  // Store temp files list in a way that cleanup can access them
718
775
  tempJsFile = allTempFiles
776
+ fileMapping = mapping
777
+ // Store file mapping in container for runtime error fixing (merge with existing)
778
+ if (!container.tsFileMapping) {
779
+ container.tsFileMapping = new Map()
780
+ }
781
+ for (const [key, value] of mapping.entries()) {
782
+ container.tsFileMapping.set(key, value)
783
+ }
719
784
  } catch (tsError) {
720
785
  throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
721
786
  }
@@ -729,6 +794,11 @@ async function loadSupportObject(modulePath, supportObjectName) {
729
794
  try {
730
795
  obj = await import(importPath)
731
796
  } catch (importError) {
797
+ // Fix error stack to point to original .ts files
798
+ if (fileMapping) {
799
+ fixErrorStack(importError, fileMapping)
800
+ }
801
+
732
802
  // Clean up temp files if created before rethrowing
733
803
  if (tempJsFile) {
734
804
  const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
@@ -777,7 +847,9 @@ async function loadSupportObject(modulePath, supportObjectName) {
777
847
 
778
848
  throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
779
849
  } catch (err) {
780
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
850
+ const newErr = new Error(`Could not include object ${supportObjectName} from module '${modulePath}': ${err.message}`)
851
+ newErr.stack = err.stack
852
+ throw newErr
781
853
  }
782
854
  }
783
855
 
@@ -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']
@@ -358,23 +388,13 @@ class Playwright extends Helper {
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
 
@@ -565,54 +585,23 @@ class Playwright extends Helper {
565
585
  }
566
586
 
567
587
  // Register all custom locator strategies from the global registry
568
- for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
569
- if (!registeredCustomLocatorStrategies.has(strategyName)) {
570
- try {
571
- // Create a selector engine factory function exactly like createValueEngine pattern
572
- // Capture variables in closure to avoid reference issues
573
- const createCustomEngine = ((name, func) => {
574
- return () => {
575
- return {
576
- create() {
577
- return null
578
- },
579
- query(root, selector) {
580
- try {
581
- if (!root) return null
582
- const result = func(selector, root)
583
- return Array.isArray(result) ? result[0] : result
584
- } catch (error) {
585
- console.warn(`Error in custom locator "${name}":`, error)
586
- return null
587
- }
588
- },
589
- queryAll(root, selector) {
590
- try {
591
- if (!root) return []
592
- const result = func(selector, root)
593
- return Array.isArray(result) ? result : result ? [result] : []
594
- } catch (error) {
595
- console.warn(`Error in custom locator "${name}":`, error)
596
- return []
597
- }
598
- },
599
- }
600
- }
601
- })(strategyName, strategyFunction)
588
+ await this._registerGlobalCustomLocators()
589
+ } catch (e) {
590
+ console.warn(e)
591
+ }
592
+ }
602
593
 
603
- await playwright.selectors.register(strategyName, createCustomEngine)
604
- registeredCustomLocatorStrategies.add(strategyName)
605
- } catch (error) {
606
- if (!error.message.includes('already registered')) {
607
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
608
- } else {
609
- console.log(`Custom locator strategy '${strategyName}' already registered`)
610
- }
611
- }
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}`)
612
603
  }
613
604
  }
614
- } catch (e) {
615
- console.warn(e)
616
605
  }
617
606
  }
618
607
 
@@ -923,7 +912,7 @@ class Playwright extends Helper {
923
912
  }
924
913
 
925
914
  async _finishTest() {
926
- if ((restartsSession() || restartsContext() || restartsBrowser()) && this.isRunning) {
915
+ if (this.isRunning) {
927
916
  try {
928
917
  await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Test finish timeout')), 10000))])
929
918
  } catch (e) {
@@ -1277,28 +1266,31 @@ class Playwright extends Helper {
1277
1266
  return this.browser
1278
1267
  }
1279
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
+
1280
1279
  _lookupCustomLocator(customStrategy) {
1281
- if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
1282
- return null
1283
- }
1280
+ if (!this._hasCustomLocatorStrategies()) return null
1284
1281
  const strategy = this.customLocatorStrategies[customStrategy]
1285
1282
  return typeof strategy === 'function' ? strategy : null
1286
1283
  }
1287
1284
 
1288
1285
  _isCustomLocator(locator) {
1289
1286
  const locatorObj = new Locator(locator)
1290
- if (locatorObj.isCustom()) {
1291
- const customLocator = this._lookupCustomLocator(locatorObj.type)
1292
- if (customLocator) {
1293
- return true
1294
- }
1295
- throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
1296
- }
1297
- 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".')
1298
1290
  }
1299
1291
 
1300
1292
  _isCustomLocatorStrategyDefined() {
1301
- return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
1293
+ return this._hasCustomLocatorStrategies()
1302
1294
  }
1303
1295
 
1304
1296
  /**
@@ -1321,49 +1313,16 @@ class Playwright extends Helper {
1321
1313
  }
1322
1314
 
1323
1315
  async _registerCustomLocatorStrategies() {
1324
- if (!this.customLocatorStrategies) return
1325
-
1326
- for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
1327
- if (!registeredCustomLocatorStrategies.has(strategyName)) {
1328
- try {
1329
- const createCustomEngine = ((name, func) => {
1330
- return () => {
1331
- return {
1332
- create(root, target) {
1333
- return null
1334
- },
1335
- query(root, selector) {
1336
- try {
1337
- if (!root) return null
1338
- const result = func(selector, root)
1339
- return Array.isArray(result) ? result[0] : result
1340
- } catch (error) {
1341
- console.warn(`Error in custom locator "${name}":`, error)
1342
- return null
1343
- }
1344
- },
1345
- queryAll(root, selector) {
1346
- try {
1347
- if (!root) return []
1348
- const result = func(selector, root)
1349
- return Array.isArray(result) ? result : result ? [result] : []
1350
- } catch (error) {
1351
- console.warn(`Error in custom locator "${name}":`, error)
1352
- return []
1353
- }
1354
- },
1355
- }
1356
- }
1357
- })(strategyName, strategyFunction)
1316
+ if (!this._hasCustomLocatorStrategies()) return
1358
1317
 
1359
- await playwright.selectors.register(strategyName, createCustomEngine)
1360
- registeredCustomLocatorStrategies.add(strategyName)
1361
- } catch (error) {
1362
- if (!error.message.includes('already registered')) {
1363
- console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
1364
- } else {
1365
- console.log(`Custom locator strategy '${strategyName}' already registered`)
1366
- }
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}`)
1367
1326
  }
1368
1327
  }
1369
1328
  }
@@ -1389,6 +1348,7 @@ class Playwright extends Helper {
1389
1348
  }
1390
1349
  }
1391
1350
 
1351
+ // Close browserContext if recordHar is enabled
1392
1352
  if (this.options.recordHar && this.browserContext) {
1393
1353
  try {
1394
1354
  await this.browserContext.close()
@@ -1398,16 +1358,16 @@ class Playwright extends Helper {
1398
1358
  }
1399
1359
  this.browserContext = null
1400
1360
 
1361
+ // Initiate browser close without waiting for it to complete
1362
+ // The browser process will be cleaned up when the Node process exits
1401
1363
  if (this.browser) {
1402
1364
  try {
1403
- // Add timeout to prevent browser.close() from hanging indefinitely
1404
- await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))])
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
+ })
1405
1369
  } catch (e) {
1406
- // Ignore errors if browser is already closed or timeout
1407
- if (!e.message?.includes('Browser close timeout')) {
1408
- // Non-timeout error, can be ignored as well
1409
- }
1410
- // Force cleanup even on error
1370
+ // Ignore any synchronous errors
1411
1371
  }
1412
1372
  }
1413
1373
  this.browser = null
@@ -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
 
@@ -73,30 +73,18 @@ export default function () {
73
73
  })
74
74
 
75
75
  event.dispatcher.on(event.all.result, () => {
76
- // Skip _finishTest for all helpers if any browser helper restarts to avoid double cleanup
77
- const hasBrowserRestart = Object.values(helpers).some(helper =>
78
- (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
79
- (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
80
- )
81
-
82
76
  Object.keys(helpers).forEach(key => {
83
77
  const helper = helpers[key]
84
- if (helper._finishTest && !hasBrowserRestart) {
78
+ if (helper._finishTest) {
85
79
  recorder.add(`hook ${key}._finishTest()`, () => helper._finishTest(), true, false)
86
80
  }
87
81
  })
88
82
  })
89
83
 
90
84
  event.dispatcher.on(event.all.after, () => {
91
- // Skip _cleanup for all helpers if any browser helper restarts to avoid double cleanup
92
- const hasBrowserRestart = Object.values(helpers).some(helper =>
93
- (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
94
- (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
95
- )
96
-
97
85
  Object.keys(helpers).forEach(key => {
98
86
  const helper = helpers[key]
99
- if (helper._cleanup && !hasBrowserRestart) {
87
+ if (helper._cleanup) {
100
88
  recorder.add(`hook ${key}._cleanup()`, () => helper._cleanup(), true, false)
101
89
  }
102
90
  })
@@ -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