codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.9.esm-aria

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 (68) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +105 -16
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +59 -47
  66. package/typings/index.d.ts +19 -7
  67. package/typings/promiseBasedTypes.d.ts +5534 -3764
  68. package/typings/types.d.ts +5789 -3775
@@ -36,6 +36,7 @@ import { highlightElement } from './scripts/highlightElement.js'
36
36
  import { blurElement } from './scripts/blurElement.js'
37
37
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
38
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
39
+ import WebElement from '../element/WebElement.js'
39
40
 
40
41
  let puppeteer
41
42
 
@@ -74,7 +75,7 @@ const consoleLogStore = new Console()
74
75
  * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to false.
75
76
  * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to false.
76
77
  * @prop {number} [waitForAction=100] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
77
- * @prop {string} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitfornavigationoptions). Array values are accepted as well.
78
+ * @prop {string|string[]} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.waitforoptions.md). Array values are accepted as well.
78
79
  * @prop {number} [pressKeyDelay=10] - delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField
79
80
  * @prop {number} [getPageTimeout=30000] - config option to set maximum navigation time in milliseconds. If the timeout is set to 0, then timeout will be disabled.
80
81
  * @prop {number} [waitForTimeout=1000] - default wait* timeout in ms.
@@ -82,13 +83,13 @@ const consoleLogStore = new Console()
82
83
  * @prop {string} [userAgent] - user-agent string.
83
84
  * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
84
85
  * @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
85
- * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
86
+ * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.launchoptions.md).
86
87
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
87
88
  */
88
89
  const config = {}
89
90
 
90
91
  /**
91
- * Uses [Google Chrome's Puppeteer](https://github.com/GoogleChrome/puppeteer) library to run tests inside headless Chrome.
92
+ * Uses [Google Chrome's Puppeteer](https://github.com/puppeteer/puppeteer) library to run tests inside headless Chrome.
92
93
  * Browser control is executed via DevTools Protocol (instead of Selenium).
93
94
  * This helper works with a browser out of the box with no additional tools required to install.
94
95
  *
@@ -361,6 +362,9 @@ class Puppeteer extends Helper {
361
362
  async _after() {
362
363
  if (!this.isRunning) return
363
364
 
365
+ // Clear popup state to prevent leakage between tests
366
+ popupStore.clear()
367
+
364
368
  // close other sessions
365
369
  const contexts = this.browser.browserContexts()
366
370
  const defaultCtx = contexts.shift()
@@ -432,9 +436,27 @@ class Puppeteer extends Helper {
432
436
  } else {
433
437
  this.activeSessionName = session
434
438
  }
439
+
435
440
  const defaultCtx = this.browser.defaultBrowserContext()
436
- const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
437
- await this._setPage(await existingPages[0].page())
441
+ if (!defaultCtx) {
442
+ this.debug('Cannot restore session vars: default browser context is undefined')
443
+ return
444
+ }
445
+
446
+ try {
447
+ const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
448
+ if (existingPages && existingPages.length > 0) {
449
+ await this._setPage(await existingPages[0].page())
450
+ // Reset context-related variables to ensure clean state after session
451
+ this.context = await this.page
452
+ this.contextLocator = null
453
+ } else {
454
+ this.debug('Cannot restore session vars: no pages available')
455
+ }
456
+ } catch (err) {
457
+ this.debug(`Failed to restore session vars: ${err.message}`)
458
+ return
459
+ }
438
460
 
439
461
  return this._waitForAction()
440
462
  },
@@ -650,9 +672,14 @@ class Puppeteer extends Helper {
650
672
  }
651
673
  }
652
674
 
653
- async _evaluateHandeInContext(...args) {
675
+ async _evaluateHandeInContext(fn, handle, ...args) {
676
+ // If handle is provided, evaluate directly on it to avoid "JavaScript world" errors
677
+ if (handle) {
678
+ return handle.evaluate(fn, ...args)
679
+ }
680
+ // Otherwise use the context
654
681
  const context = await this._getContext()
655
- return context.evaluateHandle(...args)
682
+ return context.evaluateHandle(fn, ...args)
656
683
  }
657
684
 
658
685
  async _withinBegin(locator) {
@@ -671,16 +698,22 @@ class Puppeteer extends Helper {
671
698
  return
672
699
  }
673
700
 
674
- const els = await this._locate(locator)
675
- assertElementExists(els, locator)
676
- this.context = els[0]
701
+ const el = await this._locateElement(locator)
702
+ if (!el) {
703
+ throw new ElementNotFound(locator, 'Element for within context')
704
+ }
705
+ this.context = el
677
706
 
678
707
  this.withinLocator = new Locator(locator)
679
708
  }
680
709
 
681
710
  async _withinEnd() {
682
711
  this.withinLocator = null
683
- this.context = await this.page.mainFrame().$('body')
712
+ if (this.page && !this.page.isClosed?.()) {
713
+ this.context = await this.page.mainFrame().$('body')
714
+ } else {
715
+ this.context = null
716
+ }
684
717
  }
685
718
 
686
719
  _extractDataFromPerformanceTiming(timing, ...dataNames) {
@@ -781,11 +814,13 @@ class Puppeteer extends Helper {
781
814
  * {{ react }}
782
815
  */
783
816
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
784
- const els = await this._locate(locator)
785
- assertElementExists(els, locator)
817
+ const el = await this._locateElement(locator)
818
+ if (!el) {
819
+ throw new ElementNotFound(locator, 'Element to move cursor to')
820
+ }
786
821
 
787
822
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
788
- const { x, y } = await getClickablePoint(els[0])
823
+ const { x, y } = await getClickablePoint(el)
789
824
  await this.page.mouse.move(x + offsetX, y + offsetY)
790
825
  return this._waitForAction()
791
826
  }
@@ -795,9 +830,10 @@ class Puppeteer extends Helper {
795
830
  *
796
831
  */
797
832
  async focus(locator) {
798
- const els = await this._locate(locator)
799
- assertElementExists(els, locator, 'Element to focus')
800
- const el = els[0]
833
+ const el = await this._locateElement(locator)
834
+ if (!el) {
835
+ throw new ElementNotFound(locator, 'Element to focus')
836
+ }
801
837
 
802
838
  await el.click()
803
839
  await el.focus()
@@ -809,10 +845,12 @@ class Puppeteer extends Helper {
809
845
  *
810
846
  */
811
847
  async blur(locator) {
812
- const els = await this._locate(locator)
813
- assertElementExists(els, locator, 'Element to blur')
848
+ const el = await this._locateElement(locator)
849
+ if (!el) {
850
+ throw new ElementNotFound(locator, 'Element to blur')
851
+ }
814
852
 
815
- await blurElement(els[0], this.page)
853
+ await blurElement(el, this.page)
816
854
  return this._waitForAction()
817
855
  }
818
856
 
@@ -861,11 +899,12 @@ class Puppeteer extends Helper {
861
899
  }
862
900
 
863
901
  if (locator) {
864
- const els = await this._locate(locator)
865
- assertElementExists(els, locator, 'Element')
866
- const el = els[0]
902
+ const el = await this._locateElement(locator)
903
+ if (!el) {
904
+ throw new ElementNotFound(locator, 'Element to scroll into view')
905
+ }
867
906
  await el.evaluate(el => el.scrollIntoView())
868
- const elementCoordinates = await getClickablePoint(els[0])
907
+ const elementCoordinates = await getClickablePoint(el)
869
908
  await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
870
909
  } else {
871
910
  await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
@@ -933,6 +972,21 @@ class Puppeteer extends Helper {
933
972
  return findElements.call(this, context, locator)
934
973
  }
935
974
 
975
+ /**
976
+ * Get single element by different locator types, including strict locator
977
+ * Should be used in custom helpers:
978
+ *
979
+ * ```js
980
+ * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
981
+ * ```
982
+ *
983
+ * {{ react }}
984
+ */
985
+ async _locateElement(locator) {
986
+ const context = await this.context
987
+ return findElement.call(this, context, locator)
988
+ }
989
+
936
990
  /**
937
991
  * Find a checkbox by providing human-readable text:
938
992
  * NOTE: Assumes the checkable element exists
@@ -944,7 +998,9 @@ class Puppeteer extends Helper {
944
998
  async _locateCheckable(locator, providedContext = null) {
945
999
  const context = providedContext || (await this._getContext())
946
1000
  const els = await findCheckable.call(this, locator, context)
947
- assertElementExists(els[0], locator, 'Checkbox or radio')
1001
+ if (!els || els.length === 0) {
1002
+ throw new ElementNotFound(locator, 'Checkbox or radio')
1003
+ }
948
1004
  return els[0]
949
1005
  }
950
1006
 
@@ -976,7 +1032,20 @@ class Puppeteer extends Helper {
976
1032
  *
977
1033
  */
978
1034
  async grabWebElements(locator) {
979
- return this._locate(locator)
1035
+ const elements = await this._locate(locator)
1036
+ return elements.map(element => new WebElement(element, this))
1037
+ }
1038
+
1039
+ /**
1040
+ * {{> grabWebElement }}
1041
+ *
1042
+ */
1043
+ async grabWebElement(locator) {
1044
+ const elements = await this._locate(locator)
1045
+ if (elements.length === 0) {
1046
+ throw new ElementNotFound(locator, 'Element')
1047
+ }
1048
+ return new WebElement(elements[0], this)
980
1049
  }
981
1050
 
982
1051
  async grabWebElement(locator) {
@@ -1146,7 +1215,7 @@ class Puppeteer extends Helper {
1146
1215
  *
1147
1216
  * {{ react }}
1148
1217
  */
1149
- async click(locator, context = null) {
1218
+ async click(locator = '//body', context = null) {
1150
1219
  return proceedClick.call(this, locator, context)
1151
1220
  }
1152
1221
 
@@ -1306,6 +1375,49 @@ class Puppeteer extends Helper {
1306
1375
  return proceedClick.call(this, locator, context, { button: 'right' })
1307
1376
  }
1308
1377
 
1378
+ /**
1379
+ * Performs click at specific coordinates.
1380
+ * If locator is provided, the coordinates are relative to the element.
1381
+ * If locator is not provided, the coordinates are global page coordinates.
1382
+ *
1383
+ * ```js
1384
+ * // Click at global coordinates (100, 200)
1385
+ * I.clickXY(100, 200);
1386
+ *
1387
+ * // Click at coordinates (50, 30) relative to element
1388
+ * I.clickXY('#someElement', 50, 30);
1389
+ * ```
1390
+ *
1391
+ * @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
1392
+ * @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
1393
+ * @param {number} [y] Y coordinate relative to element.
1394
+ * @returns {Promise<void>}
1395
+ */
1396
+ async clickXY(locator, x, y) {
1397
+ // If locator is a number, treat it as global X coordinate
1398
+ if (typeof locator === 'number') {
1399
+ const globalX = locator
1400
+ const globalY = x
1401
+ await this.page.mouse.click(globalX, globalY)
1402
+ return this._waitForAction()
1403
+ }
1404
+
1405
+ // Locator is provided, click relative to element
1406
+ const els = await this._locate(locator)
1407
+ assertElementExists(els, locator, 'Element to click')
1408
+
1409
+ const box = await els[0].boundingBox()
1410
+ if (!box) {
1411
+ throw new Error(`Element ${locator} is not visible or has no bounding box`)
1412
+ }
1413
+
1414
+ const absoluteX = box.x + x
1415
+ const absoluteY = box.y + y
1416
+
1417
+ await this.page.mouse.click(absoluteX, absoluteY)
1418
+ return this._waitForAction()
1419
+ }
1420
+
1309
1421
  /**
1310
1422
  * {{> checkOption }}
1311
1423
  */
@@ -1381,7 +1493,7 @@ class Puppeteer extends Helper {
1381
1493
  }
1382
1494
 
1383
1495
  /**
1384
- * _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
1496
+ * _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([puppeteer/puppeteer#1313](https://github.com/puppeteer/puppeteer/issues/1313)).
1385
1497
  *
1386
1498
  * {{> pressKeyWithKeyNormalization }}
1387
1499
  */
@@ -1840,7 +1952,7 @@ class Puppeteer extends Helper {
1840
1952
  */
1841
1953
  async grabHTMLFromAll(locator) {
1842
1954
  const els = await this._locate(locator)
1843
- const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML, el)))
1955
+ const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML)))
1844
1956
  return values
1845
1957
  }
1846
1958
 
@@ -1863,7 +1975,7 @@ class Puppeteer extends Helper {
1863
1975
  */
1864
1976
  async grabCssPropertyFromAll(locator, cssProperty) {
1865
1977
  const els = await this._locate(locator)
1866
- const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)))
1978
+ const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))))))
1867
1979
  const cssValues = res.map(props => props[toCamelCase(cssProperty)])
1868
1980
 
1869
1981
  return cssValues
@@ -1988,7 +2100,7 @@ class Puppeteer extends Helper {
1988
2100
  const array = []
1989
2101
  for (let index = 0; index < els.length; index++) {
1990
2102
  const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
1991
- array.push(await a.jsonValue())
2103
+ array.push(a)
1992
2104
  }
1993
2105
  return array
1994
2106
  }
@@ -2030,6 +2142,12 @@ class Puppeteer extends Helper {
2030
2142
 
2031
2143
  this.debug(`Screenshot is saving to ${outputFile}`)
2032
2144
 
2145
+ // Safety check: ensure page exists and is not closed
2146
+ if (!this.page || this.page.isClosed?.()) {
2147
+ this.debugSection('Screenshot', 'Page is not available, skipping screenshot')
2148
+ return
2149
+ }
2150
+
2033
2151
  await this.page.screenshot({
2034
2152
  path: outputFile,
2035
2153
  fullPage: fullPageOption,
@@ -2043,7 +2161,7 @@ class Puppeteer extends Helper {
2043
2161
 
2044
2162
  this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2045
2163
 
2046
- if (activeSessionPage) {
2164
+ if (activeSessionPage && !activeSessionPage.isClosed?.()) {
2047
2165
  await activeSessionPage.screenshot({
2048
2166
  path: outputFile,
2049
2167
  fullPage: fullPageOption,
@@ -2184,12 +2302,13 @@ class Puppeteer extends Helper {
2184
2302
  * {{> waitForClickable }}
2185
2303
  */
2186
2304
  async waitForClickable(locator, waitTimeout) {
2187
- const els = await this._locate(locator)
2188
- assertElementExists(els, locator)
2305
+ const el = await this._locateElement(locator)
2306
+ if (!el) {
2307
+ throw new ElementNotFound(locator, 'Element to wait for clickable')
2308
+ }
2189
2309
 
2190
- return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async e => {
2191
- const errorMessage = e?.message || String(e)
2192
- if (/Waiting failed/i.test(errorMessage) || /failed: timeout/i.test(errorMessage)) {
2310
+ return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
2311
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2193
2312
  throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
2194
2313
  } else {
2195
2314
  throw e
@@ -2494,7 +2613,7 @@ class Puppeteer extends Helper {
2494
2613
  /**
2495
2614
  * Waits for navigation to finish. By default, takes configured `waitForNavigation` option.
2496
2615
  *
2497
- * See [Puppeteer's reference](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitfornavigationoptions)
2616
+ * See [Puppeteer's reference](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.page.waitfornavigation.md)
2498
2617
  *
2499
2618
  * @param {*} opts
2500
2619
  */
@@ -2622,7 +2741,8 @@ class Puppeteer extends Helper {
2622
2741
  *
2623
2742
  * {{> stopRecordingTraffic }}
2624
2743
  */
2625
- stopRecordingTraffic() {
2744
+ async stopRecordingTraffic() {
2745
+ await this.page.setRequestInterception(false)
2626
2746
  stopRecordingTraffic.call(this)
2627
2747
  }
2628
2748
 
@@ -2760,27 +2880,98 @@ class Puppeteer extends Helper {
2760
2880
  }
2761
2881
  }
2762
2882
 
2763
- async function findElements(matcher, locator) {
2764
- const matchedLocator = new Locator(locator, 'css')
2765
-
2766
- if (matchedLocator.type === 'react') return findReactElements.call(this, matchedLocator)
2767
- if (matchedLocator.isRole()) return findByRole.call(this, matcher, matchedLocator)
2768
-
2769
- if (!matchedLocator.isXPath()) return matcher.$$(matchedLocator.simplify())
2883
+ export default Puppeteer
2770
2884
 
2771
- // Handle backward compatibility for different Puppeteer versions
2772
- // Puppeteer >= 19.4.0 uses xpath/ syntax, older versions use $x
2773
- try {
2774
- // Try the new xpath syntax first (for Puppeteer >= 19.4.0)
2775
- return await matcher.$$(`xpath/${matchedLocator.value}`)
2776
- } catch (error) {
2777
- // Fall back to the old $x method for older Puppeteer versions
2778
- if (matcher.$x && typeof matcher.$x === 'function') {
2779
- return await matcher.$x(matchedLocator.value)
2885
+ /**
2886
+ * Find elements using Puppeteer's native element discovery methods
2887
+ * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
2888
+ * @param {Object} matcher - Puppeteer context to search within
2889
+ * @param {Object|string} locator - Locator specification
2890
+ * @returns {Promise<Array>} Array of ElementHandle objects
2891
+ */
2892
+ async function findElements(matcher, locator) {
2893
+ // Check if locator is a Locator object with react type, or a raw object with react property
2894
+ const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
2895
+ if (isReactLocator) return findReactElements.call(this, locator)
2896
+
2897
+ locator = new Locator(locator, 'css')
2898
+
2899
+ // Check if locator is a role locator and call findByRole
2900
+ if (locator.isRole()) return findByRole.call(this, matcher, locator)
2901
+
2902
+ // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
2903
+ if (!locator.isXPath()) return matcher.$$(locator.simplify())
2904
+
2905
+ // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2906
+ if (puppeteer.default?.defaultBrowserRevision) {
2907
+ return matcher.$$(`xpath/${locator.value}`)
2908
+ }
2909
+
2910
+ // For Puppeteer 24.x+, $x method was removed
2911
+ // Use ::-p-xpath() selector syntax
2912
+ // Check if matcher has $$ method (Page, Frame, or ElementHandle)
2913
+ if (matcher && typeof matcher.$$ === 'function') {
2914
+ const xpathSelector = `::-p-xpath(${locator.value})`
2915
+ try {
2916
+ return await matcher.$$(xpathSelector)
2917
+ } catch (e) {
2918
+ // XPath selector may not work on ElementHandle, fall through to evaluate method
2919
+ this.debug && this.debug(`XPath selector failed on ${matcher.constructor?.name}: ${e.message}`)
2780
2920
  }
2781
- // If both methods fail, re-throw the original error
2782
- throw error
2783
2921
  }
2922
+
2923
+ // ElementHandles don't support XPath directly // Search within the element by making XPath relative
2924
+ try {
2925
+ const relativeXPath = locator.value.startsWith('.//') ? locator.value : `.//${locator.value.replace(/^\/\//, '')}`
2926
+
2927
+ // Use the element as context by evaluating XPath from it
2928
+ const elements = await matcher.evaluateHandle((element, xpath) => {
2929
+ const iterator = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
2930
+ const results = []
2931
+ for (let i = 0; i < iterator.snapshotLength; i++) {
2932
+ results.push(iterator.snapshotItem(i))
2933
+ }
2934
+ return results
2935
+ }, relativeXPath)
2936
+
2937
+ // Convert JSHandle to array of ElementHandles
2938
+ const properties = await elements.getProperties()
2939
+ return Array.from(properties.values())
2940
+ } catch (e) {
2941
+ this.debug(`XPath within element failed: ${e.message}`)
2942
+ }
2943
+
2944
+ // Fallback: return empty array
2945
+ return []
2946
+ }
2947
+
2948
+ /**
2949
+ * Find a single element using Puppeteer's native element discovery methods
2950
+ * Note: Puppeteer Locator API doesn't have .first() method like Playwright
2951
+ * @param {Object} matcher - Puppeteer context to search within
2952
+ * @param {Object|string} locator - Locator specification
2953
+ * @returns {Promise<Object>} Single ElementHandle object
2954
+ */
2955
+ async function findElement(matcher, locator) {
2956
+ if (locator.react) return findReactElements.call(this, locator)
2957
+ locator = new Locator(locator, 'css')
2958
+
2959
+ // Check if locator is a role locator and call findByRole
2960
+ if (locator.isRole()) {
2961
+ const elements = await findByRole.call(this, matcher, locator)
2962
+ return elements[0]
2963
+ }
2964
+
2965
+ // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2966
+ if (!locator.isXPath()) {
2967
+ const elements = await matcher.$$(locator.simplify())
2968
+ return elements[0]
2969
+ }
2970
+
2971
+ // For XPath in Puppeteer 24.x+, use the same approach as findElements
2972
+ // $x method was removed, so we use ::-p-xpath() or fallback
2973
+ const elements = await findElements.call(this, matcher, locator)
2974
+ return elements[0]
2784
2975
  }
2785
2976
 
2786
2977
  async function proceedClick(locator, context = null, options = {}) {
@@ -2970,15 +3161,19 @@ async function findFields(locator) {
2970
3161
  }
2971
3162
 
2972
3163
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2973
- const src = await this._locate(sourceLocator)
2974
- assertElementExists(src, sourceLocator, 'Source Element')
3164
+ const src = await this._locateElement(sourceLocator)
3165
+ if (!src) {
3166
+ throw new ElementNotFound(sourceLocator, 'Source Element')
3167
+ }
2975
3168
 
2976
- const dst = await this._locate(destinationLocator)
2977
- assertElementExists(dst, destinationLocator, 'Destination Element')
3169
+ const dst = await this._locateElement(destinationLocator)
3170
+ if (!dst) {
3171
+ throw new ElementNotFound(destinationLocator, 'Destination Element')
3172
+ }
2978
3173
 
2979
- // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
2980
- const dragSource = await getClickablePoint(src[0])
2981
- const dragDestination = await getClickablePoint(dst[0])
3174
+ // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
3175
+ const dragSource = await getClickablePoint(src)
3176
+ const dragDestination = await getClickablePoint(dst)
2982
3177
 
2983
3178
  // Drag start point
2984
3179
  await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
@@ -3148,7 +3343,7 @@ async function getClickablePoint(el) {
3148
3343
  }
3149
3344
 
3150
3345
  // List of key values to key definitions
3151
- // https://github.com/GoogleChrome/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
3346
+ // https://github.com/puppeteer/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
3152
3347
  const keyDefinitionMap = {
3153
3348
  0: 'Digit0',
3154
3349
  1: 'Digit1',
@@ -3226,7 +3421,9 @@ function _waitForElement(locator, options) {
3226
3421
  }
3227
3422
 
3228
3423
  async function findReactElements(locator) {
3229
- const resolved = toLocatorConfig(locator, 'react')
3424
+ // Handle both Locator objects and raw locator objects
3425
+ const resolved = locator.locator ? locator.locator : toLocatorConfig(locator, 'react')
3426
+ this.debug(`Finding React elements: ${JSON.stringify(resolved)}`)
3230
3427
 
3231
3428
  // Use createRequire to access require.resolve in ESM
3232
3429
  const { createRequire } = await import('module')
@@ -3341,4 +3538,3 @@ function createRoleTextMatcher(expected, exactMatch) {
3341
3538
  return value => typeof value === 'string' && value.includes(target)
3342
3539
  }
3343
3540
 
3344
- export { Puppeteer as default }
@@ -298,6 +298,27 @@ class REST extends Helper {
298
298
  return this._executeRequest(request)
299
299
  }
300
300
 
301
+ /**
302
+ * Send HEAD request to REST API
303
+ *
304
+ * ```js
305
+ * I.sendHeadRequest('/api/users.json');
306
+ * ```
307
+ *
308
+ * @param {*} url
309
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
310
+ *
311
+ * @returns {Promise<*>} response
312
+ */
313
+ async sendHeadRequest(url, headers = {}) {
314
+ const request = {
315
+ baseURL: this._url(url),
316
+ method: 'HEAD',
317
+ headers,
318
+ }
319
+ return this._executeRequest(request)
320
+ }
321
+
301
322
  /**
302
323
  * Sends POST request to API.
303
324
  *