codeceptjs 4.0.0-beta.9.esm-aria → 4.0.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/actor.js +12 -8
- package/lib/codecept.js +25 -1
- package/lib/command/init.js +2 -1
- package/lib/config.js +7 -22
- package/lib/container.js +116 -22
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +19 -35
- package/lib/helper/REST.js +15 -9
- package/lib/listener/config.js +11 -3
- package/lib/mocha/gherkin.js +4 -4
- package/lib/plugin/htmlReporter.js +4 -4
- package/lib/step/meta.js +18 -1
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/typescript.js +237 -0
- package/lib/workers.js +36 -41
- package/package.json +10 -1
- package/typings/promiseBasedTypes.d.ts +4 -0
- package/typings/types.d.ts +4 -0
package/lib/actor.js
CHANGED
|
@@ -75,7 +75,8 @@ export default function (obj = {}, container) {
|
|
|
75
75
|
if (!container) {
|
|
76
76
|
container = Container
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
// Get existing actor or create a new one
|
|
79
80
|
const actor = container.actor() || new Actor()
|
|
80
81
|
|
|
81
82
|
// load all helpers once container initialized
|
|
@@ -111,14 +112,17 @@ export default function (obj = {}, container) {
|
|
|
111
112
|
}
|
|
112
113
|
})
|
|
113
114
|
|
|
114
|
-
container.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// Update container.support.I to ensure it has the latest actor reference
|
|
116
|
+
if (!container.actor() || container.actor() !== actor) {
|
|
117
|
+
container.append({
|
|
118
|
+
support: {
|
|
119
|
+
I: actor,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
119
123
|
})
|
|
120
|
-
|
|
121
|
-
// add custom steps from actor
|
|
124
|
+
|
|
125
|
+
// add custom steps from actor immediately
|
|
122
126
|
Object.keys(obj).forEach(key => {
|
|
123
127
|
const ms = new MetaStep('I', key)
|
|
124
128
|
ms.setContext(actor)
|
package/lib/codecept.js
CHANGED
|
@@ -3,8 +3,9 @@ import { globSync } from 'glob'
|
|
|
3
3
|
import shuffle from 'lodash.shuffle'
|
|
4
4
|
import fsPath from 'path'
|
|
5
5
|
import { resolve } from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
7
7
|
import { dirname } from 'path'
|
|
8
|
+
import { createRequire } from 'module'
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
11
|
const __dirname = dirname(__filename)
|
|
@@ -18,6 +19,7 @@ import ActorFactory from './actor.js'
|
|
|
18
19
|
import output from './output.js'
|
|
19
20
|
import { emptyFolder } from './utils.js'
|
|
20
21
|
import { initCodeceptGlobals } from './globals.js'
|
|
22
|
+
import { validateTypeScriptSetup } from './utils/loaderCheck.js'
|
|
21
23
|
import recorder from './recorder.js'
|
|
22
24
|
|
|
23
25
|
import storeListener from './listener/store.js'
|
|
@@ -66,6 +68,21 @@ class Codecept {
|
|
|
66
68
|
modulePath = `${modulePath}.js`
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
} else {
|
|
72
|
+
// For npm packages, resolve from the user's directory
|
|
73
|
+
// This ensures packages like tsx are found in user's node_modules
|
|
74
|
+
const userDir = global.codecept_dir || process.cwd()
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Use createRequire to resolve from user's directory
|
|
78
|
+
const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
|
|
79
|
+
const resolvedPath = userRequire.resolve(requiredModule)
|
|
80
|
+
modulePath = pathToFileURL(resolvedPath).href
|
|
81
|
+
} catch (resolveError) {
|
|
82
|
+
// If resolution fails, try direct import (will check from CodeceptJS node_modules)
|
|
83
|
+
// This is the fallback for globally installed packages
|
|
84
|
+
modulePath = requiredModule
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
// Use dynamic import for ESM
|
|
71
88
|
await import(modulePath)
|
|
@@ -246,6 +263,13 @@ class Codecept {
|
|
|
246
263
|
async run(test) {
|
|
247
264
|
await container.started()
|
|
248
265
|
|
|
266
|
+
// Check TypeScript loader configuration before running tests
|
|
267
|
+
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
|
|
268
|
+
if (tsValidation.hasError) {
|
|
269
|
+
output.error(tsValidation.message)
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
|
|
249
273
|
// Ensure translations are loaded for Gherkin features
|
|
250
274
|
try {
|
|
251
275
|
const { loadTranslations } = await import('./mocha/gherkin.js')
|
package/lib/command/init.js
CHANGED
|
@@ -161,7 +161,7 @@ export default async function (initPath) {
|
|
|
161
161
|
isTypeScript = true
|
|
162
162
|
extension = isTypeScript === true ? 'ts' : 'js'
|
|
163
163
|
packages.push('typescript')
|
|
164
|
-
packages.push('
|
|
164
|
+
packages.push('tsx') // Add tsx for TypeScript support
|
|
165
165
|
packages.push('@types/node')
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -172,6 +172,7 @@ export default async function (initPath) {
|
|
|
172
172
|
config.tests = result.tests
|
|
173
173
|
if (isTypeScript) {
|
|
174
174
|
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
|
|
175
|
+
config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// create a directory tests if it is included in tests path
|
package/lib/config.js
CHANGED
|
@@ -2,6 +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
6
|
|
|
6
7
|
const defaultConfig = {
|
|
7
8
|
output: './_output',
|
|
@@ -156,31 +157,15 @@ async function loadConfigFile(configFile) {
|
|
|
156
157
|
// For .ts files, try to compile and load as JavaScript
|
|
157
158
|
if (extensionName === '.ts') {
|
|
158
159
|
try {
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
// Transpile TypeScript to JavaScript with ES module output
|
|
164
|
-
const jsContent = transpile(tsContent, {
|
|
165
|
-
module: 99, // ModuleKind.ESNext
|
|
166
|
-
target: 99, // ScriptTarget.ESNext
|
|
167
|
-
esModuleInterop: true,
|
|
168
|
-
allowSyntheticDefaultImports: true,
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// Create a temporary JS file with .mjs extension to force ES module treatment
|
|
172
|
-
const tempJsFile = configFile.replace('.ts', '.temp.mjs')
|
|
173
|
-
fs.writeFileSync(tempJsFile, jsContent)
|
|
160
|
+
// Use the TypeScript transpilation utility
|
|
161
|
+
const typescript = require('typescript')
|
|
162
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
|
|
174
163
|
|
|
175
164
|
try {
|
|
176
|
-
configModule = await import(
|
|
177
|
-
|
|
178
|
-
fs.unlinkSync(tempJsFile)
|
|
165
|
+
configModule = await import(tempFile)
|
|
166
|
+
cleanupTempFiles(allTempFiles)
|
|
179
167
|
} catch (err) {
|
|
180
|
-
|
|
181
|
-
if (fs.existsSync(tempJsFile)) {
|
|
182
|
-
fs.unlinkSync(tempJsFile)
|
|
183
|
-
}
|
|
168
|
+
cleanupTempFiles(allTempFiles)
|
|
184
169
|
throw err
|
|
185
170
|
}
|
|
186
171
|
} catch (tsError) {
|
package/lib/container.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { globSync } from 'glob'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
3
4
|
import debugModule from 'debug'
|
|
4
5
|
const debug = debugModule('codeceptjs:container')
|
|
5
6
|
import { MetaStep } from './step.js'
|
|
6
7
|
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
|
|
8
|
+
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
|
|
7
9
|
import Translation from './translation.js'
|
|
8
10
|
import MochaFactory from './mocha/factory.js'
|
|
9
11
|
import recorder from './recorder.js'
|
|
@@ -20,6 +22,7 @@ let container = {
|
|
|
20
22
|
helpers: {},
|
|
21
23
|
support: {},
|
|
22
24
|
proxySupport: {},
|
|
25
|
+
proxySupportConfig: {}, // Track config used to create proxySupport
|
|
23
26
|
plugins: {},
|
|
24
27
|
actor: null,
|
|
25
28
|
/**
|
|
@@ -30,7 +33,7 @@ let container = {
|
|
|
30
33
|
translation: {},
|
|
31
34
|
/** @type {Result | null} */
|
|
32
35
|
result: null,
|
|
33
|
-
sharedKeys: new Set() // Track keys shared via share() function
|
|
36
|
+
sharedKeys: new Set(), // Track keys shared via share() function
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -65,14 +68,15 @@ class Container {
|
|
|
65
68
|
container.support = {}
|
|
66
69
|
container.helpers = await createHelpers(config.helpers || {})
|
|
67
70
|
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
|
|
68
|
-
container.
|
|
71
|
+
container.proxySupportConfig = config.include || {}
|
|
72
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
69
73
|
container.plugins = await createPlugins(config.plugins || {}, opts)
|
|
70
74
|
container.result = new Result()
|
|
71
75
|
|
|
72
76
|
// Preload includes (so proxies can expose real objects synchronously)
|
|
73
77
|
const includes = config.include || {}
|
|
74
78
|
|
|
75
|
-
//
|
|
79
|
+
// Check if custom I is provided
|
|
76
80
|
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
|
|
77
81
|
try {
|
|
78
82
|
const mod = includes.I
|
|
@@ -87,7 +91,7 @@ class Container {
|
|
|
87
91
|
throw new Error(`Could not include object I: ${e.message}`)
|
|
88
92
|
}
|
|
89
93
|
} else {
|
|
90
|
-
// Create default actor
|
|
94
|
+
// Create default actor - this sets up the callback in asyncHelperPromise
|
|
91
95
|
createActor()
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -108,6 +112,9 @@ class Container {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
// Wait for all async helpers to finish loading and populate the actor
|
|
116
|
+
await asyncHelperPromise
|
|
117
|
+
|
|
111
118
|
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
112
119
|
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
113
120
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
@@ -202,8 +209,10 @@ class Container {
|
|
|
202
209
|
|
|
203
210
|
// If new support objects are added, update the proxy support
|
|
204
211
|
if (newContainer.support) {
|
|
205
|
-
|
|
206
|
-
container.
|
|
212
|
+
// Merge the new support config with existing config
|
|
213
|
+
container.proxySupportConfig = { ...container.proxySupportConfig, ...newContainer.support }
|
|
214
|
+
// Recreate the proxy with merged config
|
|
215
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
@@ -219,6 +228,7 @@ class Container {
|
|
|
219
228
|
static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
|
|
220
229
|
container.helpers = newHelpers
|
|
221
230
|
container.translation = await loadTranslation()
|
|
231
|
+
container.proxySupportConfig = newSupport
|
|
222
232
|
container.proxySupport = createSupportObjects(newSupport)
|
|
223
233
|
container.plugins = newPlugins
|
|
224
234
|
container.sharedKeys = new Set() // Clear shared keys
|
|
@@ -248,10 +258,10 @@ class Container {
|
|
|
248
258
|
// Instead of using append which replaces the entire container,
|
|
249
259
|
// directly update the support object to maintain proxy references
|
|
250
260
|
Object.assign(container.support, data)
|
|
251
|
-
|
|
261
|
+
|
|
252
262
|
// Track which keys were explicitly shared
|
|
253
263
|
Object.keys(data).forEach(key => container.sharedKeys.add(key))
|
|
254
|
-
|
|
264
|
+
|
|
255
265
|
if (!options.local) {
|
|
256
266
|
WorkerStorage.share(data)
|
|
257
267
|
}
|
|
@@ -290,7 +300,7 @@ async function createHelpers(config) {
|
|
|
290
300
|
if (!HelperClass) {
|
|
291
301
|
const helperResult = requireHelperFromModule(helperName, config)
|
|
292
302
|
if (helperResult instanceof Promise) {
|
|
293
|
-
// Handle async ESM loading
|
|
303
|
+
// Handle async ESM loading - create placeholder
|
|
294
304
|
helpers[helperName] = {}
|
|
295
305
|
asyncHelperPromise = asyncHelperPromise
|
|
296
306
|
.then(() => helperResult)
|
|
@@ -309,8 +319,7 @@ async function createHelpers(config) {
|
|
|
309
319
|
|
|
310
320
|
checkHelperRequirements(ResolvedHelperClass)
|
|
311
321
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
312
|
-
|
|
313
|
-
debug(`helper ${helperName} async initialized`)
|
|
322
|
+
debug(`helper ${helperName} async loaded`)
|
|
314
323
|
})
|
|
315
324
|
continue
|
|
316
325
|
} else {
|
|
@@ -330,9 +339,8 @@ async function createHelpers(config) {
|
|
|
330
339
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
331
340
|
}
|
|
332
341
|
|
|
333
|
-
debug(`helper ${helperName} async initialized`)
|
|
334
|
-
|
|
335
342
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
343
|
+
debug(`helper ${helperName} async CJS loaded`)
|
|
336
344
|
})
|
|
337
345
|
|
|
338
346
|
continue
|
|
@@ -347,9 +355,18 @@ async function createHelpers(config) {
|
|
|
347
355
|
}
|
|
348
356
|
}
|
|
349
357
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
// Don't await here - let Container.create() handle the await
|
|
359
|
+
// This allows actor callbacks to be registered before resolution
|
|
360
|
+
asyncHelperPromise = asyncHelperPromise.then(async () => {
|
|
361
|
+
// Call _init on all helpers after they're all loaded
|
|
362
|
+
for (const name in helpers) {
|
|
363
|
+
if (helpers[name]._init) {
|
|
364
|
+
await helpers[name]._init()
|
|
365
|
+
debug(`helper ${name} _init() called`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
353
370
|
return helpers
|
|
354
371
|
}
|
|
355
372
|
|
|
@@ -381,20 +398,52 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
381
398
|
throw err
|
|
382
399
|
}
|
|
383
400
|
} else {
|
|
401
|
+
// Handle TypeScript files
|
|
402
|
+
let importPath = moduleName
|
|
403
|
+
let tempJsFile = null
|
|
404
|
+
const ext = path.extname(moduleName)
|
|
405
|
+
|
|
406
|
+
if (ext === '.ts') {
|
|
407
|
+
try {
|
|
408
|
+
// Use the TypeScript transpilation utility
|
|
409
|
+
const typescript = await import('typescript')
|
|
410
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
411
|
+
|
|
412
|
+
debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
|
|
413
|
+
|
|
414
|
+
importPath = tempFile
|
|
415
|
+
tempJsFile = allTempFiles
|
|
416
|
+
} catch (tsError) {
|
|
417
|
+
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
384
421
|
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
|
|
385
422
|
try {
|
|
386
423
|
// Try dynamic import for both CommonJS and ESM modules
|
|
387
|
-
const mod = await import(
|
|
424
|
+
const mod = await import(importPath)
|
|
388
425
|
if (!mod && !mod.default) {
|
|
389
426
|
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
|
|
390
427
|
}
|
|
391
428
|
HelperClass = mod.default || mod
|
|
429
|
+
|
|
430
|
+
// Clean up temp files if created
|
|
431
|
+
if (tempJsFile) {
|
|
432
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
433
|
+
cleanupTempFiles(filesToClean)
|
|
434
|
+
}
|
|
392
435
|
} catch (err) {
|
|
436
|
+
// Clean up temp files before rethrowing
|
|
437
|
+
if (tempJsFile) {
|
|
438
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
439
|
+
cleanupTempFiles(filesToClean)
|
|
440
|
+
}
|
|
441
|
+
|
|
393
442
|
if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
|
|
394
443
|
// This is an ESM module, use dynamic import
|
|
395
444
|
try {
|
|
396
445
|
const pathModule = await import('path')
|
|
397
|
-
const absolutePath = pathModule.default.resolve(
|
|
446
|
+
const absolutePath = pathModule.default.resolve(importPath)
|
|
398
447
|
const mod = await import(absolutePath)
|
|
399
448
|
HelperClass = mod.default || mod
|
|
400
449
|
debug(`helper ${helperName} loaded via ESM import`)
|
|
@@ -523,10 +572,17 @@ function createSupportObjects(config) {
|
|
|
523
572
|
return [...new Set([...keys, ...container.sharedKeys])]
|
|
524
573
|
},
|
|
525
574
|
getOwnPropertyDescriptor(target, prop) {
|
|
575
|
+
// For destructuring to work, we need to return the actual value from the getter
|
|
576
|
+
let value
|
|
577
|
+
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
578
|
+
value = container.support[prop]
|
|
579
|
+
} else {
|
|
580
|
+
value = lazyLoad(prop)
|
|
581
|
+
}
|
|
526
582
|
return {
|
|
527
583
|
enumerable: true,
|
|
528
584
|
configurable: true,
|
|
529
|
-
value:
|
|
585
|
+
value: value,
|
|
530
586
|
}
|
|
531
587
|
},
|
|
532
588
|
get(target, key) {
|
|
@@ -674,12 +730,50 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
674
730
|
try {
|
|
675
731
|
// Use dynamic import for both ESM and CJS modules
|
|
676
732
|
let importPath = modulePath
|
|
677
|
-
|
|
733
|
+
let tempJsFile = null
|
|
734
|
+
|
|
678
735
|
if (typeof importPath === 'string') {
|
|
679
736
|
const ext = path.extname(importPath)
|
|
680
|
-
|
|
737
|
+
|
|
738
|
+
// Handle TypeScript files
|
|
739
|
+
if (ext === '.ts') {
|
|
740
|
+
try {
|
|
741
|
+
// Use the TypeScript transpilation utility
|
|
742
|
+
const typescript = await import('typescript')
|
|
743
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
744
|
+
|
|
745
|
+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
|
|
746
|
+
|
|
747
|
+
// Attach cleanup handler
|
|
748
|
+
importPath = tempFile
|
|
749
|
+
// Store temp files list in a way that cleanup can access them
|
|
750
|
+
tempJsFile = allTempFiles
|
|
751
|
+
} catch (tsError) {
|
|
752
|
+
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
753
|
+
}
|
|
754
|
+
} else if (!ext) {
|
|
755
|
+
// Append .js if no extension provided (ESM resolution requires it)
|
|
756
|
+
importPath = `${importPath}.js`
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let obj
|
|
761
|
+
try {
|
|
762
|
+
obj = await import(importPath)
|
|
763
|
+
} catch (importError) {
|
|
764
|
+
// Clean up temp files if created before rethrowing
|
|
765
|
+
if (tempJsFile) {
|
|
766
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
767
|
+
cleanupTempFiles(filesToClean)
|
|
768
|
+
}
|
|
769
|
+
throw importError
|
|
770
|
+
} finally {
|
|
771
|
+
// Clean up temp files if created
|
|
772
|
+
if (tempJsFile) {
|
|
773
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
774
|
+
cleanupTempFiles(filesToClean)
|
|
775
|
+
}
|
|
681
776
|
}
|
|
682
|
-
const obj = await import(importPath)
|
|
683
777
|
|
|
684
778
|
// Handle ESM module wrapper
|
|
685
779
|
let actualObj = obj
|
package/lib/helper/GraphQL.js
CHANGED
|
@@ -45,6 +45,8 @@ class GraphQL extends Helper {
|
|
|
45
45
|
timeout: 10000,
|
|
46
46
|
defaultHeaders: {},
|
|
47
47
|
endpoint: '',
|
|
48
|
+
onRequest: null,
|
|
49
|
+
onResponse: null,
|
|
48
50
|
}
|
|
49
51
|
this.options = Object.assign(this.options, config)
|
|
50
52
|
this.headers = { ...this.options.defaultHeaders }
|
|
@@ -87,8 +89,8 @@ class GraphQL extends Helper {
|
|
|
87
89
|
|
|
88
90
|
request.headers = { ...this.headers, ...request.headers }
|
|
89
91
|
|
|
90
|
-
if (this.
|
|
91
|
-
await this.
|
|
92
|
+
if (this.options.onRequest) {
|
|
93
|
+
await this.options.onRequest(request)
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
this.debugSection('Request', JSON.stringify(request))
|
|
@@ -102,8 +104,8 @@ class GraphQL extends Helper {
|
|
|
102
104
|
response = err.response
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
if (this.
|
|
106
|
-
await this.
|
|
107
|
+
if (this.options.onResponse) {
|
|
108
|
+
await this.options.onResponse(response)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
this.debugSection('Response', JSON.stringify(response.data))
|
|
@@ -72,8 +72,8 @@ class JSONResponse extends Helper {
|
|
|
72
72
|
if (!this.helpers[this.options.requestHelper]) {
|
|
73
73
|
throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
|
|
74
74
|
}
|
|
75
|
-
const origOnResponse = this.helpers[this.options.requestHelper].
|
|
76
|
-
this.helpers[this.options.requestHelper].
|
|
75
|
+
const origOnResponse = this.helpers[this.options.requestHelper].options.onResponse
|
|
76
|
+
this.helpers[this.options.requestHelper].options.onResponse = response => {
|
|
77
77
|
this.response = response
|
|
78
78
|
if (typeof origOnResponse === 'function') origOnResponse(response)
|
|
79
79
|
}
|
|
@@ -83,7 +83,6 @@ class JSONResponse extends Helper {
|
|
|
83
83
|
this.response = null
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
87
86
|
/**
|
|
88
87
|
* Checks that response code is equal to the provided one
|
|
89
88
|
*
|
|
@@ -372,4 +371,4 @@ class JSONResponse extends Helper {
|
|
|
372
371
|
}
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
export { JSONResponse as default }
|
|
374
|
+
export { JSONResponse, JSONResponse as default }
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -355,7 +355,7 @@ class Playwright extends Helper {
|
|
|
355
355
|
this.recordingWebSocketMessages = false
|
|
356
356
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
357
357
|
this.cdpSession = null
|
|
358
|
-
|
|
358
|
+
|
|
359
359
|
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
360
360
|
// This can happen in worker threads where config is serialized/deserialized
|
|
361
361
|
let validCustomLocators = null
|
|
@@ -367,7 +367,7 @@ class Playwright extends Helper {
|
|
|
367
367
|
validCustomLocators = config.customLocatorStrategies
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
-
|
|
370
|
+
|
|
371
371
|
this.customLocatorStrategies = validCustomLocators
|
|
372
372
|
this._customLocatorsRegistered = false
|
|
373
373
|
|
|
@@ -416,6 +416,7 @@ class Playwright extends Helper {
|
|
|
416
416
|
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
417
417
|
highlightElement: false,
|
|
418
418
|
storageState: undefined,
|
|
419
|
+
onResponse: null,
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -794,10 +795,7 @@ class Playwright extends Helper {
|
|
|
794
795
|
await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
|
|
795
796
|
}
|
|
796
797
|
// Use timeout to prevent hanging (10s should be enough for browser cleanup)
|
|
797
|
-
await Promise.race([
|
|
798
|
-
this._stopBrowser(),
|
|
799
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
|
|
800
|
-
])
|
|
798
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
|
|
801
799
|
} catch (e) {
|
|
802
800
|
console.warn('Warning during browser restart in _after:', e.message)
|
|
803
801
|
// Force cleanup even on timeout
|
|
@@ -840,10 +838,7 @@ class Playwright extends Helper {
|
|
|
840
838
|
if (this.isRunning) {
|
|
841
839
|
try {
|
|
842
840
|
// Add timeout protection to prevent hanging
|
|
843
|
-
await Promise.race([
|
|
844
|
-
this._stopBrowser(),
|
|
845
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
|
|
846
|
-
])
|
|
841
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
|
|
847
842
|
} catch (e) {
|
|
848
843
|
console.warn('Warning during suite cleanup:', e.message)
|
|
849
844
|
// Track suite cleanup failures
|
|
@@ -954,10 +949,7 @@ class Playwright extends Helper {
|
|
|
954
949
|
if (this.isRunning) {
|
|
955
950
|
try {
|
|
956
951
|
// Add timeout protection to prevent hanging
|
|
957
|
-
await Promise.race([
|
|
958
|
-
this._stopBrowser(),
|
|
959
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
|
|
960
|
-
])
|
|
952
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
|
|
961
953
|
} catch (e) {
|
|
962
954
|
console.warn('Warning during final cleanup:', e.message)
|
|
963
955
|
// Force cleanup on timeout
|
|
@@ -970,10 +962,7 @@ class Playwright extends Helper {
|
|
|
970
962
|
if (this.browser) {
|
|
971
963
|
try {
|
|
972
964
|
// Add timeout protection to prevent hanging
|
|
973
|
-
await Promise.race([
|
|
974
|
-
this._stopBrowser(),
|
|
975
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
|
|
976
|
-
])
|
|
965
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
|
|
977
966
|
} catch (e) {
|
|
978
967
|
console.warn('Warning during forced cleanup:', e.message)
|
|
979
968
|
// Force cleanup on timeout
|
|
@@ -1390,7 +1379,7 @@ class Playwright extends Helper {
|
|
|
1390
1379
|
this.context = null
|
|
1391
1380
|
this.frame = null
|
|
1392
1381
|
popupStore.clear()
|
|
1393
|
-
|
|
1382
|
+
|
|
1394
1383
|
// Remove all event listeners to prevent hanging
|
|
1395
1384
|
if (this.browser) {
|
|
1396
1385
|
try {
|
|
@@ -1399,7 +1388,7 @@ class Playwright extends Helper {
|
|
|
1399
1388
|
// Ignore errors if browser is already closed
|
|
1400
1389
|
}
|
|
1401
1390
|
}
|
|
1402
|
-
|
|
1391
|
+
|
|
1403
1392
|
if (this.options.recordHar && this.browserContext) {
|
|
1404
1393
|
try {
|
|
1405
1394
|
await this.browserContext.close()
|
|
@@ -1408,16 +1397,11 @@ class Playwright extends Helper {
|
|
|
1408
1397
|
}
|
|
1409
1398
|
}
|
|
1410
1399
|
this.browserContext = null
|
|
1411
|
-
|
|
1400
|
+
|
|
1412
1401
|
if (this.browser) {
|
|
1413
1402
|
try {
|
|
1414
1403
|
// Add timeout to prevent browser.close() from hanging indefinitely
|
|
1415
|
-
await Promise.race([
|
|
1416
|
-
this.browser.close(),
|
|
1417
|
-
new Promise((_, reject) =>
|
|
1418
|
-
setTimeout(() => reject(new Error('Browser close timeout')), 5000)
|
|
1419
|
-
)
|
|
1420
|
-
])
|
|
1404
|
+
await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))])
|
|
1421
1405
|
} catch (e) {
|
|
1422
1406
|
// Ignore errors if browser is already closed or timeout
|
|
1423
1407
|
if (!e.message?.includes('Browser close timeout')) {
|
|
@@ -1539,7 +1523,7 @@ class Playwright extends Helper {
|
|
|
1539
1523
|
acceptDownloads: true,
|
|
1540
1524
|
...this.options.emulate,
|
|
1541
1525
|
}
|
|
1542
|
-
|
|
1526
|
+
|
|
1543
1527
|
try {
|
|
1544
1528
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1545
1529
|
} catch (err) {
|
|
@@ -3183,14 +3167,14 @@ class Playwright extends Helper {
|
|
|
3183
3167
|
this.debugSection('Response', await response.text())
|
|
3184
3168
|
|
|
3185
3169
|
// hook to allow JSON response handle this
|
|
3186
|
-
if (this.
|
|
3170
|
+
if (this.options.onResponse) {
|
|
3187
3171
|
const axiosResponse = {
|
|
3188
3172
|
data: await response.json(),
|
|
3189
3173
|
status: response.status(),
|
|
3190
3174
|
statusText: response.statusText(),
|
|
3191
3175
|
headers: response.headers(),
|
|
3192
3176
|
}
|
|
3193
|
-
this.
|
|
3177
|
+
this.options.onResponse(axiosResponse)
|
|
3194
3178
|
}
|
|
3195
3179
|
|
|
3196
3180
|
return response
|
|
@@ -4337,11 +4321,11 @@ function isRoleLocatorObject(locator) {
|
|
|
4337
4321
|
*/
|
|
4338
4322
|
async function handleRoleLocator(context, locator) {
|
|
4339
4323
|
if (!isRoleLocatorObject(locator)) return null
|
|
4340
|
-
|
|
4324
|
+
|
|
4341
4325
|
const options = {}
|
|
4342
4326
|
if (locator.text) options.name = locator.text
|
|
4343
4327
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4344
|
-
|
|
4328
|
+
|
|
4345
4329
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4346
4330
|
}
|
|
4347
4331
|
|
|
@@ -4350,7 +4334,7 @@ async function findElements(matcher, locator) {
|
|
|
4350
4334
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4351
4335
|
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4352
4336
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4353
|
-
|
|
4337
|
+
|
|
4354
4338
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4355
4339
|
if (isVueLocator) return findVue(matcher, locator)
|
|
4356
4340
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
@@ -4391,7 +4375,7 @@ async function findCustomElements(matcher, locator) {
|
|
|
4391
4375
|
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4392
4376
|
// and persists in every worker thread instance
|
|
4393
4377
|
let strategyFunction = null
|
|
4394
|
-
|
|
4378
|
+
|
|
4395
4379
|
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4396
4380
|
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4397
4381
|
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
@@ -4967,7 +4951,7 @@ async function refreshContextSession() {
|
|
|
4967
4951
|
this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
|
|
4968
4952
|
return
|
|
4969
4953
|
}
|
|
4970
|
-
|
|
4954
|
+
|
|
4971
4955
|
const currentUrl = await this.grabCurrentUrl()
|
|
4972
4956
|
|
|
4973
4957
|
if (currentUrl.startsWith('http')) {
|