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.
Files changed (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. 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.proxySupport = createSupportObjects(config.include || {})
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
- // Ensure I is available for DI modules at import time
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 new Error(`Could not include object I: ${e.message}`)
92
+ throw e
88
93
  }
89
94
  } else {
90
- // Create default actor if not provided via includes
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
- const newProxySupport = createSupportObjects(newContainer.support)
206
- container.proxySupport = { ...container.proxySupport, ...newProxySupport }
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
- if (helpers[helperName]._init) await helpers[helperName]._init()
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
- for (const name in helpers) {
351
- if (helpers[name]._init) await helpers[name]._init()
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(moduleName)
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(moduleName)
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: target[prop],
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
- if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
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 (config[pluginName].require) {
586
- module = config[pluginName].require
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, config[pluginName])
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
- // Append .js if no extension provided (ESM resolution requires it)
772
+ let tempJsFile = null
773
+ let fileMapping = null
774
+
678
775
  if (typeof importPath === 'string') {
679
776
  const ext = path.extname(importPath)
680
- if (!ext) importPath = `${importPath}.js`
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
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
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
- childElement = await this.element.$(this._normalizeLocator(locator))
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
- childElements = await this.element.$$(this._normalizeLocator(locator))
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] instanceof StepConfig) {
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
- return fn(els[0])
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
- await fn(el, i)
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 result = await fn(els[0])
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 result = await fn(el)
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 result = await fn(el)
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++
@@ -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
 
@@ -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.config.onRequest) {
91
- await this.config.onRequest(request)
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.config.onResponse) {
106
- await this.config.onResponse(response)
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].config.onResponse
76
- this.helpers[this.options.requestHelper].config.onResponse = response => {
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 }