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 +2 -2
- package/lib/command/definitions.js +8 -3
- package/lib/command/workers/runTests.js +39 -2
- package/lib/config.js +3 -2
- package/lib/container.js +78 -6
- package/lib/helper/Playwright.js +80 -120
- package/lib/helper/Puppeteer.js +8 -5
- package/lib/listener/helpers.js +2 -14
- 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/record.js +8 -0
- package/lib/utils/loaderCheck.js +13 -3
- package/lib/utils/typescript.js +82 -35
- package/lib/workers.js +19 -2
- package/package.json +22 -22
- package/typings/index.d.ts +1 -1
- package/typings/promiseBasedTypes.d.ts +136 -43
- package/typings/types.d.ts +150 -74
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('
|
|
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('
|
|
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
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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']
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -565,54 +585,23 @@ class Playwright extends Helper {
|
|
|
565
585
|
}
|
|
566
586
|
|
|
567
587
|
// Register all custom locator strategies from the global registry
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1292
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
//
|
|
1404
|
-
|
|
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
|
|
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
|
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/listener/helpers.js
CHANGED
|
@@ -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
|
|
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
|
|
87
|
+
if (helper._cleanup) {
|
|
100
88
|
recorder.add(`hook ${key}._cleanup()`, () => helper._cleanup(), true, false)
|
|
101
89
|
}
|
|
102
90
|
})
|
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
|