codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.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/README.md +39 -27
- package/bin/codecept.js +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- package/typings/types.d.ts +0 -13073
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, fixErrorStack } 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,8 @@ 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
|
|
37
|
+
tsFileMapping: null, // TypeScript file mapping for error stack fixing
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -65,14 +69,15 @@ class Container {
|
|
|
65
69
|
container.support = {}
|
|
66
70
|
container.helpers = await createHelpers(config.helpers || {})
|
|
67
71
|
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
|
|
68
|
-
container.
|
|
72
|
+
container.proxySupportConfig = config.include || {}
|
|
73
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
69
74
|
container.plugins = await createPlugins(config.plugins || {}, opts)
|
|
70
75
|
container.result = new Result()
|
|
71
76
|
|
|
72
77
|
// Preload includes (so proxies can expose real objects synchronously)
|
|
73
78
|
const includes = config.include || {}
|
|
74
79
|
|
|
75
|
-
//
|
|
80
|
+
// Check if custom I is provided
|
|
76
81
|
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
|
|
77
82
|
try {
|
|
78
83
|
const mod = includes.I
|
|
@@ -84,10 +89,10 @@ class Container {
|
|
|
84
89
|
container.support.I = mod
|
|
85
90
|
}
|
|
86
91
|
} catch (e) {
|
|
87
|
-
throw
|
|
92
|
+
throw e
|
|
88
93
|
}
|
|
89
94
|
} else {
|
|
90
|
-
// Create default actor
|
|
95
|
+
// Create default actor - this sets up the callback in asyncHelperPromise
|
|
91
96
|
createActor()
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -108,6 +113,9 @@ class Container {
|
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
// Wait for all async helpers to finish loading and populate the actor
|
|
117
|
+
await asyncHelperPromise
|
|
118
|
+
|
|
111
119
|
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
112
120
|
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
113
121
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
@@ -169,6 +177,15 @@ class Container {
|
|
|
169
177
|
return container.translation
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Get TypeScript file mapping for error stack fixing
|
|
182
|
+
*
|
|
183
|
+
* @api
|
|
184
|
+
*/
|
|
185
|
+
static tsFileMapping() {
|
|
186
|
+
return container.tsFileMapping
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
/**
|
|
173
190
|
* Get Mocha instance
|
|
174
191
|
*
|
|
@@ -202,8 +219,10 @@ class Container {
|
|
|
202
219
|
|
|
203
220
|
// If new support objects are added, update the proxy support
|
|
204
221
|
if (newContainer.support) {
|
|
205
|
-
|
|
206
|
-
container.
|
|
222
|
+
// Merge the new support config with existing config
|
|
223
|
+
container.proxySupportConfig = { ...container.proxySupportConfig, ...newContainer.support }
|
|
224
|
+
// Recreate the proxy with merged config
|
|
225
|
+
container.proxySupport = createSupportObjects(container.proxySupportConfig)
|
|
207
226
|
}
|
|
208
227
|
|
|
209
228
|
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
@@ -219,6 +238,7 @@ class Container {
|
|
|
219
238
|
static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
|
|
220
239
|
container.helpers = newHelpers
|
|
221
240
|
container.translation = await loadTranslation()
|
|
241
|
+
container.proxySupportConfig = newSupport
|
|
222
242
|
container.proxySupport = createSupportObjects(newSupport)
|
|
223
243
|
container.plugins = newPlugins
|
|
224
244
|
container.sharedKeys = new Set() // Clear shared keys
|
|
@@ -248,10 +268,10 @@ class Container {
|
|
|
248
268
|
// Instead of using append which replaces the entire container,
|
|
249
269
|
// directly update the support object to maintain proxy references
|
|
250
270
|
Object.assign(container.support, data)
|
|
251
|
-
|
|
271
|
+
|
|
252
272
|
// Track which keys were explicitly shared
|
|
253
273
|
Object.keys(data).forEach(key => container.sharedKeys.add(key))
|
|
254
|
-
|
|
274
|
+
|
|
255
275
|
if (!options.local) {
|
|
256
276
|
WorkerStorage.share(data)
|
|
257
277
|
}
|
|
@@ -290,7 +310,7 @@ async function createHelpers(config) {
|
|
|
290
310
|
if (!HelperClass) {
|
|
291
311
|
const helperResult = requireHelperFromModule(helperName, config)
|
|
292
312
|
if (helperResult instanceof Promise) {
|
|
293
|
-
// Handle async ESM loading
|
|
313
|
+
// Handle async ESM loading - create placeholder
|
|
294
314
|
helpers[helperName] = {}
|
|
295
315
|
asyncHelperPromise = asyncHelperPromise
|
|
296
316
|
.then(() => helperResult)
|
|
@@ -309,8 +329,7 @@ async function createHelpers(config) {
|
|
|
309
329
|
|
|
310
330
|
checkHelperRequirements(ResolvedHelperClass)
|
|
311
331
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
312
|
-
|
|
313
|
-
debug(`helper ${helperName} async initialized`)
|
|
332
|
+
debug(`helper ${helperName} async loaded`)
|
|
314
333
|
})
|
|
315
334
|
continue
|
|
316
335
|
} else {
|
|
@@ -330,9 +349,8 @@ async function createHelpers(config) {
|
|
|
330
349
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
331
350
|
}
|
|
332
351
|
|
|
333
|
-
debug(`helper ${helperName} async initialized`)
|
|
334
|
-
|
|
335
352
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
353
|
+
debug(`helper ${helperName} async CJS loaded`)
|
|
336
354
|
})
|
|
337
355
|
|
|
338
356
|
continue
|
|
@@ -347,9 +365,18 @@ async function createHelpers(config) {
|
|
|
347
365
|
}
|
|
348
366
|
}
|
|
349
367
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
368
|
+
// Don't await here - let Container.create() handle the await
|
|
369
|
+
// This allows actor callbacks to be registered before resolution
|
|
370
|
+
asyncHelperPromise = asyncHelperPromise.then(async () => {
|
|
371
|
+
// Call _init on all helpers after they're all loaded
|
|
372
|
+
for (const name in helpers) {
|
|
373
|
+
if (helpers[name]._init) {
|
|
374
|
+
await helpers[name]._init()
|
|
375
|
+
debug(`helper ${name} _init() called`)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
353
380
|
return helpers
|
|
354
381
|
}
|
|
355
382
|
|
|
@@ -381,20 +408,66 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
381
408
|
throw err
|
|
382
409
|
}
|
|
383
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
|
+
|
|
384
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.
|
|
385
441
|
try {
|
|
386
442
|
// Try dynamic import for both CommonJS and ESM modules
|
|
387
|
-
const mod = await import(
|
|
443
|
+
const mod = await import(importPath)
|
|
388
444
|
if (!mod && !mod.default) {
|
|
389
445
|
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
|
|
390
446
|
}
|
|
391
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
|
+
}
|
|
392
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
|
+
|
|
393
466
|
if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
|
|
394
467
|
// This is an ESM module, use dynamic import
|
|
395
468
|
try {
|
|
396
469
|
const pathModule = await import('path')
|
|
397
|
-
const absolutePath = pathModule.default.resolve(
|
|
470
|
+
const absolutePath = pathModule.default.resolve(importPath)
|
|
398
471
|
const mod = await import(absolutePath)
|
|
399
472
|
HelperClass = mod.default || mod
|
|
400
473
|
debug(`helper ${helperName} loaded via ESM import`)
|
|
@@ -523,10 +596,17 @@ function createSupportObjects(config) {
|
|
|
523
596
|
return [...new Set([...keys, ...container.sharedKeys])]
|
|
524
597
|
},
|
|
525
598
|
getOwnPropertyDescriptor(target, prop) {
|
|
599
|
+
// For destructuring to work, we need to return the actual value from the getter
|
|
600
|
+
let value
|
|
601
|
+
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
602
|
+
value = container.support[prop]
|
|
603
|
+
} else {
|
|
604
|
+
value = lazyLoad(prop)
|
|
605
|
+
}
|
|
526
606
|
return {
|
|
527
607
|
enumerable: true,
|
|
528
608
|
configurable: true,
|
|
529
|
-
value:
|
|
609
|
+
value: value,
|
|
530
610
|
}
|
|
531
611
|
},
|
|
532
612
|
get(target, key) {
|
|
@@ -577,13 +657,28 @@ async function createPlugins(config, options = {}) {
|
|
|
577
657
|
const enabledPluginsByOptions = (options.plugins || '').split(',')
|
|
578
658
|
for (const pluginName in config) {
|
|
579
659
|
if (!config[pluginName]) config[pluginName] = {}
|
|
580
|
-
|
|
660
|
+
const pluginConfig = config[pluginName]
|
|
661
|
+
if (!pluginConfig.enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
|
|
581
662
|
continue // plugin is disabled
|
|
582
663
|
}
|
|
664
|
+
|
|
665
|
+
// Generic workers gate:
|
|
666
|
+
// - runInWorker / runInWorkers controls plugin execution inside worker threads.
|
|
667
|
+
// - runInParent / runInMain can disable plugin in workers parent process.
|
|
668
|
+
const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
|
|
669
|
+
const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
|
|
670
|
+
|
|
671
|
+
if (options.child && !runInWorker) {
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (!options.child && process.env.RUNS_WITH_WORKERS === 'true' && !runInParent) {
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
583
678
|
let module
|
|
584
679
|
try {
|
|
585
|
-
if (
|
|
586
|
-
module =
|
|
680
|
+
if (pluginConfig.require) {
|
|
681
|
+
module = pluginConfig.require
|
|
587
682
|
if (module.startsWith('.')) {
|
|
588
683
|
// local
|
|
589
684
|
module = path.resolve(global.codecept_dir, module) // custom plugin
|
|
@@ -593,7 +688,7 @@ async function createPlugins(config, options = {}) {
|
|
|
593
688
|
}
|
|
594
689
|
|
|
595
690
|
// Use async loading for all plugins (ESM and CJS)
|
|
596
|
-
plugins[pluginName] = await loadPluginAsync(module,
|
|
691
|
+
plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
|
|
597
692
|
debug(`plugin ${pluginName} loaded via async import`)
|
|
598
693
|
} catch (err) {
|
|
599
694
|
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
|
|
@@ -674,12 +769,64 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
674
769
|
try {
|
|
675
770
|
// Use dynamic import for both ESM and CJS modules
|
|
676
771
|
let importPath = modulePath
|
|
677
|
-
|
|
772
|
+
let tempJsFile = null
|
|
773
|
+
let fileMapping = null
|
|
774
|
+
|
|
678
775
|
if (typeof importPath === 'string') {
|
|
679
776
|
const ext = path.extname(importPath)
|
|
680
|
-
|
|
777
|
+
|
|
778
|
+
// Handle TypeScript files
|
|
779
|
+
if (ext === '.ts') {
|
|
780
|
+
try {
|
|
781
|
+
// Use the TypeScript transpilation utility
|
|
782
|
+
const typescript = await import('typescript')
|
|
783
|
+
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
|
|
784
|
+
|
|
785
|
+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
|
|
786
|
+
|
|
787
|
+
// Attach cleanup handler
|
|
788
|
+
importPath = tempFile
|
|
789
|
+
// Store temp files list in a way that cleanup can access them
|
|
790
|
+
tempJsFile = allTempFiles
|
|
791
|
+
fileMapping = mapping
|
|
792
|
+
// Store file mapping in container for runtime error fixing (merge with existing)
|
|
793
|
+
if (!container.tsFileMapping) {
|
|
794
|
+
container.tsFileMapping = new Map()
|
|
795
|
+
}
|
|
796
|
+
for (const [key, value] of mapping.entries()) {
|
|
797
|
+
container.tsFileMapping.set(key, value)
|
|
798
|
+
}
|
|
799
|
+
} catch (tsError) {
|
|
800
|
+
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
801
|
+
}
|
|
802
|
+
} else if (!ext) {
|
|
803
|
+
// Append .js if no extension provided (ESM resolution requires it)
|
|
804
|
+
importPath = `${importPath}.js`
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let obj
|
|
809
|
+
try {
|
|
810
|
+
obj = await import(importPath)
|
|
811
|
+
} catch (importError) {
|
|
812
|
+
// Fix error stack to point to original .ts files
|
|
813
|
+
if (fileMapping) {
|
|
814
|
+
fixErrorStack(importError, fileMapping)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Clean up temp files if created before rethrowing
|
|
818
|
+
if (tempJsFile) {
|
|
819
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
820
|
+
cleanupTempFiles(filesToClean)
|
|
821
|
+
}
|
|
822
|
+
throw importError
|
|
823
|
+
} finally {
|
|
824
|
+
// Clean up temp files if created
|
|
825
|
+
if (tempJsFile) {
|
|
826
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
827
|
+
cleanupTempFiles(filesToClean)
|
|
828
|
+
}
|
|
681
829
|
}
|
|
682
|
-
const obj = await import(importPath)
|
|
683
830
|
|
|
684
831
|
// Handle ESM module wrapper
|
|
685
832
|
let actualObj = obj
|
|
@@ -715,7 +862,9 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
715
862
|
|
|
716
863
|
throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
|
|
717
864
|
} catch (err) {
|
|
718
|
-
|
|
865
|
+
const newErr = new Error(`Could not include object ${supportObjectName} from module '${modulePath}': ${err.message}`)
|
|
866
|
+
newErr.stack = err.stack
|
|
867
|
+
throw newErr
|
|
719
868
|
}
|
|
720
869
|
}
|
|
721
870
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
|
+
import { simplifyHtmlElement } from '../html.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Unified WebElement class that wraps native element instances from different helpers
|
|
@@ -81,6 +82,10 @@ class WebElement {
|
|
|
81
82
|
async getProperty(name) {
|
|
82
83
|
switch (this.helperType) {
|
|
83
84
|
case 'playwright':
|
|
85
|
+
// For Locator objects, use inputValue() for the 'value' property
|
|
86
|
+
if (name === 'value' && this.element.inputValue) {
|
|
87
|
+
return this.element.inputValue()
|
|
88
|
+
}
|
|
84
89
|
return this.element.evaluate((el, propName) => el[propName], name)
|
|
85
90
|
case 'webdriver':
|
|
86
91
|
return this.element.getProperty(name)
|
|
@@ -236,10 +241,15 @@ class WebElement {
|
|
|
236
241
|
async type(text, options = {}) {
|
|
237
242
|
switch (this.helperType) {
|
|
238
243
|
case 'playwright':
|
|
244
|
+
// Playwright Locator objects use fill() instead of type()
|
|
245
|
+
if (this.element.fill) {
|
|
246
|
+
return this.element.fill(text, options)
|
|
247
|
+
}
|
|
239
248
|
return this.element.type(text, options)
|
|
240
249
|
case 'webdriver':
|
|
241
250
|
return this.element.setValue(text)
|
|
242
251
|
case 'puppeteer':
|
|
252
|
+
await this.element.evaluate(el => { el.value = '' })
|
|
243
253
|
return this.element.type(text, options)
|
|
244
254
|
default:
|
|
245
255
|
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
@@ -256,7 +266,18 @@ class WebElement {
|
|
|
256
266
|
|
|
257
267
|
switch (this.helperType) {
|
|
258
268
|
case 'playwright':
|
|
259
|
-
|
|
269
|
+
// Playwright Locator objects use locator() method
|
|
270
|
+
if (this.element.locator) {
|
|
271
|
+
const childLocator = this.element.locator(this._normalizeLocator(locator))
|
|
272
|
+
// Get the element handle from the locator
|
|
273
|
+
try {
|
|
274
|
+
childElement = await childLocator.elementHandle()
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
280
|
+
}
|
|
260
281
|
break
|
|
261
282
|
case 'webdriver':
|
|
262
283
|
try {
|
|
@@ -285,7 +306,14 @@ class WebElement {
|
|
|
285
306
|
|
|
286
307
|
switch (this.helperType) {
|
|
287
308
|
case 'playwright':
|
|
288
|
-
|
|
309
|
+
// Playwright Locator objects use locator() method
|
|
310
|
+
if (this.element.locator) {
|
|
311
|
+
const childLocator = this.element.locator(this._normalizeLocator(locator))
|
|
312
|
+
// Get all element handles from the locator
|
|
313
|
+
childElements = await childLocator.elementHandles()
|
|
314
|
+
} else {
|
|
315
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
316
|
+
}
|
|
289
317
|
break
|
|
290
318
|
case 'webdriver':
|
|
291
319
|
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
@@ -306,6 +334,57 @@ class WebElement {
|
|
|
306
334
|
* @returns {string} Normalized CSS selector
|
|
307
335
|
* @private
|
|
308
336
|
*/
|
|
337
|
+
async toAbsoluteXPath() {
|
|
338
|
+
const xpathFn = (el) => {
|
|
339
|
+
const parts = []
|
|
340
|
+
let current = el
|
|
341
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
342
|
+
let index = 0
|
|
343
|
+
let sibling = current.previousSibling
|
|
344
|
+
while (sibling) {
|
|
345
|
+
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
|
|
346
|
+
index++
|
|
347
|
+
}
|
|
348
|
+
sibling = sibling.previousSibling
|
|
349
|
+
}
|
|
350
|
+
const tagName = current.tagName.toLowerCase()
|
|
351
|
+
const pathIndex = index > 0 ? `[${index + 1}]` : ''
|
|
352
|
+
parts.unshift(`${tagName}${pathIndex}`)
|
|
353
|
+
current = current.parentElement
|
|
354
|
+
}
|
|
355
|
+
return '//' + parts.join('/')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
switch (this.helperType) {
|
|
359
|
+
case 'playwright':
|
|
360
|
+
return this.element.evaluate(xpathFn)
|
|
361
|
+
case 'puppeteer':
|
|
362
|
+
return this.element.evaluate(xpathFn)
|
|
363
|
+
case 'webdriver':
|
|
364
|
+
return this.helper.browser.execute(xpathFn, this.element)
|
|
365
|
+
default:
|
|
366
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async toOuterHTML() {
|
|
371
|
+
switch (this.helperType) {
|
|
372
|
+
case 'playwright':
|
|
373
|
+
return this.element.evaluate(el => el.outerHTML)
|
|
374
|
+
case 'puppeteer':
|
|
375
|
+
return this.element.evaluate(el => el.outerHTML)
|
|
376
|
+
case 'webdriver':
|
|
377
|
+
return this.helper.browser.execute(el => el.outerHTML, this.element)
|
|
378
|
+
default:
|
|
379
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async toSimplifiedHTML(maxLength = 300) {
|
|
384
|
+
const outerHTML = await this.toOuterHTML()
|
|
385
|
+
return simplifyHtmlElement(outerHTML, maxLength)
|
|
386
|
+
}
|
|
387
|
+
|
|
309
388
|
_normalizeLocator(locator) {
|
|
310
389
|
if (typeof locator === 'string') {
|
|
311
390
|
return locator
|
package/lib/els.js
CHANGED
|
@@ -6,10 +6,11 @@ import recordStep from './step/record.js'
|
|
|
6
6
|
import FuncStep from './step/func.js'
|
|
7
7
|
import { truth } from './assert/truth.js'
|
|
8
8
|
import { isAsyncFunction, humanizeFunction } from './utils.js'
|
|
9
|
+
import WebElement from './element/WebElement.js'
|
|
9
10
|
|
|
10
11
|
function element(purpose, locator, fn) {
|
|
11
12
|
let stepConfig
|
|
12
|
-
if (arguments[arguments.length - 1]
|
|
13
|
+
if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
|
|
13
14
|
stepConfig = arguments[arguments.length - 1]
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -28,7 +29,8 @@ function element(purpose, locator, fn) {
|
|
|
28
29
|
const els = await step.helper._locate(locator)
|
|
29
30
|
output.debug(`Found ${els.length} elements, using first element`)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
const wrapped = new WebElement(els[0], step.helper)
|
|
33
|
+
return fn(wrapped)
|
|
32
34
|
},
|
|
33
35
|
stepConfig,
|
|
34
36
|
)
|
|
@@ -52,7 +54,8 @@ function eachElement(purpose, locator, fn) {
|
|
|
52
54
|
let i = 0
|
|
53
55
|
for (const el of els) {
|
|
54
56
|
try {
|
|
55
|
-
|
|
57
|
+
const wrapped = new WebElement(el, step.helper)
|
|
58
|
+
await fn(wrapped, i)
|
|
56
59
|
} catch (err) {
|
|
57
60
|
output.error(`eachElement: failed operation on element #${i} ${el}`)
|
|
58
61
|
errs.push(err)
|
|
@@ -74,7 +77,8 @@ function expectElement(locator, fn) {
|
|
|
74
77
|
const els = await step.helper._locate(locator)
|
|
75
78
|
output.debug(`Found ${els.length} elements, first will be used for assertion`)
|
|
76
79
|
|
|
77
|
-
const
|
|
80
|
+
const wrapped = new WebElement(els[0], step.helper)
|
|
81
|
+
const result = await fn(wrapped)
|
|
78
82
|
const assertion = truth(`element (${locator})`, fn.toString())
|
|
79
83
|
assertion.assert(result)
|
|
80
84
|
})
|
|
@@ -92,7 +96,8 @@ function expectAnyElement(locator, fn) {
|
|
|
92
96
|
|
|
93
97
|
let found = false
|
|
94
98
|
for (const el of els) {
|
|
95
|
-
const
|
|
99
|
+
const wrapped = new WebElement(el, step.helper)
|
|
100
|
+
const result = await fn(wrapped)
|
|
96
101
|
if (result) {
|
|
97
102
|
found = true
|
|
98
103
|
break
|
|
@@ -113,7 +118,8 @@ function expectAllElements(locator, fn) {
|
|
|
113
118
|
let i = 1
|
|
114
119
|
for (const el of els) {
|
|
115
120
|
output.debug(`checking element #${i}: ${el}`)
|
|
116
|
-
const
|
|
121
|
+
const wrapped = new WebElement(el, step.helper)
|
|
122
|
+
const result = await fn(wrapped)
|
|
117
123
|
const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
|
|
118
124
|
assertion.assert(result)
|
|
119
125
|
i++
|
package/lib/helper/Appium.js
CHANGED
|
@@ -1543,8 +1543,8 @@ class Appium extends Webdriver {
|
|
|
1543
1543
|
/**
|
|
1544
1544
|
* {{> dontSeeElement }}
|
|
1545
1545
|
*/
|
|
1546
|
-
async dontSeeElement(locator) {
|
|
1547
|
-
if (this.isWeb) return super.dontSeeElement(locator)
|
|
1546
|
+
async dontSeeElement(locator, context = null) {
|
|
1547
|
+
if (this.isWeb) return super.dontSeeElement(locator, context)
|
|
1548
1548
|
|
|
1549
1549
|
// For mobile native apps, use safe isDisplayed wrapper
|
|
1550
1550
|
const parsedLocator = parseLocator.call(this, locator)
|
|
@@ -1589,9 +1589,9 @@ class Appium extends Webdriver {
|
|
|
1589
1589
|
* {{> fillField }}
|
|
1590
1590
|
*
|
|
1591
1591
|
*/
|
|
1592
|
-
async fillField(field, value) {
|
|
1592
|
+
async fillField(field, value, context = null) {
|
|
1593
1593
|
value = value.toString()
|
|
1594
|
-
if (this.isWeb) return super.fillField(field, value)
|
|
1594
|
+
if (this.isWeb) return super.fillField(field, value, context)
|
|
1595
1595
|
return super.fillField(parseLocator.call(this, field), value)
|
|
1596
1596
|
}
|
|
1597
1597
|
|
|
@@ -1706,8 +1706,8 @@ class Appium extends Webdriver {
|
|
|
1706
1706
|
* {{> seeElement }}
|
|
1707
1707
|
*
|
|
1708
1708
|
*/
|
|
1709
|
-
async seeElement(locator) {
|
|
1710
|
-
if (this.isWeb) return super.seeElement(locator)
|
|
1709
|
+
async seeElement(locator, context = null) {
|
|
1710
|
+
if (this.isWeb) return super.seeElement(locator, context)
|
|
1711
1711
|
|
|
1712
1712
|
// For mobile native apps, use safe isDisplayed wrapper
|
|
1713
1713
|
const parsedLocator = parseLocator.call(this, locator)
|
|
@@ -1754,8 +1754,8 @@ class Appium extends Webdriver {
|
|
|
1754
1754
|
*
|
|
1755
1755
|
* Supported only for web testing
|
|
1756
1756
|
*/
|
|
1757
|
-
async selectOption(select, option) {
|
|
1758
|
-
if (this.isWeb) return super.selectOption(select, option)
|
|
1757
|
+
async selectOption(select, option, context = null) {
|
|
1758
|
+
if (this.isWeb) return super.selectOption(select, option, context)
|
|
1759
1759
|
throw new Error("Should be used only in Web context. In native context use 'click' method instead")
|
|
1760
1760
|
}
|
|
1761
1761
|
|
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 }
|