codeceptjs 4.0.0-beta.5 → 4.0.0-beta.6.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 (179) hide show
  1. package/README.md +0 -45
  2. package/bin/codecept.js +46 -57
  3. package/lib/actor.js +15 -11
  4. package/lib/ai.js +6 -5
  5. package/lib/assert/empty.js +9 -8
  6. package/lib/assert/equal.js +15 -17
  7. package/lib/assert/error.js +2 -2
  8. package/lib/assert/include.js +9 -11
  9. package/lib/assert/throws.js +1 -1
  10. package/lib/assert/truth.js +8 -5
  11. package/lib/assert.js +18 -18
  12. package/lib/codecept.js +66 -107
  13. package/lib/colorUtils.js +48 -50
  14. package/lib/command/check.js +32 -27
  15. package/lib/command/configMigrate.js +11 -10
  16. package/lib/command/definitions.js +16 -10
  17. package/lib/command/dryRun.js +16 -16
  18. package/lib/command/generate.js +29 -26
  19. package/lib/command/gherkin/init.js +36 -38
  20. package/lib/command/gherkin/snippets.js +14 -14
  21. package/lib/command/gherkin/steps.js +21 -18
  22. package/lib/command/info.js +8 -8
  23. package/lib/command/init.js +34 -31
  24. package/lib/command/interactive.js +11 -10
  25. package/lib/command/list.js +10 -9
  26. package/lib/command/run-multiple/chunk.js +5 -5
  27. package/lib/command/run-multiple/collection.js +5 -5
  28. package/lib/command/run-multiple/run.js +3 -3
  29. package/lib/command/run-multiple.js +16 -13
  30. package/lib/command/run-rerun.js +6 -7
  31. package/lib/command/run-workers.js +10 -24
  32. package/lib/command/run.js +8 -8
  33. package/lib/command/utils.js +20 -18
  34. package/lib/command/workers/runTests.js +117 -269
  35. package/lib/config.js +111 -49
  36. package/lib/container.js +299 -102
  37. package/lib/data/context.js +6 -5
  38. package/lib/data/dataScenarioConfig.js +1 -1
  39. package/lib/data/dataTableArgument.js +1 -1
  40. package/lib/data/table.js +1 -1
  41. package/lib/effects.js +94 -10
  42. package/lib/els.js +11 -9
  43. package/lib/event.js +11 -10
  44. package/lib/globals.js +141 -0
  45. package/lib/heal.js +12 -12
  46. package/lib/helper/AI.js +1 -1
  47. package/lib/helper/ApiDataFactory.js +16 -13
  48. package/lib/helper/FileSystem.js +32 -12
  49. package/lib/helper/GraphQL.js +1 -1
  50. package/lib/helper/GraphQLDataFactory.js +1 -1
  51. package/lib/helper/JSONResponse.js +19 -30
  52. package/lib/helper/Mochawesome.js +9 -28
  53. package/lib/helper/Playwright.js +668 -265
  54. package/lib/helper/Puppeteer.js +284 -169
  55. package/lib/helper/REST.js +29 -12
  56. package/lib/helper/WebDriver.js +191 -71
  57. package/lib/helper/errors/ConnectionRefused.js +6 -6
  58. package/lib/helper/errors/ElementAssertion.js +11 -16
  59. package/lib/helper/errors/ElementNotFound.js +5 -9
  60. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  61. package/lib/helper/extras/Console.js +11 -11
  62. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  63. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  64. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  65. package/lib/helper/extras/Popup.js +1 -1
  66. package/lib/helper/extras/React.js +29 -30
  67. package/lib/helper/network/actions.js +33 -48
  68. package/lib/helper/network/utils.js +76 -83
  69. package/lib/helper/scripts/blurElement.js +6 -6
  70. package/lib/helper/scripts/focusElement.js +6 -6
  71. package/lib/helper/scripts/highlightElement.js +9 -9
  72. package/lib/helper/scripts/isElementClickable.js +34 -34
  73. package/lib/helper.js +2 -1
  74. package/lib/history.js +23 -20
  75. package/lib/hooks.js +10 -10
  76. package/lib/html.js +90 -100
  77. package/lib/index.js +48 -21
  78. package/lib/listener/config.js +8 -9
  79. package/lib/listener/emptyRun.js +6 -7
  80. package/lib/listener/exit.js +4 -3
  81. package/lib/listener/globalRetry.js +5 -5
  82. package/lib/listener/globalTimeout.js +11 -10
  83. package/lib/listener/helpers.js +33 -14
  84. package/lib/listener/mocha.js +3 -4
  85. package/lib/listener/result.js +4 -5
  86. package/lib/listener/steps.js +7 -18
  87. package/lib/listener/store.js +3 -3
  88. package/lib/locator.js +213 -192
  89. package/lib/mocha/asyncWrapper.js +108 -75
  90. package/lib/mocha/bdd.js +99 -13
  91. package/lib/mocha/cli.js +60 -27
  92. package/lib/mocha/factory.js +75 -19
  93. package/lib/mocha/featureConfig.js +1 -1
  94. package/lib/mocha/gherkin.js +57 -25
  95. package/lib/mocha/hooks.js +12 -3
  96. package/lib/mocha/index.js +13 -4
  97. package/lib/mocha/inject.js +22 -5
  98. package/lib/mocha/scenarioConfig.js +2 -2
  99. package/lib/mocha/suite.js +9 -2
  100. package/lib/mocha/test.js +10 -13
  101. package/lib/mocha/ui.js +28 -31
  102. package/lib/output.js +11 -9
  103. package/lib/parser.js +44 -44
  104. package/lib/pause.js +15 -16
  105. package/lib/plugin/analyze.js +19 -12
  106. package/lib/plugin/auth.js +20 -21
  107. package/lib/plugin/autoDelay.js +12 -8
  108. package/lib/plugin/coverage.js +12 -8
  109. package/lib/plugin/customLocator.js +3 -3
  110. package/lib/plugin/customReporter.js +3 -2
  111. package/lib/plugin/heal.js +14 -9
  112. package/lib/plugin/pageInfo.js +10 -10
  113. package/lib/plugin/pauseOnFail.js +4 -3
  114. package/lib/plugin/retryFailedStep.js +47 -5
  115. package/lib/plugin/screenshotOnFail.js +75 -37
  116. package/lib/plugin/stepByStepReport.js +14 -14
  117. package/lib/plugin/stepTimeout.js +4 -3
  118. package/lib/plugin/subtitles.js +6 -5
  119. package/lib/recorder.js +33 -23
  120. package/lib/rerun.js +69 -26
  121. package/lib/result.js +4 -4
  122. package/lib/secret.js +18 -17
  123. package/lib/session.js +95 -89
  124. package/lib/step/base.js +6 -6
  125. package/lib/step/config.js +1 -1
  126. package/lib/step/func.js +3 -3
  127. package/lib/step/helper.js +3 -3
  128. package/lib/step/meta.js +4 -4
  129. package/lib/step/record.js +11 -11
  130. package/lib/step/retry.js +3 -3
  131. package/lib/step/section.js +3 -3
  132. package/lib/step.js +7 -10
  133. package/lib/steps.js +9 -5
  134. package/lib/store.js +1 -1
  135. package/lib/timeout.js +1 -7
  136. package/lib/transform.js +8 -8
  137. package/lib/translation.js +32 -18
  138. package/lib/utils.js +68 -97
  139. package/lib/workerStorage.js +16 -17
  140. package/lib/workers.js +145 -171
  141. package/package.json +63 -57
  142. package/translations/de-DE.js +2 -2
  143. package/translations/fr-FR.js +2 -2
  144. package/translations/index.js +23 -10
  145. package/translations/it-IT.js +2 -2
  146. package/translations/ja-JP.js +2 -2
  147. package/translations/nl-NL.js +2 -2
  148. package/translations/pl-PL.js +2 -2
  149. package/translations/pt-BR.js +2 -2
  150. package/translations/ru-RU.js +2 -2
  151. package/translations/utils.js +11 -2
  152. package/translations/zh-CN.js +2 -2
  153. package/translations/zh-TW.js +2 -2
  154. package/typings/index.d.ts +7 -18
  155. package/typings/promiseBasedTypes.d.ts +3769 -5450
  156. package/typings/types.d.ts +3953 -5778
  157. package/bin/test-server.js +0 -53
  158. package/lib/element/WebElement.js +0 -327
  159. package/lib/helper/Nightmare.js +0 -1486
  160. package/lib/helper/Protractor.js +0 -1840
  161. package/lib/helper/TestCafe.js +0 -1391
  162. package/lib/helper/clientscripts/nightmare.js +0 -213
  163. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  164. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  165. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  166. package/lib/listener/retryEnhancer.js +0 -85
  167. package/lib/plugin/allure.js +0 -15
  168. package/lib/plugin/autoLogin.js +0 -5
  169. package/lib/plugin/commentStep.js +0 -141
  170. package/lib/plugin/eachElement.js +0 -127
  171. package/lib/plugin/fakerTransform.js +0 -49
  172. package/lib/plugin/htmlReporter.js +0 -1947
  173. package/lib/plugin/retryTo.js +0 -16
  174. package/lib/plugin/selenoid.js +0 -364
  175. package/lib/plugin/standardActingHelpers.js +0 -6
  176. package/lib/plugin/tryTo.js +0 -16
  177. package/lib/plugin/wdio.js +0 -247
  178. package/lib/test-server.js +0 -323
  179. package/lib/within.js +0 -90
@@ -1,21 +1,19 @@
1
- const axios = require('axios')
2
- const fs = require('fs')
3
- const fsExtra = require('fs-extra')
4
- const path = require('path')
5
-
6
- const Helper = require('@codeceptjs/helper')
7
- const { v4: uuidv4 } = require('uuid')
8
- const promiseRetry = require('promise-retry')
9
- const Locator = require('../locator')
10
- const recorder = require('../recorder')
11
- const store = require('../store')
12
- const stringIncludes = require('../assert/include').includes
13
- const { urlEquals } = require('../assert/equal')
14
- const { equals } = require('../assert/equal')
15
- const { empty } = require('../assert/empty')
16
- const { truth } = require('../assert/truth')
17
- const isElementClickable = require('./scripts/isElementClickable')
18
- const {
1
+ import axios from 'axios'
2
+ import fs from 'fs'
3
+ import fsExtra from 'fs-extra'
4
+ import path from 'path'
5
+ import Helper from '@codeceptjs/helper'
6
+ import { v4 as uuidv4 } from 'uuid'
7
+ import promiseRetry from 'promise-retry'
8
+ import Locator from '../locator.js'
9
+ import recorder from '../recorder.js'
10
+ import store from '../store.js'
11
+ import { includes as stringIncludes } from '../assert/include.js'
12
+ import { urlEquals, equals } from '../assert/equal.js'
13
+ import { empty } from '../assert/empty.js'
14
+ import { truth } from '../assert/truth.js'
15
+ import isElementClickable from './scripts/isElementClickable.js'
16
+ import {
19
17
  xpathLocator,
20
18
  ucfirst,
21
19
  fileExists,
@@ -28,19 +26,31 @@ const {
28
26
  isModifierKey,
29
27
  requireWithFallback,
30
28
  normalizeSpacesInString,
31
- } = require('../utils')
32
- const { isColorProperty, convertColorToRGBA } = require('../colorUtils')
33
- const ElementNotFound = require('./errors/ElementNotFound')
34
- const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused')
35
- const Popup = require('./extras/Popup')
36
- const Console = require('./extras/Console')
37
- const { highlightElement } = require('./scripts/highlightElement')
38
- const { blurElement } = require('./scripts/blurElement')
39
- const { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
40
- const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
41
- const WebElement = require('../element/WebElement')
29
+ } from '../utils.js'
30
+ import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
+ import ElementNotFound from './errors/ElementNotFound.js'
32
+ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
33
+ import Popup from './extras/Popup.js'
34
+ import Console from './extras/Console.js'
35
+ import { highlightElement } from './scripts/highlightElement.js'
36
+ import { blurElement } from './scripts/blurElement.js'
37
+ import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
+ import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
42
39
 
43
40
  let puppeteer
41
+
42
+ /**
43
+ * Wraps error objects that don't have a proper message property
44
+ * This is needed for ESM compatibility with Puppeteer error handling
45
+ */
46
+ function wrapError(e) {
47
+ if (e && typeof e === 'object' && !e.message) {
48
+ const err = new Error(String(e))
49
+ err.stack = e.stack
50
+ return err
51
+ }
52
+ return e
53
+ }
44
54
  let perfTiming
45
55
  const popupStore = new Popup()
46
56
  const consoleLogStore = new Console()
@@ -214,7 +224,7 @@ class Puppeteer extends Helper {
214
224
  constructor(config) {
215
225
  super(config)
216
226
 
217
- puppeteer = requireWithFallback('puppeteer', 'puppeteer-core')
227
+ // puppeteer will be loaded dynamically in _init method
218
228
  // set defaults
219
229
  this.isRemoteBrowser = false
220
230
  this.isRunning = false
@@ -294,13 +304,34 @@ class Puppeteer extends Helper {
294
304
 
295
305
  static _checkRequirements() {
296
306
  try {
297
- requireWithFallback('puppeteer', 'puppeteer-core')
307
+ // In ESM, puppeteer will be checked via dynamic import in _init
308
+ // The import will fail at module load time if puppeteer is missing
309
+ return null
298
310
  } catch (e) {
299
311
  return ['puppeteer']
300
312
  }
301
313
  }
302
314
 
303
- _init() {}
315
+ async _init() {
316
+ // Load puppeteer dynamically with fallback
317
+ if (!puppeteer) {
318
+ try {
319
+ const puppeteerModule = await import('puppeteer')
320
+ puppeteer = puppeteerModule.default || puppeteerModule
321
+ this.debugSection('Puppeteer', `Loaded puppeteer successfully, launch available: ${!!puppeteer.launch}`)
322
+ } catch (e) {
323
+ try {
324
+ const puppeteerModule = await import('puppeteer-core')
325
+ puppeteer = puppeteerModule.default || puppeteerModule
326
+ this.debugSection('Puppeteer', `Loaded puppeteer-core successfully, launch available: ${!!puppeteer.launch}`)
327
+ } catch (e2) {
328
+ throw new Error('Neither puppeteer nor puppeteer-core could be loaded. Please install one of them.')
329
+ }
330
+ }
331
+ } else {
332
+ this.debugSection('Puppeteer', `Puppeteer already loaded, launch available: ${!!puppeteer.launch}`)
333
+ }
334
+ }
304
335
 
305
336
  _beforeSuite() {
306
337
  if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
@@ -563,6 +594,12 @@ class Puppeteer extends Helper {
563
594
  }
564
595
 
565
596
  async _startBrowser() {
597
+ this.debugSection('Puppeteer', `Starting browser. Puppeteer available: ${!!puppeteer}, launch available: ${!!puppeteer?.launch}`)
598
+
599
+ if (!puppeteer) {
600
+ throw new Error('Puppeteer is not loaded. Make sure _init() was called before _startBrowser()')
601
+ }
602
+
566
603
  if (this.isRemoteBrowser) {
567
604
  try {
568
605
  this.browser = await puppeteer.connect(this.puppeteerOptions)
@@ -634,11 +671,9 @@ class Puppeteer extends Helper {
634
671
  return
635
672
  }
636
673
 
637
- const el = await this._locateElement(locator)
638
- if (!el) {
639
- throw new ElementNotFound(locator, 'Element for within context')
640
- }
641
- this.context = el
674
+ const els = await this._locate(locator)
675
+ assertElementExists(els, locator)
676
+ this.context = els[0]
642
677
 
643
678
  this.withinLocator = new Locator(locator)
644
679
  }
@@ -682,7 +717,21 @@ class Puppeteer extends Helper {
682
717
  this.currentRunningTest.artifacts.trace = fileName
683
718
  }
684
719
 
685
- await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
720
+ try {
721
+ await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
722
+ } catch (err) {
723
+ // Handle terminal navigation errors that shouldn't be retried
724
+ if (
725
+ err.message &&
726
+ (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed') || err.message.includes('Navigation timeout'))
727
+ ) {
728
+ // Mark this as a terminal error to prevent retries
729
+ const terminalError = new Error(err.message)
730
+ terminalError.isTerminal = true
731
+ throw terminalError
732
+ }
733
+ throw err
734
+ }
686
735
 
687
736
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
688
737
 
@@ -732,13 +781,11 @@ class Puppeteer extends Helper {
732
781
  * {{ react }}
733
782
  */
734
783
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
735
- const el = await this._locateElement(locator)
736
- if (!el) {
737
- throw new ElementNotFound(locator, 'Element to move cursor to')
738
- }
784
+ const els = await this._locate(locator)
785
+ assertElementExists(els, locator)
739
786
 
740
787
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
741
- const { x, y } = await getClickablePoint(el)
788
+ const { x, y } = await getClickablePoint(els[0])
742
789
  await this.page.mouse.move(x + offsetX, y + offsetY)
743
790
  return this._waitForAction()
744
791
  }
@@ -748,10 +795,9 @@ class Puppeteer extends Helper {
748
795
  *
749
796
  */
750
797
  async focus(locator) {
751
- const el = await this._locateElement(locator)
752
- if (!el) {
753
- throw new ElementNotFound(locator, 'Element to focus')
754
- }
798
+ const els = await this._locate(locator)
799
+ assertElementExists(els, locator, 'Element to focus')
800
+ const el = els[0]
755
801
 
756
802
  await el.click()
757
803
  await el.focus()
@@ -763,12 +809,10 @@ class Puppeteer extends Helper {
763
809
  *
764
810
  */
765
811
  async blur(locator) {
766
- const el = await this._locateElement(locator)
767
- if (!el) {
768
- throw new ElementNotFound(locator, 'Element to blur')
769
- }
812
+ const els = await this._locate(locator)
813
+ assertElementExists(els, locator, 'Element to blur')
770
814
 
771
- await blurElement(el, this.page)
815
+ await blurElement(els[0], this.page)
772
816
  return this._waitForAction()
773
817
  }
774
818
 
@@ -817,12 +861,11 @@ class Puppeteer extends Helper {
817
861
  }
818
862
 
819
863
  if (locator) {
820
- const el = await this._locateElement(locator)
821
- if (!el) {
822
- throw new ElementNotFound(locator, 'Element to scroll into view')
823
- }
864
+ const els = await this._locate(locator)
865
+ assertElementExists(els, locator, 'Element')
866
+ const el = els[0]
824
867
  await el.evaluate(el => el.scrollIntoView())
825
- const elementCoordinates = await getClickablePoint(el)
868
+ const elementCoordinates = await getClickablePoint(els[0])
826
869
  await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
827
870
  } else {
828
871
  await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
@@ -890,21 +933,6 @@ class Puppeteer extends Helper {
890
933
  return findElements.call(this, context, locator)
891
934
  }
892
935
 
893
- /**
894
- * Get single element by different locator types, including strict locator
895
- * Should be used in custom helpers:
896
- *
897
- * ```js
898
- * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
899
- * ```
900
- *
901
- * {{ react }}
902
- */
903
- async _locateElement(locator) {
904
- const context = await this.context
905
- return findElement.call(this, context, locator)
906
- }
907
-
908
936
  /**
909
937
  * Find a checkbox by providing human-readable text:
910
938
  * NOTE: Assumes the checkable element exists
@@ -916,9 +944,7 @@ class Puppeteer extends Helper {
916
944
  async _locateCheckable(locator, providedContext = null) {
917
945
  const context = providedContext || (await this._getContext())
918
946
  const els = await findCheckable.call(this, locator, context)
919
- if (!els || els.length === 0) {
920
- throw new ElementNotFound(locator, 'Checkbox or radio')
921
- }
947
+ assertElementExists(els[0], locator, 'Checkbox or radio')
922
948
  return els[0]
923
949
  }
924
950
 
@@ -950,20 +976,13 @@ class Puppeteer extends Helper {
950
976
  *
951
977
  */
952
978
  async grabWebElements(locator) {
953
- const elements = await this._locate(locator)
954
- return elements.map(element => new WebElement(element, this))
979
+ return this._locate(locator)
955
980
  }
956
981
 
957
- /**
958
- * {{> grabWebElement }}
959
- *
960
- */
961
982
  async grabWebElement(locator) {
962
- const elements = await this._locate(locator)
963
- if (elements.length === 0) {
964
- throw new ElementNotFound(locator, 'Element')
965
- }
966
- return new WebElement(elements[0], this)
983
+ const els = await this._locate(locator)
984
+ assertElementExists(els, locator)
985
+ return els[0]
967
986
  }
968
987
 
969
988
  /**
@@ -1292,8 +1311,16 @@ class Puppeteer extends Helper {
1292
1311
  */
1293
1312
  async checkOption(field, context = null) {
1294
1313
  const elm = await this._locateCheckable(field, context)
1295
- const curentlyChecked = await elm.getProperty('checked').then(checkedProperty => checkedProperty.jsonValue())
1296
- // Only check if NOT currently checked
1314
+ let curentlyChecked = await elm
1315
+ .getProperty('checked')
1316
+ .then(checkedProperty => checkedProperty.jsonValue())
1317
+ .catch(() => null)
1318
+
1319
+ if (!curentlyChecked) {
1320
+ const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
1321
+ curentlyChecked = ariaChecked === 'true'
1322
+ }
1323
+
1297
1324
  if (!curentlyChecked) {
1298
1325
  await elm.click()
1299
1326
  return this._waitForAction()
@@ -1305,8 +1332,16 @@ class Puppeteer extends Helper {
1305
1332
  */
1306
1333
  async uncheckOption(field, context = null) {
1307
1334
  const elm = await this._locateCheckable(field, context)
1308
- const curentlyChecked = await elm.getProperty('checked').then(checkedProperty => checkedProperty.jsonValue())
1309
- // Only uncheck if currently checked
1335
+ let curentlyChecked = await elm
1336
+ .getProperty('checked')
1337
+ .then(checkedProperty => checkedProperty.jsonValue())
1338
+ .catch(() => null)
1339
+
1340
+ if (!curentlyChecked) {
1341
+ const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
1342
+ curentlyChecked = ariaChecked === 'true'
1343
+ }
1344
+
1310
1345
  if (curentlyChecked) {
1311
1346
  await elm.click()
1312
1347
  return this._waitForAction()
@@ -2149,13 +2184,12 @@ class Puppeteer extends Helper {
2149
2184
  * {{> waitForClickable }}
2150
2185
  */
2151
2186
  async waitForClickable(locator, waitTimeout) {
2152
- const el = await this._locateElement(locator)
2153
- if (!el) {
2154
- throw new ElementNotFound(locator, 'Element to wait for clickable')
2155
- }
2187
+ const els = await this._locate(locator)
2188
+ assertElementExists(els, locator)
2156
2189
 
2157
- return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
2158
- if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
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)) {
2159
2193
  throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
2160
2194
  } else {
2161
2195
  throw e
@@ -2726,51 +2760,27 @@ class Puppeteer extends Helper {
2726
2760
  }
2727
2761
  }
2728
2762
 
2729
- module.exports = Puppeteer
2730
-
2731
- /**
2732
- * Find elements using Puppeteer's native element discovery methods
2733
- * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
2734
- * @param {Object} matcher - Puppeteer context to search within
2735
- * @param {Object|string} locator - Locator specification
2736
- * @returns {Promise<Array>} Array of ElementHandle objects
2737
- */
2738
2763
  async function findElements(matcher, locator) {
2739
- if (locator.react) return findReactElements.call(this, locator)
2740
- locator = new Locator(locator, 'css')
2764
+ const matchedLocator = new Locator(locator, 'css')
2741
2765
 
2742
- // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
2743
- if (!locator.isXPath()) return matcher.$$(locator.simplify())
2744
- // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2745
- if (puppeteer.default?.defaultBrowserRevision) {
2746
- return matcher.$$(`xpath/${locator.value}`)
2747
- }
2748
- return matcher.$x(locator.value)
2749
- }
2766
+ if (matchedLocator.type === 'react') return findReactElements.call(this, matchedLocator)
2767
+ if (matchedLocator.isRole()) return findByRole.call(this, matcher, matchedLocator)
2750
2768
 
2751
- /**
2752
- * Find a single element using Puppeteer's native element discovery methods
2753
- * Note: Puppeteer Locator API doesn't have .first() method like Playwright
2754
- * @param {Object} matcher - Puppeteer context to search within
2755
- * @param {Object|string} locator - Locator specification
2756
- * @returns {Promise<Object>} Single ElementHandle object
2757
- */
2758
- async function findElement(matcher, locator) {
2759
- if (locator.react) return findReactElements.call(this, locator)
2760
- locator = new Locator(locator, 'css')
2761
-
2762
- // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2763
- if (!locator.isXPath()) {
2764
- const elements = await matcher.$$(locator.simplify())
2765
- return elements[0]
2766
- }
2767
- // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2768
- if (puppeteer.default?.defaultBrowserRevision) {
2769
- const elements = await matcher.$$(`xpath/${locator.value}`)
2770
- return elements[0]
2771
- }
2772
- const elements = await matcher.$x(locator.value)
2773
- return elements[0]
2769
+ if (!matchedLocator.isXPath()) return matcher.$$(matchedLocator.simplify())
2770
+
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)
2780
+ }
2781
+ // If both methods fail, re-throw the original error
2782
+ throw error
2783
+ }
2774
2784
  }
2775
2785
 
2776
2786
  async function proceedClick(locator, context = null, options = {}) {
@@ -2800,12 +2810,12 @@ async function proceedClick(locator, context = null, options = {}) {
2800
2810
  }
2801
2811
 
2802
2812
  async function findClickable(matcher, locator) {
2803
- if (locator.react) return findReactElements.call(this, locator)
2804
- locator = new Locator(locator)
2805
- if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
2813
+ const matchedLocator = new Locator(locator)
2814
+
2815
+ if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
2806
2816
 
2807
2817
  let els
2808
- const literal = xpathLocator.literal(locator.value)
2818
+ const literal = xpathLocator.literal(matchedLocator.value)
2809
2819
 
2810
2820
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
2811
2821
  if (els.length) return els
@@ -2820,7 +2830,15 @@ async function findClickable(matcher, locator) {
2820
2830
  // Do nothing
2821
2831
  }
2822
2832
 
2823
- return findElements.call(this, matcher, locator.value) // by css or xpath
2833
+ // Try ARIA selector for accessible name
2834
+ try {
2835
+ els = await matcher.$$(`::-p-aria(${matchedLocator.value})`)
2836
+ if (els.length) return els
2837
+ } catch (err) {
2838
+ // ARIA selector not supported or failed
2839
+ }
2840
+
2841
+ return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
2824
2842
  }
2825
2843
 
2826
2844
  async function proceedSee(assertType, text, context, strict = false) {
@@ -2864,10 +2882,10 @@ async function findCheckable(locator, context) {
2864
2882
 
2865
2883
  const matchedLocator = new Locator(locator)
2866
2884
  if (!matchedLocator.isFuzzy()) {
2867
- return findElements.call(this, contextEl, matchedLocator.simplify())
2885
+ return findElements.call(this, contextEl, matchedLocator)
2868
2886
  }
2869
2887
 
2870
- const literal = xpathLocator.literal(locator)
2888
+ const literal = xpathLocator.literal(matchedLocator.value)
2871
2889
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
2872
2890
  if (els.length) {
2873
2891
  return els
@@ -2876,15 +2894,39 @@ async function findCheckable(locator, context) {
2876
2894
  if (els.length) {
2877
2895
  return els
2878
2896
  }
2879
- return findElements.call(this, contextEl, locator)
2897
+
2898
+ // Try ARIA selector for accessible name
2899
+ try {
2900
+ els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
2901
+ if (els.length) return els
2902
+ } catch (err) {
2903
+ // ARIA selector not supported or failed
2904
+ }
2905
+
2906
+ return findElements.call(this, contextEl, matchedLocator.value)
2880
2907
  }
2881
2908
 
2882
2909
  async function proceedIsChecked(assertType, option) {
2883
2910
  let els = await findCheckable.call(this, option)
2884
2911
  assertElementExists(els, option, 'Checkable')
2885
- els = await Promise.all(els.map(el => el.getProperty('checked')))
2886
- els = await Promise.all(els.map(el => el.jsonValue()))
2887
- const selected = els.reduce((prev, cur) => prev || cur)
2912
+
2913
+ const checkedStates = await Promise.all(
2914
+ els.map(async el => {
2915
+ const checked = await el
2916
+ .getProperty('checked')
2917
+ .then(p => p.jsonValue())
2918
+ .catch(() => null)
2919
+
2920
+ if (checked) {
2921
+ return checked
2922
+ }
2923
+
2924
+ const ariaChecked = await el.evaluate(el => el.getAttribute('aria-checked'))
2925
+ return ariaChecked === 'true'
2926
+ }),
2927
+ )
2928
+
2929
+ const selected = checkedStates.reduce((prev, cur) => prev || cur)
2888
2930
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
2889
2931
  }
2890
2932
 
@@ -2899,7 +2941,7 @@ async function findFields(locator) {
2899
2941
  if (!matchedLocator.isFuzzy()) {
2900
2942
  return this._locate(matchedLocator)
2901
2943
  }
2902
- const literal = xpathLocator.literal(locator)
2944
+ const literal = xpathLocator.literal(matchedLocator.value)
2903
2945
 
2904
2946
  let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
2905
2947
  if (els.length) {
@@ -2914,23 +2956,29 @@ async function findFields(locator) {
2914
2956
  if (els.length) {
2915
2957
  return els
2916
2958
  }
2917
- return this._locate({ css: locator })
2959
+
2960
+ // Try ARIA selector for accessible name
2961
+ try {
2962
+ const page = await this.context
2963
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
2964
+ if (els.length) return els
2965
+ } catch (err) {
2966
+ // ARIA selector not supported or failed
2967
+ }
2968
+
2969
+ return this._locate({ css: matchedLocator.value })
2918
2970
  }
2919
2971
 
2920
2972
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2921
- const src = await this._locateElement(sourceLocator)
2922
- if (!src) {
2923
- throw new ElementNotFound(sourceLocator, 'Source Element')
2924
- }
2973
+ const src = await this._locate(sourceLocator)
2974
+ assertElementExists(src, sourceLocator, 'Source Element')
2925
2975
 
2926
- const dst = await this._locateElement(destinationLocator)
2927
- if (!dst) {
2928
- throw new ElementNotFound(destinationLocator, 'Destination Element')
2929
- }
2976
+ const dst = await this._locate(destinationLocator)
2977
+ assertElementExists(dst, destinationLocator, 'Destination Element')
2930
2978
 
2931
- // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
2932
- const dragSource = await getClickablePoint(src)
2933
- const dragDestination = await getClickablePoint(dst)
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])
2934
2982
 
2935
2983
  // Drag start point
2936
2984
  await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
@@ -2992,19 +3040,30 @@ async function proceedSeeInField(assertType, field, value) {
2992
3040
  }
2993
3041
  return proceedMultiple(els[0])
2994
3042
  }
2995
- const fieldVal = await el.getProperty('value').then(el => el.jsonValue())
3043
+
3044
+ let fieldVal = await el.getProperty('value').then(el => el.jsonValue())
3045
+
3046
+ if (fieldVal === undefined || fieldVal === null) {
3047
+ fieldVal = await el.evaluate(el => el.textContent || el.innerText)
3048
+ }
3049
+
2996
3050
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
2997
3051
  }
2998
3052
 
2999
3053
  async function filterFieldsByValue(elements, value, onlySelected) {
3000
3054
  const matches = []
3001
3055
  for (const element of elements) {
3002
- const val = await element.getProperty('value').then(el => el.jsonValue())
3056
+ let val = await element.getProperty('value').then(el => el.jsonValue())
3057
+
3058
+ if (val === undefined || val === null) {
3059
+ val = await element.evaluate(el => el.textContent || el.innerText)
3060
+ }
3061
+
3003
3062
  let isSelected = true
3004
3063
  if (onlySelected) {
3005
3064
  isSelected = await elementSelected(element)
3006
3065
  }
3007
- if ((value == null || val.indexOf(value) > -1) && isSelected) {
3066
+ if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
3008
3067
  matches.push(element)
3009
3068
  }
3010
3069
  }
@@ -3166,7 +3225,12 @@ function _waitForElement(locator, options) {
3166
3225
  }
3167
3226
  }
3168
3227
 
3169
- async function findReactElements(locator, props = {}, state = {}) {
3228
+ async function findReactElements(locator) {
3229
+ const resolved = toLocatorConfig(locator, 'react')
3230
+
3231
+ // Use createRequire to access require.resolve in ESM
3232
+ const { createRequire } = await import('module')
3233
+ const require = createRequire(import.meta.url)
3170
3234
  const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
3171
3235
  await this.page.evaluate(resqScript.toString())
3172
3236
 
@@ -3209,9 +3273,9 @@ async function findReactElements(locator, props = {}, state = {}) {
3209
3273
  return [...nodes]
3210
3274
  },
3211
3275
  {
3212
- selector: locator.react,
3213
- props: locator.props || {},
3214
- state: locator.state || {},
3276
+ selector: resolved.react,
3277
+ props: resolved.props || {},
3278
+ state: resolved.state || {},
3215
3279
  },
3216
3280
  )
3217
3281
 
@@ -3227,3 +3291,54 @@ async function findReactElements(locator, props = {}, state = {}) {
3227
3291
  await arrayHandle.dispose()
3228
3292
  return result
3229
3293
  }
3294
+
3295
+ async function findByRole(matcher, locator) {
3296
+ const resolved = toLocatorConfig(locator, 'role')
3297
+ const roleSelector = buildRoleSelector(resolved)
3298
+
3299
+ if (!resolved.text && !resolved.name) {
3300
+ return matcher.$$(roleSelector)
3301
+ }
3302
+
3303
+ const allElements = await matcher.$$(roleSelector)
3304
+ const filtered = []
3305
+ const accessibleName = resolved.text ?? resolved.name
3306
+ const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
3307
+
3308
+ for (const el of allElements) {
3309
+ const texts = await el.evaluate(e => {
3310
+ const ariaLabel = e.hasAttribute('aria-label') ? e.getAttribute('aria-label') : ''
3311
+ const labelText = e.id ? document.querySelector(`label[for="${e.id}"]`)?.textContent.trim() || '' : ''
3312
+ const placeholder = e.getAttribute('placeholder') || ''
3313
+ const innerText = e.innerText ? e.innerText.trim() : ''
3314
+ return [ariaLabel || labelText, placeholder, innerText]
3315
+ })
3316
+
3317
+ if (texts.some(text => matcherFn(text))) filtered.push(el)
3318
+ }
3319
+
3320
+ return filtered
3321
+ }
3322
+
3323
+ function toLocatorConfig(locator, key) {
3324
+ const matchedLocator = new Locator(locator, key)
3325
+ if (matchedLocator.locator) return matchedLocator.locator
3326
+ return { [key]: matchedLocator.value }
3327
+ }
3328
+
3329
+ function buildRoleSelector(resolved) {
3330
+ return `::-p-aria([role="${resolved.role}"])`
3331
+ }
3332
+
3333
+ function createRoleTextMatcher(expected, exactMatch) {
3334
+ if (expected instanceof RegExp) {
3335
+ return value => expected.test(value || '')
3336
+ }
3337
+ const target = String(expected)
3338
+ if (exactMatch) {
3339
+ return value => value === target
3340
+ }
3341
+ return value => typeof value === 'string' && value.includes(target)
3342
+ }
3343
+
3344
+ export { Puppeteer as default }