codeceptjs 3.7.6-beta.4 → 4.0.0-beta.10.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 (191) hide show
  1. package/README.md +1 -3
  2. package/bin/codecept.js +51 -53
  3. package/bin/test-server.js +14 -3
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/actor.js +15 -11
  6. package/lib/ai.js +72 -107
  7. package/lib/assert/empty.js +9 -8
  8. package/lib/assert/equal.js +15 -17
  9. package/lib/assert/error.js +2 -2
  10. package/lib/assert/include.js +9 -11
  11. package/lib/assert/throws.js +1 -1
  12. package/lib/assert/truth.js +8 -5
  13. package/lib/assert.js +18 -18
  14. package/lib/codecept.js +102 -75
  15. package/lib/colorUtils.js +48 -50
  16. package/lib/command/check.js +32 -27
  17. package/lib/command/configMigrate.js +11 -10
  18. package/lib/command/definitions.js +16 -10
  19. package/lib/command/dryRun.js +16 -16
  20. package/lib/command/generate.js +62 -27
  21. package/lib/command/gherkin/init.js +36 -38
  22. package/lib/command/gherkin/snippets.js +14 -14
  23. package/lib/command/gherkin/steps.js +21 -18
  24. package/lib/command/info.js +8 -8
  25. package/lib/command/init.js +36 -29
  26. package/lib/command/interactive.js +11 -10
  27. package/lib/command/list.js +10 -9
  28. package/lib/command/run-multiple/chunk.js +5 -5
  29. package/lib/command/run-multiple/collection.js +5 -5
  30. package/lib/command/run-multiple/run.js +3 -3
  31. package/lib/command/run-multiple.js +16 -13
  32. package/lib/command/run-rerun.js +6 -7
  33. package/lib/command/run-workers.js +24 -9
  34. package/lib/command/run.js +23 -8
  35. package/lib/command/utils.js +20 -18
  36. package/lib/command/workers/runTests.js +197 -114
  37. package/lib/config.js +124 -51
  38. package/lib/container.js +438 -87
  39. package/lib/data/context.js +6 -5
  40. package/lib/data/dataScenarioConfig.js +1 -1
  41. package/lib/data/dataTableArgument.js +1 -1
  42. package/lib/data/table.js +1 -1
  43. package/lib/effects.js +94 -10
  44. package/lib/element/WebElement.js +2 -2
  45. package/lib/els.js +11 -9
  46. package/lib/event.js +11 -10
  47. package/lib/globals.js +141 -0
  48. package/lib/heal.js +12 -12
  49. package/lib/helper/AI.js +11 -11
  50. package/lib/helper/ApiDataFactory.js +50 -19
  51. package/lib/helper/Appium.js +19 -27
  52. package/lib/helper/FileSystem.js +32 -12
  53. package/lib/helper/GraphQL.js +3 -3
  54. package/lib/helper/GraphQLDataFactory.js +4 -4
  55. package/lib/helper/JSONResponse.js +25 -29
  56. package/lib/helper/Mochawesome.js +7 -4
  57. package/lib/helper/Playwright.js +902 -164
  58. package/lib/helper/Puppeteer.js +383 -76
  59. package/lib/helper/REST.js +29 -12
  60. package/lib/helper/WebDriver.js +268 -61
  61. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +18 -9
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
  71. package/lib/helper/extras/Popup.js +1 -1
  72. package/lib/helper/extras/React.js +29 -30
  73. package/lib/helper/network/actions.js +29 -44
  74. package/lib/helper/network/utils.js +76 -83
  75. package/lib/helper/scripts/blurElement.js +6 -6
  76. package/lib/helper/scripts/focusElement.js +6 -6
  77. package/lib/helper/scripts/highlightElement.js +9 -9
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -1
  80. package/lib/history.js +23 -20
  81. package/lib/hooks.js +10 -10
  82. package/lib/html.js +90 -100
  83. package/lib/index.js +48 -21
  84. package/lib/listener/config.js +19 -12
  85. package/lib/listener/emptyRun.js +6 -7
  86. package/lib/listener/enhancedGlobalRetry.js +6 -6
  87. package/lib/listener/exit.js +4 -3
  88. package/lib/listener/globalRetry.js +5 -5
  89. package/lib/listener/globalTimeout.js +30 -14
  90. package/lib/listener/helpers.js +39 -14
  91. package/lib/listener/mocha.js +3 -4
  92. package/lib/listener/result.js +4 -5
  93. package/lib/listener/retryEnhancer.js +3 -3
  94. package/lib/listener/steps.js +8 -7
  95. package/lib/listener/store.js +3 -3
  96. package/lib/locator.js +213 -192
  97. package/lib/mocha/asyncWrapper.js +105 -62
  98. package/lib/mocha/bdd.js +99 -13
  99. package/lib/mocha/cli.js +59 -26
  100. package/lib/mocha/factory.js +78 -19
  101. package/lib/mocha/featureConfig.js +1 -1
  102. package/lib/mocha/gherkin.js +56 -24
  103. package/lib/mocha/hooks.js +12 -3
  104. package/lib/mocha/index.js +13 -4
  105. package/lib/mocha/inject.js +22 -5
  106. package/lib/mocha/scenarioConfig.js +2 -2
  107. package/lib/mocha/suite.js +9 -2
  108. package/lib/mocha/test.js +10 -7
  109. package/lib/mocha/ui.js +28 -18
  110. package/lib/output.js +10 -8
  111. package/lib/parser.js +44 -44
  112. package/lib/pause.js +15 -16
  113. package/lib/plugin/analyze.js +19 -12
  114. package/lib/plugin/auth.js +20 -21
  115. package/lib/plugin/autoDelay.js +12 -8
  116. package/lib/plugin/coverage.js +28 -11
  117. package/lib/plugin/customLocator.js +3 -3
  118. package/lib/plugin/customReporter.js +3 -2
  119. package/lib/plugin/enhancedRetryFailedStep.js +6 -6
  120. package/lib/plugin/heal.js +14 -9
  121. package/lib/plugin/htmlReporter.js +724 -99
  122. package/lib/plugin/pageInfo.js +10 -10
  123. package/lib/plugin/pauseOnFail.js +4 -3
  124. package/lib/plugin/retryFailedStep.js +48 -5
  125. package/lib/plugin/screenshotOnFail.js +75 -37
  126. package/lib/plugin/stepByStepReport.js +14 -14
  127. package/lib/plugin/stepTimeout.js +4 -3
  128. package/lib/plugin/subtitles.js +6 -5
  129. package/lib/recorder.js +33 -14
  130. package/lib/rerun.js +69 -26
  131. package/lib/result.js +4 -4
  132. package/lib/retryCoordinator.js +2 -2
  133. package/lib/secret.js +18 -17
  134. package/lib/session.js +95 -89
  135. package/lib/step/base.js +7 -7
  136. package/lib/step/comment.js +2 -2
  137. package/lib/step/config.js +1 -1
  138. package/lib/step/func.js +3 -3
  139. package/lib/step/helper.js +3 -3
  140. package/lib/step/meta.js +5 -5
  141. package/lib/step/record.js +11 -11
  142. package/lib/step/retry.js +3 -3
  143. package/lib/step/section.js +3 -3
  144. package/lib/step.js +7 -10
  145. package/lib/steps.js +9 -5
  146. package/lib/store.js +1 -1
  147. package/lib/template/heal.js +1 -1
  148. package/lib/template/prompts/generatePageObject.js +31 -0
  149. package/lib/template/prompts/healStep.js +13 -0
  150. package/lib/template/prompts/writeStep.js +9 -0
  151. package/lib/test-server.js +17 -6
  152. package/lib/timeout.js +1 -7
  153. package/lib/transform.js +8 -8
  154. package/lib/translation.js +32 -18
  155. package/lib/utils/mask_data.js +4 -10
  156. package/lib/utils.js +66 -64
  157. package/lib/workerStorage.js +17 -17
  158. package/lib/workers.js +214 -84
  159. package/package.json +41 -37
  160. package/translations/de-DE.js +2 -2
  161. package/translations/fr-FR.js +2 -2
  162. package/translations/index.js +23 -10
  163. package/translations/it-IT.js +2 -2
  164. package/translations/ja-JP.js +2 -2
  165. package/translations/nl-NL.js +2 -2
  166. package/translations/pl-PL.js +2 -2
  167. package/translations/pt-BR.js +2 -2
  168. package/translations/ru-RU.js +2 -2
  169. package/translations/utils.js +4 -3
  170. package/translations/zh-CN.js +2 -2
  171. package/translations/zh-TW.js +2 -2
  172. package/typings/index.d.ts +5 -3
  173. package/typings/promiseBasedTypes.d.ts +4 -0
  174. package/typings/types.d.ts +4 -0
  175. package/lib/helper/Nightmare.js +0 -1486
  176. package/lib/helper/Protractor.js +0 -1840
  177. package/lib/helper/TestCafe.js +0 -1391
  178. package/lib/helper/clientscripts/nightmare.js +0 -213
  179. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  180. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  181. package/lib/plugin/allure.js +0 -15
  182. package/lib/plugin/autoLogin.js +0 -5
  183. package/lib/plugin/commentStep.js +0 -141
  184. package/lib/plugin/eachElement.js +0 -127
  185. package/lib/plugin/fakerTransform.js +0 -49
  186. package/lib/plugin/retryTo.js +0 -16
  187. package/lib/plugin/selenoid.js +0 -364
  188. package/lib/plugin/standardActingHelpers.js +0 -6
  189. package/lib/plugin/tryTo.js +0 -16
  190. package/lib/plugin/wdio.js +0 -247
  191. 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,32 @@ 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'
39
+ import WebElement from '../element/WebElement.js'
42
40
 
43
41
  let puppeteer
42
+
43
+ /**
44
+ * Wraps error objects that don't have a proper message property
45
+ * This is needed for ESM compatibility with Puppeteer error handling
46
+ */
47
+ function wrapError(e) {
48
+ if (e && typeof e === 'object' && !e.message) {
49
+ const err = new Error(String(e))
50
+ err.stack = e.stack
51
+ return err
52
+ }
53
+ return e
54
+ }
44
55
  let perfTiming
45
56
  const popupStore = new Popup()
46
57
  const consoleLogStore = new Console()
@@ -214,7 +225,7 @@ class Puppeteer extends Helper {
214
225
  constructor(config) {
215
226
  super(config)
216
227
 
217
- puppeteer = requireWithFallback('puppeteer', 'puppeteer-core')
228
+ // puppeteer will be loaded dynamically in _init method
218
229
  // set defaults
219
230
  this.isRemoteBrowser = false
220
231
  this.isRunning = false
@@ -294,13 +305,34 @@ class Puppeteer extends Helper {
294
305
 
295
306
  static _checkRequirements() {
296
307
  try {
297
- requireWithFallback('puppeteer', 'puppeteer-core')
308
+ // In ESM, puppeteer will be checked via dynamic import in _init
309
+ // The import will fail at module load time if puppeteer is missing
310
+ return null
298
311
  } catch (e) {
299
312
  return ['puppeteer']
300
313
  }
301
314
  }
302
315
 
303
- _init() {}
316
+ async _init() {
317
+ // Load puppeteer dynamically with fallback
318
+ if (!puppeteer) {
319
+ try {
320
+ const puppeteerModule = await import('puppeteer')
321
+ puppeteer = puppeteerModule.default || puppeteerModule
322
+ this.debugSection('Puppeteer', `Loaded puppeteer successfully, launch available: ${!!puppeteer.launch}`)
323
+ } catch (e) {
324
+ try {
325
+ const puppeteerModule = await import('puppeteer-core')
326
+ puppeteer = puppeteerModule.default || puppeteerModule
327
+ this.debugSection('Puppeteer', `Loaded puppeteer-core successfully, launch available: ${!!puppeteer.launch}`)
328
+ } catch (e2) {
329
+ throw new Error('Neither puppeteer nor puppeteer-core could be loaded. Please install one of them.')
330
+ }
331
+ }
332
+ } else {
333
+ this.debugSection('Puppeteer', `Puppeteer already loaded, launch available: ${!!puppeteer.launch}`)
334
+ }
335
+ }
304
336
 
305
337
  _beforeSuite() {
306
338
  if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
@@ -404,9 +436,27 @@ class Puppeteer extends Helper {
404
436
  } else {
405
437
  this.activeSessionName = session
406
438
  }
439
+
407
440
  const defaultCtx = this.browser.defaultBrowserContext()
408
- const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
409
- 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
+ }
410
460
 
411
461
  return this._waitForAction()
412
462
  },
@@ -566,6 +616,12 @@ class Puppeteer extends Helper {
566
616
  }
567
617
 
568
618
  async _startBrowser() {
619
+ this.debugSection('Puppeteer', `Starting browser. Puppeteer available: ${!!puppeteer}, launch available: ${!!puppeteer?.launch}`)
620
+
621
+ if (!puppeteer) {
622
+ throw new Error('Puppeteer is not loaded. Make sure _init() was called before _startBrowser()')
623
+ }
624
+
569
625
  if (this.isRemoteBrowser) {
570
626
  try {
571
627
  this.browser = await puppeteer.connect(this.puppeteerOptions)
@@ -616,9 +672,14 @@ class Puppeteer extends Helper {
616
672
  }
617
673
  }
618
674
 
619
- 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
620
681
  const context = await this._getContext()
621
- return context.evaluateHandle(...args)
682
+ return context.evaluateHandle(fn, ...args)
622
683
  }
623
684
 
624
685
  async _withinBegin(locator) {
@@ -648,7 +709,11 @@ class Puppeteer extends Helper {
648
709
 
649
710
  async _withinEnd() {
650
711
  this.withinLocator = null
651
- 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
+ }
652
717
  }
653
718
 
654
719
  _extractDataFromPerformanceTiming(timing, ...dataNames) {
@@ -685,7 +750,21 @@ class Puppeteer extends Helper {
685
750
  this.currentRunningTest.artifacts.trace = fileName
686
751
  }
687
752
 
688
- await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
753
+ try {
754
+ await this.page.goto(url, { waitUntil: this.options.waitForNavigation })
755
+ } catch (err) {
756
+ // Handle terminal navigation errors that shouldn't be retried
757
+ if (
758
+ err.message &&
759
+ (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'))
760
+ ) {
761
+ // Mark this as a terminal error to prevent retries
762
+ const terminalError = new Error(err.message)
763
+ terminalError.isTerminal = true
764
+ throw terminalError
765
+ }
766
+ throw err
767
+ }
689
768
 
690
769
  const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
691
770
 
@@ -969,6 +1048,12 @@ class Puppeteer extends Helper {
969
1048
  return new WebElement(elements[0], this)
970
1049
  }
971
1050
 
1051
+ async grabWebElement(locator) {
1052
+ const els = await this._locate(locator)
1053
+ assertElementExists(els, locator)
1054
+ return els[0]
1055
+ }
1056
+
972
1057
  /**
973
1058
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
974
1059
  *
@@ -1130,7 +1215,7 @@ class Puppeteer extends Helper {
1130
1215
  *
1131
1216
  * {{ react }}
1132
1217
  */
1133
- async click(locator, context = null) {
1218
+ async click(locator = '//body', context = null) {
1134
1219
  return proceedClick.call(this, locator, context)
1135
1220
  }
1136
1221
 
@@ -1290,13 +1375,64 @@ class Puppeteer extends Helper {
1290
1375
  return proceedClick.call(this, locator, context, { button: 'right' })
1291
1376
  }
1292
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
+
1293
1421
  /**
1294
1422
  * {{> checkOption }}
1295
1423
  */
1296
1424
  async checkOption(field, context = null) {
1297
1425
  const elm = await this._locateCheckable(field, context)
1298
- const curentlyChecked = await elm.getProperty('checked').then(checkedProperty => checkedProperty.jsonValue())
1299
- // Only check if NOT currently checked
1426
+ let curentlyChecked = await elm
1427
+ .getProperty('checked')
1428
+ .then(checkedProperty => checkedProperty.jsonValue())
1429
+ .catch(() => null)
1430
+
1431
+ if (!curentlyChecked) {
1432
+ const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
1433
+ curentlyChecked = ariaChecked === 'true'
1434
+ }
1435
+
1300
1436
  if (!curentlyChecked) {
1301
1437
  await elm.click()
1302
1438
  return this._waitForAction()
@@ -1308,8 +1444,16 @@ class Puppeteer extends Helper {
1308
1444
  */
1309
1445
  async uncheckOption(field, context = null) {
1310
1446
  const elm = await this._locateCheckable(field, context)
1311
- const curentlyChecked = await elm.getProperty('checked').then(checkedProperty => checkedProperty.jsonValue())
1312
- // Only uncheck if currently checked
1447
+ let curentlyChecked = await elm
1448
+ .getProperty('checked')
1449
+ .then(checkedProperty => checkedProperty.jsonValue())
1450
+ .catch(() => null)
1451
+
1452
+ if (!curentlyChecked) {
1453
+ const ariaChecked = await elm.evaluate(el => el.getAttribute('aria-checked'))
1454
+ curentlyChecked = ariaChecked === 'true'
1455
+ }
1456
+
1313
1457
  if (curentlyChecked) {
1314
1458
  await elm.click()
1315
1459
  return this._waitForAction()
@@ -1808,7 +1952,7 @@ class Puppeteer extends Helper {
1808
1952
  */
1809
1953
  async grabHTMLFromAll(locator) {
1810
1954
  const els = await this._locate(locator)
1811
- 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)))
1812
1956
  return values
1813
1957
  }
1814
1958
 
@@ -1831,7 +1975,7 @@ class Puppeteer extends Helper {
1831
1975
  */
1832
1976
  async grabCssPropertyFromAll(locator, cssProperty) {
1833
1977
  const els = await this._locate(locator)
1834
- 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))))))
1835
1979
  const cssValues = res.map(props => props[toCamelCase(cssProperty)])
1836
1980
 
1837
1981
  return cssValues
@@ -1956,7 +2100,7 @@ class Puppeteer extends Helper {
1956
2100
  const array = []
1957
2101
  for (let index = 0; index < els.length; index++) {
1958
2102
  const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
1959
- array.push(await a.jsonValue())
2103
+ array.push(a)
1960
2104
  }
1961
2105
  return array
1962
2106
  }
@@ -1998,6 +2142,12 @@ class Puppeteer extends Helper {
1998
2142
 
1999
2143
  this.debug(`Screenshot is saving to ${outputFile}`)
2000
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
+
2001
2151
  await this.page.screenshot({
2002
2152
  path: outputFile,
2003
2153
  fullPage: fullPageOption,
@@ -2011,7 +2161,7 @@ class Puppeteer extends Helper {
2011
2161
 
2012
2162
  this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
2013
2163
 
2014
- if (activeSessionPage) {
2164
+ if (activeSessionPage && !activeSessionPage.isClosed?.()) {
2015
2165
  await activeSessionPage.screenshot({
2016
2166
  path: outputFile,
2017
2167
  fullPage: fullPageOption,
@@ -2730,7 +2880,7 @@ class Puppeteer extends Helper {
2730
2880
  }
2731
2881
  }
2732
2882
 
2733
- module.exports = Puppeteer
2883
+ export default Puppeteer
2734
2884
 
2735
2885
  /**
2736
2886
  * Find elements using Puppeteer's native element discovery methods
@@ -2740,16 +2890,59 @@ module.exports = Puppeteer
2740
2890
  * @returns {Promise<Array>} Array of ElementHandle objects
2741
2891
  */
2742
2892
  async function findElements(matcher, locator) {
2743
- if (locator.react) return findReactElements.call(this, 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
+
2744
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)
2745
2901
 
2746
2902
  // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
2747
2903
  if (!locator.isXPath()) return matcher.$$(locator.simplify())
2904
+
2748
2905
  // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2749
2906
  if (puppeteer.default?.defaultBrowserRevision) {
2750
2907
  return matcher.$$(`xpath/${locator.value}`)
2751
2908
  }
2752
- return matcher.$x(locator.value)
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}`)
2920
+ }
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 []
2753
2946
  }
2754
2947
 
2755
2948
  /**
@@ -2762,18 +2955,22 @@ async function findElements(matcher, locator) {
2762
2955
  async function findElement(matcher, locator) {
2763
2956
  if (locator.react) return findReactElements.call(this, locator)
2764
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
+ }
2765
2964
 
2766
2965
  // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2767
2966
  if (!locator.isXPath()) {
2768
2967
  const elements = await matcher.$$(locator.simplify())
2769
2968
  return elements[0]
2770
2969
  }
2771
- // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2772
- if (puppeteer.default?.defaultBrowserRevision) {
2773
- const elements = await matcher.$$(`xpath/${locator.value}`)
2774
- return elements[0]
2775
- }
2776
- const elements = await matcher.$x(locator.value)
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)
2777
2974
  return elements[0]
2778
2975
  }
2779
2976
 
@@ -2804,12 +3001,12 @@ async function proceedClick(locator, context = null, options = {}) {
2804
3001
  }
2805
3002
 
2806
3003
  async function findClickable(matcher, locator) {
2807
- if (locator.react) return findReactElements.call(this, locator)
2808
- locator = new Locator(locator)
2809
- if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
3004
+ const matchedLocator = new Locator(locator)
3005
+
3006
+ if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
2810
3007
 
2811
3008
  let els
2812
- const literal = xpathLocator.literal(locator.value)
3009
+ const literal = xpathLocator.literal(matchedLocator.value)
2813
3010
 
2814
3011
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
2815
3012
  if (els.length) return els
@@ -2824,7 +3021,15 @@ async function findClickable(matcher, locator) {
2824
3021
  // Do nothing
2825
3022
  }
2826
3023
 
2827
- return findElements.call(this, matcher, locator.value) // by css or xpath
3024
+ // Try ARIA selector for accessible name
3025
+ try {
3026
+ els = await matcher.$$(`::-p-aria(${matchedLocator.value})`)
3027
+ if (els.length) return els
3028
+ } catch (err) {
3029
+ // ARIA selector not supported or failed
3030
+ }
3031
+
3032
+ return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
2828
3033
  }
2829
3034
 
2830
3035
  async function proceedSee(assertType, text, context, strict = false) {
@@ -2868,10 +3073,10 @@ async function findCheckable(locator, context) {
2868
3073
 
2869
3074
  const matchedLocator = new Locator(locator)
2870
3075
  if (!matchedLocator.isFuzzy()) {
2871
- return findElements.call(this, contextEl, matchedLocator.simplify())
3076
+ return findElements.call(this, contextEl, matchedLocator)
2872
3077
  }
2873
3078
 
2874
- const literal = xpathLocator.literal(locator)
3079
+ const literal = xpathLocator.literal(matchedLocator.value)
2875
3080
  let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
2876
3081
  if (els.length) {
2877
3082
  return els
@@ -2880,15 +3085,39 @@ async function findCheckable(locator, context) {
2880
3085
  if (els.length) {
2881
3086
  return els
2882
3087
  }
2883
- return findElements.call(this, contextEl, locator)
3088
+
3089
+ // Try ARIA selector for accessible name
3090
+ try {
3091
+ els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
3092
+ if (els.length) return els
3093
+ } catch (err) {
3094
+ // ARIA selector not supported or failed
3095
+ }
3096
+
3097
+ return findElements.call(this, contextEl, matchedLocator.value)
2884
3098
  }
2885
3099
 
2886
3100
  async function proceedIsChecked(assertType, option) {
2887
3101
  let els = await findCheckable.call(this, option)
2888
3102
  assertElementExists(els, option, 'Checkable')
2889
- els = await Promise.all(els.map(el => el.getProperty('checked')))
2890
- els = await Promise.all(els.map(el => el.jsonValue()))
2891
- const selected = els.reduce((prev, cur) => prev || cur)
3103
+
3104
+ const checkedStates = await Promise.all(
3105
+ els.map(async el => {
3106
+ const checked = await el
3107
+ .getProperty('checked')
3108
+ .then(p => p.jsonValue())
3109
+ .catch(() => null)
3110
+
3111
+ if (checked) {
3112
+ return checked
3113
+ }
3114
+
3115
+ const ariaChecked = await el.evaluate(el => el.getAttribute('aria-checked'))
3116
+ return ariaChecked === 'true'
3117
+ }),
3118
+ )
3119
+
3120
+ const selected = checkedStates.reduce((prev, cur) => prev || cur)
2892
3121
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
2893
3122
  }
2894
3123
 
@@ -2903,7 +3132,7 @@ async function findFields(locator) {
2903
3132
  if (!matchedLocator.isFuzzy()) {
2904
3133
  return this._locate(matchedLocator)
2905
3134
  }
2906
- const literal = xpathLocator.literal(locator)
3135
+ const literal = xpathLocator.literal(matchedLocator.value)
2907
3136
 
2908
3137
  let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
2909
3138
  if (els.length) {
@@ -2918,7 +3147,17 @@ async function findFields(locator) {
2918
3147
  if (els.length) {
2919
3148
  return els
2920
3149
  }
2921
- return this._locate({ css: locator })
3150
+
3151
+ // Try ARIA selector for accessible name
3152
+ try {
3153
+ const page = await this.context
3154
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3155
+ if (els.length) return els
3156
+ } catch (err) {
3157
+ // ARIA selector not supported or failed
3158
+ }
3159
+
3160
+ return this._locate({ css: matchedLocator.value })
2922
3161
  }
2923
3162
 
2924
3163
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -2996,19 +3235,30 @@ async function proceedSeeInField(assertType, field, value) {
2996
3235
  }
2997
3236
  return proceedMultiple(els[0])
2998
3237
  }
2999
- const fieldVal = await el.getProperty('value').then(el => el.jsonValue())
3238
+
3239
+ let fieldVal = await el.getProperty('value').then(el => el.jsonValue())
3240
+
3241
+ if (fieldVal === undefined || fieldVal === null) {
3242
+ fieldVal = await el.evaluate(el => el.textContent || el.innerText)
3243
+ }
3244
+
3000
3245
  return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
3001
3246
  }
3002
3247
 
3003
3248
  async function filterFieldsByValue(elements, value, onlySelected) {
3004
3249
  const matches = []
3005
3250
  for (const element of elements) {
3006
- const val = await element.getProperty('value').then(el => el.jsonValue())
3251
+ let val = await element.getProperty('value').then(el => el.jsonValue())
3252
+
3253
+ if (val === undefined || val === null) {
3254
+ val = await element.evaluate(el => el.textContent || el.innerText)
3255
+ }
3256
+
3007
3257
  let isSelected = true
3008
3258
  if (onlySelected) {
3009
3259
  isSelected = await elementSelected(element)
3010
3260
  }
3011
- if ((value == null || val.indexOf(value) > -1) && isSelected) {
3261
+ if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
3012
3262
  matches.push(element)
3013
3263
  }
3014
3264
  }
@@ -3170,7 +3420,14 @@ function _waitForElement(locator, options) {
3170
3420
  }
3171
3421
  }
3172
3422
 
3173
- async function findReactElements(locator, props = {}, state = {}) {
3423
+ async function findReactElements(locator) {
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)}`)
3427
+
3428
+ // Use createRequire to access require.resolve in ESM
3429
+ const { createRequire } = await import('module')
3430
+ const require = createRequire(import.meta.url)
3174
3431
  const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
3175
3432
  await this.page.evaluate(resqScript.toString())
3176
3433
 
@@ -3213,9 +3470,9 @@ async function findReactElements(locator, props = {}, state = {}) {
3213
3470
  return [...nodes]
3214
3471
  },
3215
3472
  {
3216
- selector: locator.react,
3217
- props: locator.props || {},
3218
- state: locator.state || {},
3473
+ selector: resolved.react,
3474
+ props: resolved.props || {},
3475
+ state: resolved.state || {},
3219
3476
  },
3220
3477
  )
3221
3478
 
@@ -3231,3 +3488,53 @@ async function findReactElements(locator, props = {}, state = {}) {
3231
3488
  await arrayHandle.dispose()
3232
3489
  return result
3233
3490
  }
3491
+
3492
+ async function findByRole(matcher, locator) {
3493
+ const resolved = toLocatorConfig(locator, 'role')
3494
+ const roleSelector = buildRoleSelector(resolved)
3495
+
3496
+ if (!resolved.text && !resolved.name) {
3497
+ return matcher.$$(roleSelector)
3498
+ }
3499
+
3500
+ const allElements = await matcher.$$(roleSelector)
3501
+ const filtered = []
3502
+ const accessibleName = resolved.text ?? resolved.name
3503
+ const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
3504
+
3505
+ for (const el of allElements) {
3506
+ const texts = await el.evaluate(e => {
3507
+ const ariaLabel = e.hasAttribute('aria-label') ? e.getAttribute('aria-label') : ''
3508
+ const labelText = e.id ? document.querySelector(`label[for="${e.id}"]`)?.textContent.trim() || '' : ''
3509
+ const placeholder = e.getAttribute('placeholder') || ''
3510
+ const innerText = e.innerText ? e.innerText.trim() : ''
3511
+ return [ariaLabel || labelText, placeholder, innerText]
3512
+ })
3513
+
3514
+ if (texts.some(text => matcherFn(text))) filtered.push(el)
3515
+ }
3516
+
3517
+ return filtered
3518
+ }
3519
+
3520
+ function toLocatorConfig(locator, key) {
3521
+ const matchedLocator = new Locator(locator, key)
3522
+ if (matchedLocator.locator) return matchedLocator.locator
3523
+ return { [key]: matchedLocator.value }
3524
+ }
3525
+
3526
+ function buildRoleSelector(resolved) {
3527
+ return `::-p-aria([role="${resolved.role}"])`
3528
+ }
3529
+
3530
+ function createRoleTextMatcher(expected, exactMatch) {
3531
+ if (expected instanceof RegExp) {
3532
+ return value => expected.test(value || '')
3533
+ }
3534
+ const target = String(expected)
3535
+ if (exactMatch) {
3536
+ return value => value === target
3537
+ }
3538
+ return value => typeof value === 'string' && value.includes(target)
3539
+ }
3540
+