codeceptjs 4.0.0-beta.1 → 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 (207) hide show
  1. package/README.md +133 -120
  2. package/bin/codecept.js +107 -96
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/docs/webapi/click.mustache +5 -1
  6. package/lib/actor.js +71 -103
  7. package/lib/ai.js +159 -188
  8. package/lib/assert/empty.js +22 -24
  9. package/lib/assert/equal.js +30 -37
  10. package/lib/assert/error.js +14 -14
  11. package/lib/assert/include.js +43 -48
  12. package/lib/assert/throws.js +11 -11
  13. package/lib/assert/truth.js +22 -22
  14. package/lib/assert.js +20 -18
  15. package/lib/codecept.js +238 -162
  16. package/lib/colorUtils.js +50 -52
  17. package/lib/command/check.js +206 -0
  18. package/lib/command/configMigrate.js +56 -51
  19. package/lib/command/definitions.js +96 -109
  20. package/lib/command/dryRun.js +77 -79
  21. package/lib/command/generate.js +234 -194
  22. package/lib/command/gherkin/init.js +42 -33
  23. package/lib/command/gherkin/snippets.js +76 -74
  24. package/lib/command/gherkin/steps.js +20 -17
  25. package/lib/command/info.js +74 -38
  26. package/lib/command/init.js +300 -290
  27. package/lib/command/interactive.js +41 -32
  28. package/lib/command/list.js +28 -27
  29. package/lib/command/run-multiple/chunk.js +51 -48
  30. package/lib/command/run-multiple/collection.js +5 -5
  31. package/lib/command/run-multiple/run.js +5 -1
  32. package/lib/command/run-multiple.js +97 -97
  33. package/lib/command/run-rerun.js +19 -25
  34. package/lib/command/run-workers.js +68 -92
  35. package/lib/command/run.js +39 -27
  36. package/lib/command/utils.js +80 -64
  37. package/lib/command/workers/runTests.js +388 -226
  38. package/lib/config.js +124 -50
  39. package/lib/container.js +751 -260
  40. package/lib/data/context.js +60 -61
  41. package/lib/data/dataScenarioConfig.js +47 -47
  42. package/lib/data/dataTableArgument.js +32 -32
  43. package/lib/data/table.js +22 -22
  44. package/lib/effects.js +307 -0
  45. package/lib/element/WebElement.js +327 -0
  46. package/lib/els.js +160 -0
  47. package/lib/event.js +173 -163
  48. package/lib/globals.js +141 -0
  49. package/lib/heal.js +89 -85
  50. package/lib/helper/AI.js +131 -41
  51. package/lib/helper/ApiDataFactory.js +107 -75
  52. package/lib/helper/Appium.js +542 -404
  53. package/lib/helper/FileSystem.js +100 -79
  54. package/lib/helper/GraphQL.js +44 -43
  55. package/lib/helper/GraphQLDataFactory.js +52 -52
  56. package/lib/helper/JSONResponse.js +126 -88
  57. package/lib/helper/Mochawesome.js +54 -29
  58. package/lib/helper/Playwright.js +2547 -1316
  59. package/lib/helper/Puppeteer.js +1578 -1181
  60. package/lib/helper/REST.js +209 -68
  61. package/lib/helper/WebDriver.js +1482 -1342
  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 +17 -8
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
  71. package/lib/helper/extras/Popup.js +22 -22
  72. package/lib/helper/extras/React.js +27 -28
  73. package/lib/helper/network/actions.js +36 -42
  74. package/lib/helper/network/utils.js +78 -84
  75. package/lib/helper/scripts/blurElement.js +5 -5
  76. package/lib/helper/scripts/focusElement.js +5 -5
  77. package/lib/helper/scripts/highlightElement.js +8 -8
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -3
  80. package/lib/history.js +23 -19
  81. package/lib/hooks.js +8 -8
  82. package/lib/html.js +94 -104
  83. package/lib/index.js +38 -27
  84. package/lib/listener/config.js +30 -23
  85. package/lib/listener/emptyRun.js +54 -0
  86. package/lib/listener/enhancedGlobalRetry.js +110 -0
  87. package/lib/listener/exit.js +16 -18
  88. package/lib/listener/globalRetry.js +70 -0
  89. package/lib/listener/globalTimeout.js +181 -0
  90. package/lib/listener/helpers.js +76 -51
  91. package/lib/listener/mocha.js +10 -11
  92. package/lib/listener/result.js +11 -0
  93. package/lib/listener/retryEnhancer.js +85 -0
  94. package/lib/listener/steps.js +71 -59
  95. package/lib/listener/store.js +20 -0
  96. package/lib/locator.js +214 -197
  97. package/lib/mocha/asyncWrapper.js +274 -0
  98. package/lib/mocha/bdd.js +167 -0
  99. package/lib/mocha/cli.js +341 -0
  100. package/lib/mocha/factory.js +163 -0
  101. package/lib/mocha/featureConfig.js +89 -0
  102. package/lib/mocha/gherkin.js +231 -0
  103. package/lib/mocha/hooks.js +121 -0
  104. package/lib/mocha/index.js +21 -0
  105. package/lib/mocha/inject.js +46 -0
  106. package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
  107. package/lib/mocha/suite.js +89 -0
  108. package/lib/mocha/test.js +184 -0
  109. package/lib/mocha/types.d.ts +42 -0
  110. package/lib/mocha/ui.js +242 -0
  111. package/lib/output.js +141 -71
  112. package/lib/parser.js +47 -44
  113. package/lib/pause.js +173 -145
  114. package/lib/plugin/analyze.js +403 -0
  115. package/lib/plugin/{autoLogin.js → auth.js} +178 -79
  116. package/lib/plugin/autoDelay.js +36 -40
  117. package/lib/plugin/coverage.js +131 -78
  118. package/lib/plugin/customLocator.js +22 -21
  119. package/lib/plugin/customReporter.js +53 -0
  120. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  121. package/lib/plugin/heal.js +101 -110
  122. package/lib/plugin/htmlReporter.js +3648 -0
  123. package/lib/plugin/pageInfo.js +140 -0
  124. package/lib/plugin/pauseOnFail.js +12 -11
  125. package/lib/plugin/retryFailedStep.js +82 -47
  126. package/lib/plugin/screenshotOnFail.js +111 -92
  127. package/lib/plugin/stepByStepReport.js +159 -101
  128. package/lib/plugin/stepTimeout.js +20 -25
  129. package/lib/plugin/subtitles.js +38 -38
  130. package/lib/recorder.js +193 -130
  131. package/lib/rerun.js +94 -49
  132. package/lib/result.js +238 -0
  133. package/lib/retryCoordinator.js +207 -0
  134. package/lib/secret.js +20 -18
  135. package/lib/session.js +95 -89
  136. package/lib/step/base.js +239 -0
  137. package/lib/step/comment.js +10 -0
  138. package/lib/step/config.js +50 -0
  139. package/lib/step/func.js +46 -0
  140. package/lib/step/helper.js +50 -0
  141. package/lib/step/meta.js +99 -0
  142. package/lib/step/record.js +74 -0
  143. package/lib/step/retry.js +11 -0
  144. package/lib/step/section.js +55 -0
  145. package/lib/step.js +18 -329
  146. package/lib/steps.js +54 -0
  147. package/lib/store.js +38 -7
  148. package/lib/template/heal.js +3 -12
  149. package/lib/template/prompts/generatePageObject.js +31 -0
  150. package/lib/template/prompts/healStep.js +13 -0
  151. package/lib/template/prompts/writeStep.js +9 -0
  152. package/lib/test-server.js +334 -0
  153. package/lib/timeout.js +60 -0
  154. package/lib/transform.js +8 -8
  155. package/lib/translation.js +34 -21
  156. package/lib/utils/mask_data.js +47 -0
  157. package/lib/utils.js +411 -228
  158. package/lib/workerStorage.js +37 -34
  159. package/lib/workers.js +532 -296
  160. package/package.json +115 -95
  161. package/translations/de-DE.js +5 -3
  162. package/translations/fr-FR.js +5 -4
  163. package/translations/index.js +22 -12
  164. package/translations/it-IT.js +4 -3
  165. package/translations/ja-JP.js +4 -3
  166. package/translations/nl-NL.js +76 -0
  167. package/translations/pl-PL.js +4 -3
  168. package/translations/pt-BR.js +4 -3
  169. package/translations/ru-RU.js +4 -3
  170. package/translations/utils.js +10 -0
  171. package/translations/zh-CN.js +4 -3
  172. package/translations/zh-TW.js +4 -3
  173. package/typings/index.d.ts +546 -185
  174. package/typings/promiseBasedTypes.d.ts +150 -879
  175. package/typings/types.d.ts +547 -996
  176. package/lib/cli.js +0 -249
  177. package/lib/dirname.js +0 -5
  178. package/lib/helper/Expect.js +0 -425
  179. package/lib/helper/ExpectHelper.js +0 -399
  180. package/lib/helper/MockServer.js +0 -223
  181. package/lib/helper/Nightmare.js +0 -1411
  182. package/lib/helper/Protractor.js +0 -1835
  183. package/lib/helper/SoftExpectHelper.js +0 -381
  184. package/lib/helper/TestCafe.js +0 -1410
  185. package/lib/helper/clientscripts/nightmare.js +0 -213
  186. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  187. package/lib/helper/testcafe/testcafe-utils.js +0 -63
  188. package/lib/interfaces/bdd.js +0 -98
  189. package/lib/interfaces/featureConfig.js +0 -69
  190. package/lib/interfaces/gherkin.js +0 -195
  191. package/lib/listener/artifacts.js +0 -19
  192. package/lib/listener/retry.js +0 -68
  193. package/lib/listener/timeout.js +0 -109
  194. package/lib/mochaFactory.js +0 -110
  195. package/lib/plugin/allure.js +0 -15
  196. package/lib/plugin/commentStep.js +0 -136
  197. package/lib/plugin/debugErrors.js +0 -67
  198. package/lib/plugin/eachElement.js +0 -127
  199. package/lib/plugin/fakerTransform.js +0 -49
  200. package/lib/plugin/retryTo.js +0 -121
  201. package/lib/plugin/selenoid.js +0 -371
  202. package/lib/plugin/standardActingHelpers.js +0 -9
  203. package/lib/plugin/tryTo.js +0 -105
  204. package/lib/plugin/wdio.js +0 -246
  205. package/lib/scenario.js +0 -222
  206. package/lib/ui.js +0 -238
  207. package/lib/within.js +0 -70
@@ -1,19 +1,18 @@
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';
15
- import isElementClickable from './scripts/isElementClickable';
16
-
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'
17
16
  import {
18
17
  xpathLocator,
19
18
  ucfirst,
@@ -27,33 +26,35 @@ import {
27
26
  isModifierKey,
28
27
  requireWithFallback,
29
28
  normalizeSpacesInString,
30
- } from '../utils.js';
31
-
32
- import { isColorProperty, convertColorToRGBA } from '../colorUtils';
33
- import ElementNotFound from './errors/ElementNotFound.js';
34
- import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused';
35
- import Popup from './extras/Popup';
36
- import Console from './extras/Console';
37
- import findReact from './extras/React';
38
- import { highlightElement } from './scripts/highlightElement';
39
- import { blurElement } from './scripts/blurElement';
40
- import { focusElement } from './scripts/focusElement';
41
-
42
- import {
43
- dontSeeElementError,
44
- seeElementError,
45
- dontSeeElementInDOMError,
46
- seeElementInDOMError,
47
- } from './errors/ElementAssertion';
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'
40
+
41
+ let puppeteer
48
42
 
49
- import {
50
- dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
51
- } from './network/actions';
52
-
53
- let puppeteer;
54
- let perfTiming;
55
- const popupStore = new Popup();
56
- const consoleLogStore = new Console();
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
+ }
55
+ let perfTiming
56
+ const popupStore = new Popup()
57
+ const consoleLogStore = new Console()
57
58
 
58
59
  /**
59
60
  * ## Configuration
@@ -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
- const config = {};
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
  *
@@ -222,29 +223,29 @@ const config = {};
222
223
  */
223
224
  class Puppeteer extends Helper {
224
225
  constructor(config) {
225
- super(config);
226
+ super(config)
226
227
 
227
- puppeteer = requireWithFallback('puppeteer', 'puppeteer-core');
228
+ // puppeteer will be loaded dynamically in _init method
228
229
  // set defaults
229
- this.isRemoteBrowser = false;
230
- this.isRunning = false;
231
- this.isAuthenticated = false;
232
- this.sessionPages = {};
233
- this.activeSessionName = '';
230
+ this.isRemoteBrowser = false
231
+ this.isRunning = false
232
+ this.isAuthenticated = false
233
+ this.sessionPages = {}
234
+ this.activeSessionName = ''
234
235
 
235
236
  // for network stuff
236
- this.requests = [];
237
- this.recording = false;
238
- this.recordedAtLeastOnce = false;
237
+ this.requests = []
238
+ this.recording = false
239
+ this.recordedAtLeastOnce = false
239
240
 
240
241
  // for websocket messages
241
- this.webSocketMessages = [];
242
- this.recordingWebSocketMessages = false;
243
- this.recordedWebSocketMessagesAtLeastOnce = false;
244
- this.cdpSession = null;
242
+ this.webSocketMessages = []
243
+ this.recordingWebSocketMessages = false
244
+ this.recordedWebSocketMessagesAtLeastOnce = false
245
+ this.cdpSession = null
245
246
 
246
247
  // override defaults with config
247
- this._setConfig(config);
248
+ this._setConfig(config)
248
249
  }
249
250
 
250
251
  _validateConfig(config) {
@@ -265,156 +266,201 @@ class Puppeteer extends Helper {
265
266
  show: false,
266
267
  defaultPopupAction: 'accept',
267
268
  highlightElement: false,
268
- };
269
+ }
269
270
 
270
- return Object.assign(defaults, config);
271
+ return Object.assign(defaults, config)
271
272
  }
272
273
 
273
274
  _getOptions(config) {
274
- return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome;
275
+ return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome
275
276
  }
276
277
 
277
278
  _setConfig(config) {
278
- this.options = this._validateConfig(config);
279
+ this.options = this._validateConfig(config)
279
280
  this.puppeteerOptions = {
280
281
  headless: !this.options.show,
281
282
  ...this._getOptions(config),
282
- };
283
- if (this.puppeteerOptions.headless) this.puppeteerOptions.headless = 'new';
284
- this.isRemoteBrowser = !!this.puppeteerOptions.browserWSEndpoint;
285
- popupStore.defaultAction = this.options.defaultPopupAction;
283
+ }
284
+ if (this.puppeteerOptions.headless) this.puppeteerOptions.headless = 'new'
285
+ this.isRemoteBrowser = !!this.puppeteerOptions.browserWSEndpoint
286
+ popupStore.defaultAction = this.options.defaultPopupAction
286
287
  }
287
288
 
288
289
  static _config() {
289
290
  return [
290
291
  { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
291
292
  {
292
- name: 'show', message: 'Show browser window', default: true, type: 'confirm',
293
+ name: 'show',
294
+ message: 'Show browser window',
295
+ default: true,
296
+ type: 'confirm',
293
297
  },
294
298
  {
295
- name: 'windowSize', message: 'Browser viewport size', default: '1200x900',
299
+ name: 'windowSize',
300
+ message: 'Browser viewport size',
301
+ default: '1200x900',
296
302
  },
297
- ];
303
+ ]
298
304
  }
299
305
 
300
306
  static _checkRequirements() {
301
307
  try {
302
- 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
303
311
  } catch (e) {
304
- return ['puppeteer'];
312
+ return ['puppeteer']
305
313
  }
306
314
  }
307
315
 
308
- _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
+ }
309
335
  }
310
336
 
311
337
  _beforeSuite() {
312
338
  if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
313
- this.debugSection('Session', 'Starting singleton browser session');
314
- return this._startBrowser();
339
+ this.debugSection('Session', 'Starting singleton browser session')
340
+ return this._startBrowser()
315
341
  }
316
342
  }
317
343
 
318
344
  async _before(test) {
319
- this.sessionPages = {};
320
- this.currentRunningTest = test;
345
+ this.sessionPages = {}
346
+ this.currentRunningTest = test
321
347
  recorder.retry({
322
- retries: process.env.FAILED_STEP_RETRIES || 3,
348
+ retries: test?.opts?.conditionalRetries || 3,
323
349
  when: err => {
324
- if (!err || typeof (err.message) !== 'string') {
325
- return false;
350
+ if (!err || typeof err.message !== 'string') {
351
+ return false
326
352
  }
327
353
  // ignore context errors
328
- return err.message.includes('context');
354
+ return err.message.includes('context')
329
355
  },
330
- });
331
- if (this.options.restart && !this.options.manualStart) return this._startBrowser();
332
- if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
333
- return this.browser;
356
+ })
357
+ if (this.options.restart && !this.options.manualStart) return this._startBrowser()
358
+ if (!this.isRunning && !this.options.manualStart) return this._startBrowser()
359
+ return this.browser
334
360
  }
335
361
 
336
362
  async _after() {
337
- if (!this.isRunning) return;
363
+ if (!this.isRunning) return
364
+
365
+ // Clear popup state to prevent leakage between tests
366
+ popupStore.clear()
338
367
 
339
368
  // close other sessions
340
- const contexts = this.browser.browserContexts();
341
- const defaultCtx = contexts.shift();
369
+ const contexts = this.browser.browserContexts()
370
+ const defaultCtx = contexts.shift()
342
371
 
343
- await Promise.all(contexts.map(c => c.close()));
372
+ await Promise.all(contexts.map(c => c.close()))
344
373
 
345
374
  if (this.options.restart) {
346
- this.isRunning = false;
347
- return this._stopBrowser();
375
+ this.isRunning = false
376
+ return this._stopBrowser()
348
377
  }
349
378
 
350
379
  // ensure this.page is from default context
351
380
  if (this.page) {
352
- const existingPages = defaultCtx.targets().filter(t => t.type() === 'page');
353
- await this._setPage(await existingPages[0].page());
381
+ const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
382
+ await this._setPage(await existingPages[0].page())
354
383
  }
355
384
 
356
- if (this.options.keepBrowserState) return;
385
+ if (this.options.keepBrowserState) return
357
386
 
358
387
  if (!this.options.keepCookies) {
359
- this.debugSection('Session', 'cleaning cookies and localStorage');
360
- await this.clearCookie();
388
+ this.debugSection('Session', 'cleaning cookies and localStorage')
389
+ await this.clearCookie()
361
390
  }
362
- const currentUrl = await this.grabCurrentUrl();
391
+ const currentUrl = await this.grabCurrentUrl()
363
392
 
364
393
  if (currentUrl.startsWith('http')) {
365
- await this.executeScript('localStorage.clear();').catch((err) => {
366
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
367
- });
368
- await this.executeScript('sessionStorage.clear();').catch((err) => {
369
- if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
370
- });
394
+ await this.executeScript('localStorage.clear();').catch(err => {
395
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
396
+ })
397
+ await this.executeScript('sessionStorage.clear();').catch(err => {
398
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
399
+ })
371
400
  }
372
- await this.closeOtherTabs();
373
- return this.browser;
401
+ await this.closeOtherTabs()
402
+ return this.browser
374
403
  }
375
404
 
376
- _afterSuite() {
377
- }
405
+ _afterSuite() {}
378
406
 
379
407
  _finishTest() {
380
- if (!this.options.restart && this.isRunning) return this._stopBrowser();
408
+ if (!this.options.restart && this.isRunning) return this._stopBrowser()
381
409
  }
382
410
 
383
411
  _session() {
384
412
  return {
385
413
  start: async (name = '') => {
386
- this.debugSection('Incognito Tab', 'opened');
387
- this.activeSessionName = name;
414
+ this.debugSection('Incognito Tab', 'opened')
415
+ this.activeSessionName = name
388
416
 
389
- const bc = await this.browser.createBrowserContext();
390
- await bc.newPage();
417
+ const bc = await this.browser.createBrowserContext()
418
+ await bc.newPage()
391
419
 
392
420
  // Create a new page inside context.
393
- return bc;
421
+ return bc
394
422
  },
395
423
  stop: async () => {
396
424
  // is closed by _after
397
425
  },
398
- loadVars: async (context) => {
399
- const existingPages = context.targets().filter(t => t.type() === 'page');
400
- this.sessionPages[this.activeSessionName] = await existingPages[0].page();
401
- return this._setPage(this.sessionPages[this.activeSessionName]);
426
+ loadVars: async context => {
427
+ const existingPages = context.targets().filter(t => t.type() === 'page')
428
+ this.sessionPages[this.activeSessionName] = await existingPages[0].page()
429
+ return this._setPage(this.sessionPages[this.activeSessionName])
402
430
  },
403
- restoreVars: async (session) => {
404
- this.withinLocator = null;
431
+ restoreVars: async session => {
432
+ this.withinLocator = null
405
433
 
406
434
  if (!session) {
407
- this.activeSessionName = '';
435
+ this.activeSessionName = ''
408
436
  } else {
409
- this.activeSessionName = session;
437
+ this.activeSessionName = session
438
+ }
439
+
440
+ const defaultCtx = this.browser.defaultBrowserContext()
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
410
459
  }
411
- const defaultCtx = this.browser.defaultBrowserContext();
412
- const existingPages = defaultCtx.targets().filter(t => t.type() === 'page');
413
- await this._setPage(await existingPages[0].page());
414
460
 
415
- return this._waitForAction();
461
+ return this._waitForAction()
416
462
  },
417
- };
463
+ }
418
464
  }
419
465
 
420
466
  /**
@@ -435,7 +481,7 @@ class Puppeteer extends Helper {
435
481
  * @param {function} fn async function that is executed with Puppeteer as argument
436
482
  */
437
483
  usePuppeteerTo(description, fn) {
438
- return this._useTo(...arguments);
484
+ return this._useTo(...arguments)
439
485
  }
440
486
 
441
487
  /**
@@ -449,7 +495,7 @@ class Puppeteer extends Helper {
449
495
  * ```
450
496
  */
451
497
  amAcceptingPopups() {
452
- popupStore.actionType = 'accept';
498
+ popupStore.actionType = 'accept'
453
499
  }
454
500
 
455
501
  /**
@@ -458,7 +504,7 @@ class Puppeteer extends Helper {
458
504
  * libraries](http://jster.net/category/windows-modals-popups).
459
505
  */
460
506
  acceptPopup() {
461
- popupStore.assertPopupActionType('accept');
507
+ popupStore.assertPopupActionType('accept')
462
508
  }
463
509
 
464
510
  /**
@@ -472,23 +518,23 @@ class Puppeteer extends Helper {
472
518
  * ```
473
519
  */
474
520
  amCancellingPopups() {
475
- popupStore.actionType = 'cancel';
521
+ popupStore.actionType = 'cancel'
476
522
  }
477
523
 
478
524
  /**
479
525
  * Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
480
526
  */
481
527
  cancelPopup() {
482
- popupStore.assertPopupActionType('cancel');
528
+ popupStore.assertPopupActionType('cancel')
483
529
  }
484
530
 
485
531
  /**
486
532
  * {{> seeInPopup }}
487
533
  */
488
534
  async seeInPopup(text) {
489
- popupStore.assertPopupVisible();
490
- const popupText = await popupStore.popup.message();
491
- stringIncludes('text in popup').assert(text, popupText);
535
+ popupStore.assertPopupVisible()
536
+ const popupText = await popupStore.popup.message()
537
+ stringIncludes('text in popup').assert(text, popupText)
492
538
  }
493
539
 
494
540
  /**
@@ -496,25 +542,25 @@ class Puppeteer extends Helper {
496
542
  * @param {object} page page to set
497
543
  */
498
544
  async _setPage(page) {
499
- page = await page;
500
- this._addPopupListener(page);
501
- this._addErrorListener(page);
502
- this.page = page;
503
- if (!page) return;
504
- page.setDefaultNavigationTimeout(this.options.getPageTimeout);
505
- this.context = await this.page.$('body');
545
+ page = await page
546
+ this._addPopupListener(page)
547
+ this._addErrorListener(page)
548
+ this.page = page
549
+ if (!page) return
550
+ page.setDefaultNavigationTimeout(this.options.getPageTimeout)
551
+ this.context = await this.page.$('body')
506
552
  if (this.options.browser === 'chrome') {
507
- await page.bringToFront();
553
+ await page.bringToFront()
508
554
  }
509
555
  }
510
556
 
511
557
  async _addErrorListener(page) {
512
558
  if (!page) {
513
- return;
559
+ return
514
560
  }
515
- page.on('error', async (error) => {
516
- console.error('Puppeteer page error', error);
517
- });
561
+ page.on('error', async error => {
562
+ console.error('Puppeteer page error', error)
563
+ })
518
564
  }
519
565
 
520
566
  /**
@@ -526,32 +572,32 @@ class Puppeteer extends Helper {
526
572
  */
527
573
  _addPopupListener(page) {
528
574
  if (!page) {
529
- return;
575
+ return
530
576
  }
531
- page.on('dialog', async (dialog) => {
532
- popupStore.popup = dialog;
533
- const action = popupStore.actionType || this.options.defaultPopupAction;
534
- await this._waitForAction();
577
+ page.on('dialog', async dialog => {
578
+ popupStore.popup = dialog
579
+ const action = popupStore.actionType || this.options.defaultPopupAction
580
+ await this._waitForAction()
535
581
 
536
582
  switch (action) {
537
583
  case 'accept':
538
- return dialog.accept();
584
+ return dialog.accept()
539
585
 
540
586
  case 'cancel':
541
- return dialog.dismiss();
587
+ return dialog.dismiss()
542
588
 
543
589
  default: {
544
- throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted');
590
+ throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted')
545
591
  }
546
592
  }
547
- });
593
+ })
548
594
  }
549
595
 
550
596
  /**
551
597
  * Gets page URL including hash.
552
598
  */
553
599
  async _getPageUrl() {
554
- return this.executeScript(() => window.location.href);
600
+ return this.executeScript(() => window.location.href)
555
601
  }
556
602
 
557
603
  /**
@@ -564,138 +610,167 @@ class Puppeteer extends Helper {
564
610
  */
565
611
  async grabPopupText() {
566
612
  if (popupStore.popup) {
567
- return popupStore.popup.message();
613
+ return popupStore.popup.message()
568
614
  }
569
- return null;
615
+ return null
570
616
  }
571
617
 
572
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
+
573
625
  if (this.isRemoteBrowser) {
574
626
  try {
575
- this.browser = await puppeteer.connect(this.puppeteerOptions);
627
+ this.browser = await puppeteer.connect(this.puppeteerOptions)
576
628
  } catch (err) {
577
629
  if (err.toString().indexOf('ECONNREFUSED')) {
578
- throw new RemoteBrowserConnectionRefused(err);
630
+ throw new RemoteBrowserConnectionRefused(err)
579
631
  }
580
- throw err;
632
+ throw err
581
633
  }
582
634
  } else {
583
- this.browser = await puppeteer.launch(this.puppeteerOptions);
635
+ this.browser = await puppeteer.launch(this.puppeteerOptions)
584
636
  }
585
637
 
586
- this.browser.on('targetcreated', target => target.page().then(page => targetCreatedHandler.call(this, page)).catch((e) => {
587
- console.error('Puppeteer page error', e);
588
- }));
589
- this.browser.on('targetchanged', (target) => {
590
- this.debugSection('Url', target.url());
591
- });
638
+ this.browser.on('targetcreated', target =>
639
+ target
640
+ .page()
641
+ .then(page => targetCreatedHandler.call(this, page))
642
+ .catch(e => {
643
+ console.error('Puppeteer page error', e)
644
+ }),
645
+ )
646
+ this.browser.on('targetchanged', target => {
647
+ this.debugSection('Url', target.url())
648
+ })
592
649
 
593
- const existingPages = await this.browser.pages();
594
- const mainPage = existingPages[0] || (await this.browser.newPage());
650
+ const existingPages = await this.browser.pages()
651
+ const mainPage = existingPages[0] || (await this.browser.newPage())
595
652
 
596
653
  if (existingPages.length) {
597
654
  // Run the handler as it will not be triggered if the page already exists
598
- targetCreatedHandler.call(this, mainPage);
655
+ targetCreatedHandler.call(this, mainPage)
599
656
  }
600
- await this._setPage(mainPage);
601
- await this.closeOtherTabs();
657
+ await this._setPage(mainPage)
658
+ await this.closeOtherTabs()
602
659
 
603
- this.isRunning = true;
660
+ this.isRunning = true
604
661
  }
605
662
 
606
663
  async _stopBrowser() {
607
- this.withinLocator = null;
608
- this._setPage(null);
609
- this.context = null;
610
- popupStore.clear();
611
- this.isAuthenticated = false;
612
- await this.browser.close();
664
+ this.withinLocator = null
665
+ this._setPage(null)
666
+ this.context = null
667
+ popupStore.clear()
668
+ this.isAuthenticated = false
669
+ await this.browser.close()
613
670
  if (this.isRemoteBrowser) {
614
- await this.browser.disconnect();
671
+ await this.browser.disconnect()
615
672
  }
616
673
  }
617
674
 
618
- async _evaluateHandeInContext(...args) {
619
- const context = await this._getContext();
620
- return context.evaluateHandle(...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
681
+ const context = await this._getContext()
682
+ return context.evaluateHandle(fn, ...args)
621
683
  }
622
684
 
623
685
  async _withinBegin(locator) {
624
686
  if (this.withinLocator) {
625
- throw new Error('Can\'t start within block inside another within block');
687
+ throw new Error("Can't start within block inside another within block")
626
688
  }
627
689
 
628
- const frame = isFrameLocator(locator);
690
+ const frame = isFrameLocator(locator)
629
691
 
630
692
  if (frame) {
631
693
  if (Array.isArray(frame)) {
632
- return this.switchTo(null)
633
- .then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()));
694
+ return this.switchTo(null).then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()))
634
695
  }
635
- await this.switchTo(frame);
636
- this.withinLocator = new Locator(frame);
637
- return;
696
+ await this.switchTo(frame)
697
+ this.withinLocator = new Locator(frame)
698
+ return
638
699
  }
639
700
 
640
- const els = await this._locate(locator);
641
- assertElementExists(els, locator);
642
- 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
643
706
 
644
- this.withinLocator = new Locator(locator);
707
+ this.withinLocator = new Locator(locator)
645
708
  }
646
709
 
647
710
  async _withinEnd() {
648
- this.withinLocator = null;
649
- this.context = await this.page.mainFrame().$('body');
711
+ this.withinLocator = null
712
+ if (this.page && !this.page.isClosed?.()) {
713
+ this.context = await this.page.mainFrame().$('body')
714
+ } else {
715
+ this.context = null
716
+ }
650
717
  }
651
718
 
652
719
  _extractDataFromPerformanceTiming(timing, ...dataNames) {
653
- const navigationStart = timing.navigationStart;
720
+ const navigationStart = timing.navigationStart
654
721
 
655
- const extractedData = {};
656
- dataNames.forEach((name) => {
657
- extractedData[name] = timing[name] - navigationStart;
658
- });
722
+ const extractedData = {}
723
+ dataNames.forEach(name => {
724
+ extractedData[name] = timing[name] - navigationStart
725
+ })
659
726
 
660
- return extractedData;
727
+ return extractedData
661
728
  }
662
729
 
663
730
  /**
664
731
  * {{> amOnPage }}
665
732
  */
666
733
  async amOnPage(url) {
667
- if (!(/^\w+\:\/\//.test(url))) {
668
- url = this.options.url + url;
734
+ if (!/^\w+\:\/\//.test(url)) {
735
+ url = this.options.url + url
669
736
  }
670
737
 
671
- if (this.options.basicAuth && (this.isAuthenticated !== true)) {
738
+ if (this.options.basicAuth && this.isAuthenticated !== true) {
672
739
  if (url.includes(this.options.url)) {
673
- await this.page.authenticate(this.options.basicAuth);
674
- this.isAuthenticated = true;
740
+ await this.page.authenticate(this.options.basicAuth)
741
+ this.isAuthenticated = true
675
742
  }
676
743
  }
677
744
 
678
745
  if (this.options.trace) {
679
- const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`;
680
- const dir = path.dirname(fileName);
681
- if (!fileExists(dir)) fs.mkdirSync(dir);
682
- await this.page.tracing.start({ screenshots: true, path: fileName });
683
- this.currentRunningTest.artifacts.trace = fileName;
746
+ const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
747
+ const dir = path.dirname(fileName)
748
+ if (!fileExists(dir)) fs.mkdirSync(dir)
749
+ await this.page.tracing.start({ screenshots: true, path: fileName })
750
+ this.currentRunningTest.artifacts.trace = fileName
684
751
  }
685
752
 
686
- 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
+ }
687
768
 
688
- const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)));
769
+ const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
689
770
 
690
- perfTiming = this._extractDataFromPerformanceTiming(
691
- performanceTiming,
692
- 'responseEnd',
693
- 'domInteractive',
694
- 'domContentLoadedEventEnd',
695
- 'loadEventEnd',
696
- );
771
+ perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
697
772
 
698
- return this._waitForAction();
773
+ return this._waitForAction()
699
774
  }
700
775
 
701
776
  /**
@@ -709,11 +784,11 @@ class Puppeteer extends Helper {
709
784
  */
710
785
  async resizeWindow(width, height) {
711
786
  if (width === 'maximize') {
712
- throw new Error('Puppeteer can\'t control windows, so it can\'t maximize it');
787
+ throw new Error("Puppeteer can't control windows, so it can't maximize it")
713
788
  }
714
789
 
715
- await this.page.setViewport({ width, height });
716
- return this._waitForAction();
790
+ await this.page.setViewport({ width, height })
791
+ return this._waitForAction()
717
792
  }
718
793
 
719
794
  /**
@@ -729,9 +804,9 @@ class Puppeteer extends Helper {
729
804
  */
730
805
  async setPuppeteerRequestHeaders(customHeaders) {
731
806
  if (!customHeaders) {
732
- throw new Error('Cannot send empty headers.');
807
+ throw new Error('Cannot send empty headers.')
733
808
  }
734
- return this.page.setExtraHTTPHeaders(customHeaders);
809
+ return this.page.setExtraHTTPHeaders(customHeaders)
735
810
  }
736
811
 
737
812
  /**
@@ -739,13 +814,15 @@ class Puppeteer extends Helper {
739
814
  * {{ react }}
740
815
  */
741
816
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
742
- const els = await this._locate(locator);
743
- assertElementExists(els, locator);
817
+ const el = await this._locateElement(locator)
818
+ if (!el) {
819
+ throw new ElementNotFound(locator, 'Element to move cursor to')
820
+ }
744
821
 
745
822
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
746
- const { x, y } = await getClickablePoint(els[0]);
747
- await this.page.mouse.move(x + offsetX, y + offsetY);
748
- return this._waitForAction();
823
+ const { x, y } = await getClickablePoint(el)
824
+ await this.page.mouse.move(x + offsetX, y + offsetY)
825
+ return this._waitForAction()
749
826
  }
750
827
 
751
828
  /**
@@ -753,13 +830,14 @@ class Puppeteer extends Helper {
753
830
  *
754
831
  */
755
832
  async focus(locator) {
756
- const els = await this._locate(locator);
757
- assertElementExists(els, locator, 'Element to focus');
758
- const el = els[0];
833
+ const el = await this._locateElement(locator)
834
+ if (!el) {
835
+ throw new ElementNotFound(locator, 'Element to focus')
836
+ }
759
837
 
760
- await el.click();
761
- await el.focus();
762
- return this._waitForAction();
838
+ await el.click()
839
+ await el.focus()
840
+ return this._waitForAction()
763
841
  }
764
842
 
765
843
  /**
@@ -767,25 +845,27 @@ class Puppeteer extends Helper {
767
845
  *
768
846
  */
769
847
  async blur(locator) {
770
- const els = await this._locate(locator);
771
- 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
+ }
772
852
 
773
- await blurElement(els[0], this.page);
774
- return this._waitForAction();
853
+ await blurElement(el, this.page)
854
+ return this._waitForAction()
775
855
  }
776
856
 
777
857
  /**
778
858
  * {{> dragAndDrop }}
779
859
  */
780
860
  async dragAndDrop(srcElement, destElement) {
781
- return proceedDragAndDrop.call(this, srcElement, destElement);
861
+ return proceedDragAndDrop.call(this, srcElement, destElement)
782
862
  }
783
863
 
784
864
  /**
785
865
  * {{> refreshPage }}
786
866
  */
787
867
  async refreshPage() {
788
- return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
868
+ return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation })
789
869
  }
790
870
 
791
871
  /**
@@ -793,8 +873,8 @@ class Puppeteer extends Helper {
793
873
  */
794
874
  scrollPageToTop() {
795
875
  return this.executeScript(() => {
796
- window.scrollTo(0, 0);
797
- });
876
+ window.scrollTo(0, 0)
877
+ })
798
878
  }
799
879
 
800
880
  /**
@@ -802,16 +882,10 @@ class Puppeteer extends Helper {
802
882
  */
803
883
  scrollPageToBottom() {
804
884
  return this.executeScript(() => {
805
- const body = document.body;
806
- const html = document.documentElement;
807
- window.scrollTo(0, Math.max(
808
- body.scrollHeight,
809
- body.offsetHeight,
810
- html.clientHeight,
811
- html.scrollHeight,
812
- html.offsetHeight,
813
- ));
814
- });
885
+ const body = document.body
886
+ const html = document.documentElement
887
+ window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
888
+ })
815
889
  }
816
890
 
817
891
  /**
@@ -819,68 +893,68 @@ class Puppeteer extends Helper {
819
893
  */
820
894
  async scrollTo(locator, offsetX = 0, offsetY = 0) {
821
895
  if (typeof locator === 'number' && typeof offsetX === 'number') {
822
- offsetY = offsetX;
823
- offsetX = locator;
824
- locator = null;
896
+ offsetY = offsetX
897
+ offsetX = locator
898
+ locator = null
825
899
  }
826
900
 
827
901
  if (locator) {
828
- const els = await this._locate(locator);
829
- assertElementExists(els, locator, 'Element');
830
- const el = els[0];
831
- await el.evaluate((el) => el.scrollIntoView());
832
- const elementCoordinates = await getClickablePoint(els[0]);
833
- await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
902
+ const el = await this._locateElement(locator)
903
+ if (!el) {
904
+ throw new ElementNotFound(locator, 'Element to scroll into view')
905
+ }
906
+ await el.evaluate(el => el.scrollIntoView())
907
+ const elementCoordinates = await getClickablePoint(el)
908
+ await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
834
909
  } else {
835
- await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
910
+ await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
836
911
  }
837
- return this._waitForAction();
912
+ return this._waitForAction()
838
913
  }
839
914
 
840
915
  /**
841
916
  * {{> seeInTitle }}
842
917
  */
843
918
  async seeInTitle(text) {
844
- const title = await this.page.title();
845
- stringIncludes('web page title').assert(text, title);
919
+ const title = await this.page.title()
920
+ stringIncludes('web page title').assert(text, title)
846
921
  }
847
922
 
848
923
  /**
849
924
  * {{> grabPageScrollPosition }}
850
925
  */
851
926
  async grabPageScrollPosition() {
852
- /* eslint-disable comma-dangle */
853
927
  function getScrollPosition() {
854
928
  return {
855
929
  x: window.pageXOffset,
856
- y: window.pageYOffset
857
- };
930
+ y: window.pageYOffset,
931
+ }
858
932
  }
859
- /* eslint-enable comma-dangle */
860
- return this.executeScript(getScrollPosition);
933
+
934
+ return this.executeScript(getScrollPosition)
861
935
  }
862
936
 
863
937
  /**
864
938
  * {{> seeTitleEquals }}
865
939
  */
866
940
  async seeTitleEquals(text) {
867
- const title = await this.page.title();
868
- return equals('web page title').assert(title, text);
941
+ const title = await this.page.title()
942
+ return equals('web page title').assert(title, text)
869
943
  }
870
944
 
871
945
  /**
872
946
  * {{> dontSeeInTitle }}
873
947
  */
874
948
  async dontSeeInTitle(text) {
875
- const title = await this.page.title();
876
- stringIncludes('web page title').negate(text, title);
949
+ const title = await this.page.title()
950
+ stringIncludes('web page title').negate(text, title)
877
951
  }
878
952
 
879
953
  /**
880
954
  * {{> grabTitle }}
881
955
  */
882
956
  async grabTitle() {
883
- return this.page.title();
957
+ return this.page.title()
884
958
  }
885
959
 
886
960
  /**
@@ -894,8 +968,23 @@ class Puppeteer extends Helper {
894
968
  * {{ react }}
895
969
  */
896
970
  async _locate(locator) {
897
- const context = await this.context;
898
- return findElements.call(this, context, locator);
971
+ const context = await this.context
972
+ return findElements.call(this, context, locator)
973
+ }
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)
899
988
  }
900
989
 
901
990
  /**
@@ -907,10 +996,12 @@ class Puppeteer extends Helper {
907
996
  * ```
908
997
  */
909
998
  async _locateCheckable(locator, providedContext = null) {
910
- const context = providedContext || (await this._getContext());
911
- const els = await findCheckable.call(this, locator, context);
912
- assertElementExists(els[0], locator, 'Checkbox or radio');
913
- return els[0];
999
+ const context = providedContext || (await this._getContext())
1000
+ const els = await findCheckable.call(this, locator, context)
1001
+ if (!els || els.length === 0) {
1002
+ throw new ElementNotFound(locator, 'Checkbox or radio')
1003
+ }
1004
+ return els[0]
914
1005
  }
915
1006
 
916
1007
  /**
@@ -921,8 +1012,8 @@ class Puppeteer extends Helper {
921
1012
  * ```
922
1013
  */
923
1014
  async _locateClickable(locator) {
924
- const context = await this.context;
925
- return findClickable.call(this, context, locator);
1015
+ const context = await this.context
1016
+ return findClickable.call(this, context, locator)
926
1017
  }
927
1018
 
928
1019
  /**
@@ -933,7 +1024,7 @@ class Puppeteer extends Helper {
933
1024
  * ```
934
1025
  */
935
1026
  async _locateFields(locator) {
936
- return findFields.call(this, locator);
1027
+ return findFields.call(this, locator)
937
1028
  }
938
1029
 
939
1030
  /**
@@ -941,7 +1032,26 @@ class Puppeteer extends Helper {
941
1032
  *
942
1033
  */
943
1034
  async grabWebElements(locator) {
944
- 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)
1049
+ }
1050
+
1051
+ async grabWebElement(locator) {
1052
+ const els = await this._locate(locator)
1053
+ assertElementExists(els, locator)
1054
+ return els[0]
945
1055
  }
946
1056
 
947
1057
  /**
@@ -955,17 +1065,17 @@ class Puppeteer extends Helper {
955
1065
  * @param {number} [num=1]
956
1066
  */
957
1067
  async switchToNextTab(num = 1) {
958
- const pages = await this.browser.pages();
959
- const index = pages.indexOf(this.page);
960
- this.withinLocator = null;
961
- const page = pages[index + num];
1068
+ const pages = await this.browser.pages()
1069
+ const index = pages.indexOf(this.page)
1070
+ this.withinLocator = null
1071
+ const page = pages[index + num]
962
1072
 
963
1073
  if (!page) {
964
- throw new Error(`There is no ability to switch to next tab with offset ${num}`);
1074
+ throw new Error(`There is no ability to switch to next tab with offset ${num}`)
965
1075
  }
966
1076
 
967
- await this._setPage(page);
968
- return this._waitForAction();
1077
+ await this._setPage(page)
1078
+ return this._waitForAction()
969
1079
  }
970
1080
 
971
1081
  /**
@@ -978,17 +1088,17 @@ class Puppeteer extends Helper {
978
1088
  * @param {number} [num=1]
979
1089
  */
980
1090
  async switchToPreviousTab(num = 1) {
981
- const pages = await this.browser.pages();
982
- const index = pages.indexOf(this.page);
983
- this.withinLocator = null;
984
- const page = pages[index - num];
1091
+ const pages = await this.browser.pages()
1092
+ const index = pages.indexOf(this.page)
1093
+ this.withinLocator = null
1094
+ const page = pages[index - num]
985
1095
 
986
1096
  if (!page) {
987
- throw new Error(`There is no ability to switch to previous tab with offset ${num}`);
1097
+ throw new Error(`There is no ability to switch to previous tab with offset ${num}`)
988
1098
  }
989
1099
 
990
- await this._setPage(page);
991
- return this._waitForAction();
1100
+ await this._setPage(page)
1101
+ return this._waitForAction()
992
1102
  }
993
1103
 
994
1104
  /**
@@ -999,10 +1109,10 @@ class Puppeteer extends Helper {
999
1109
  * ```
1000
1110
  */
1001
1111
  async closeCurrentTab() {
1002
- const oldPage = this.page;
1003
- await this.switchToPreviousTab();
1004
- await oldPage.close();
1005
- return this._waitForAction();
1112
+ const oldPage = this.page
1113
+ await this.switchToPreviousTab()
1114
+ await oldPage.close()
1115
+ return this._waitForAction()
1006
1116
  }
1007
1117
 
1008
1118
  /**
@@ -1013,15 +1123,15 @@ class Puppeteer extends Helper {
1013
1123
  * ```
1014
1124
  */
1015
1125
  async closeOtherTabs() {
1016
- const pages = await this.browser.pages();
1017
- const otherPages = pages.filter(page => page !== this.page);
1126
+ const pages = await this.browser.pages()
1127
+ const otherPages = pages.filter(page => page !== this.page)
1018
1128
 
1019
- let p = Promise.resolve();
1020
- otherPages.forEach((page) => {
1021
- p = p.then(() => page.close());
1022
- });
1023
- await p;
1024
- return this._waitForAction();
1129
+ let p = Promise.resolve()
1130
+ otherPages.forEach(page => {
1131
+ p = p.then(() => page.close())
1132
+ })
1133
+ await p
1134
+ return this._waitForAction()
1025
1135
  }
1026
1136
 
1027
1137
  /**
@@ -1032,16 +1142,16 @@ class Puppeteer extends Helper {
1032
1142
  * ```
1033
1143
  */
1034
1144
  async openNewTab() {
1035
- await this._setPage(await this.browser.newPage());
1036
- return this._waitForAction();
1145
+ await this._setPage(await this.browser.newPage())
1146
+ return this._waitForAction()
1037
1147
  }
1038
1148
 
1039
1149
  /**
1040
1150
  * {{> grabNumberOfOpenTabs }}
1041
1151
  */
1042
1152
  async grabNumberOfOpenTabs() {
1043
- const pages = await this.browser.pages();
1044
- return pages.length;
1153
+ const pages = await this.browser.pages()
1154
+ return pages.length
1045
1155
  }
1046
1156
 
1047
1157
  /**
@@ -1049,14 +1159,14 @@ class Puppeteer extends Helper {
1049
1159
  * {{ react }}
1050
1160
  */
1051
1161
  async seeElement(locator) {
1052
- let els = await this._locate(locator);
1053
- els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1162
+ let els = await this._locate(locator)
1163
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1054
1164
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1055
- els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1165
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
1056
1166
  try {
1057
- return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
1167
+ return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
1058
1168
  } catch (e) {
1059
- dontSeeElementError(locator);
1169
+ dontSeeElementError(locator)
1060
1170
  }
1061
1171
  }
1062
1172
 
@@ -1065,14 +1175,14 @@ class Puppeteer extends Helper {
1065
1175
  * {{ react }}
1066
1176
  */
1067
1177
  async dontSeeElement(locator) {
1068
- let els = await this._locate(locator);
1069
- els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1178
+ let els = await this._locate(locator)
1179
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1070
1180
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1071
- els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1181
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
1072
1182
  try {
1073
- return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
1183
+ return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
1074
1184
  } catch (e) {
1075
- seeElementError(locator);
1185
+ seeElementError(locator)
1076
1186
  }
1077
1187
  }
1078
1188
 
@@ -1080,11 +1190,11 @@ class Puppeteer extends Helper {
1080
1190
  * {{> seeElementInDOM }}
1081
1191
  */
1082
1192
  async seeElementInDOM(locator) {
1083
- const els = await this._locate(locator);
1193
+ const els = await this._locate(locator)
1084
1194
  try {
1085
- return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'));
1195
+ return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
1086
1196
  } catch (e) {
1087
- dontSeeElementInDOMError(locator);
1197
+ dontSeeElementInDOMError(locator)
1088
1198
  }
1089
1199
  }
1090
1200
 
@@ -1092,11 +1202,11 @@ class Puppeteer extends Helper {
1092
1202
  * {{> dontSeeElementInDOM }}
1093
1203
  */
1094
1204
  async dontSeeElementInDOM(locator) {
1095
- const els = await this._locate(locator);
1205
+ const els = await this._locate(locator)
1096
1206
  try {
1097
- return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'));
1207
+ return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
1098
1208
  } catch (e) {
1099
- seeElementInDOMError(locator);
1209
+ seeElementInDOMError(locator)
1100
1210
  }
1101
1211
  }
1102
1212
 
@@ -1105,8 +1215,8 @@ class Puppeteer extends Helper {
1105
1215
  *
1106
1216
  * {{ react }}
1107
1217
  */
1108
- async click(locator, context = null) {
1109
- return proceedClick.call(this, locator, context);
1218
+ async click(locator = '//body', context = null) {
1219
+ return proceedClick.call(this, locator, context)
1110
1220
  }
1111
1221
 
1112
1222
  /**
@@ -1115,28 +1225,28 @@ class Puppeteer extends Helper {
1115
1225
  * {{ react }}
1116
1226
  */
1117
1227
  async forceClick(locator, context = null) {
1118
- let matcher = await this.context;
1228
+ let matcher = await this.context
1119
1229
  if (context) {
1120
- const els = await this._locate(context);
1121
- assertElementExists(els, context);
1122
- matcher = els[0];
1230
+ const els = await this._locate(context)
1231
+ assertElementExists(els, context)
1232
+ matcher = els[0]
1123
1233
  }
1124
1234
 
1125
- const els = await findClickable.call(this, matcher, locator);
1235
+ const els = await findClickable.call(this, matcher, locator)
1126
1236
  if (context) {
1127
- assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`);
1237
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
1128
1238
  } else {
1129
- assertElementExists(els, locator, 'Clickable element');
1239
+ assertElementExists(els, locator, 'Clickable element')
1130
1240
  }
1131
- const elem = els[0];
1132
- return this.executeScript((el) => {
1241
+ const elem = els[0]
1242
+ return this.executeScript(el => {
1133
1243
  if (document.activeElement instanceof HTMLElement) {
1134
- document.activeElement.blur();
1244
+ document.activeElement.blur()
1135
1245
  }
1136
- const event = document.createEvent('MouseEvent');
1137
- event.initEvent('click', true, true);
1138
- return el.dispatchEvent(event);
1139
- }, elem);
1246
+ const event = document.createEvent('MouseEvent')
1247
+ event.initEvent('click', true, true)
1248
+ return el.dispatchEvent(event)
1249
+ }, elem)
1140
1250
  }
1141
1251
 
1142
1252
  /**
@@ -1145,7 +1255,7 @@ class Puppeteer extends Helper {
1145
1255
  * {{ react }}
1146
1256
  */
1147
1257
  async clickLink(locator, context = null) {
1148
- return proceedClick.call(this, locator, context, { waitForNavigation: true });
1258
+ return proceedClick.call(this, locator, context, { waitForNavigation: true })
1149
1259
  }
1150
1260
 
1151
1261
  /**
@@ -1166,16 +1276,16 @@ class Puppeteer extends Helper {
1166
1276
  * @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
1167
1277
  */
1168
1278
  async handleDownloads(downloadPath = 'downloads') {
1169
- downloadPath = path.join(global.output_dir, downloadPath);
1279
+ downloadPath = path.join(global.output_dir, downloadPath)
1170
1280
  if (!fs.existsSync(downloadPath)) {
1171
- fs.mkdirSync(downloadPath, '0777');
1281
+ fs.mkdirSync(downloadPath, '0777')
1172
1282
  }
1173
- fsExtra.emptyDirSync(downloadPath);
1283
+ fsExtra.emptyDirSync(downloadPath)
1174
1284
 
1175
1285
  try {
1176
- return this.page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath });
1286
+ return this.page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
1177
1287
  } catch (e) {
1178
- return this.page._client().send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath });
1288
+ return this.page._client().send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
1179
1289
  }
1180
1290
  }
1181
1291
 
@@ -1185,27 +1295,27 @@ class Puppeteer extends Helper {
1185
1295
  * Please use `handleDownloads()` instead.
1186
1296
  */
1187
1297
  async downloadFile(locator, customName) {
1188
- let fileName;
1189
- await this.page.setRequestInterception(true);
1190
-
1191
- const xRequest = await new Promise((resolve) => {
1192
- this.page.on('request', (request) => {
1193
- console.log('rq', request, customName);
1194
- const grabbedFileName = request.url().split('/')[request.url().split('/').length - 1];
1195
- const fileExtension = request.url().split('/')[request.url().split('/').length - 1].split('.')[1];
1196
- console.log('nm', customName, fileExtension);
1298
+ let fileName
1299
+ await this.page.setRequestInterception(true)
1300
+
1301
+ const xRequest = await new Promise(resolve => {
1302
+ this.page.on('request', request => {
1303
+ console.log('rq', request, customName)
1304
+ const grabbedFileName = request.url().split('/')[request.url().split('/').length - 1]
1305
+ const fileExtension = request.url().split('/')[request.url().split('/').length - 1].split('.')[1]
1306
+ console.log('nm', customName, fileExtension)
1197
1307
  if (customName && path.extname(customName) !== fileExtension) {
1198
- console.log('bypassing a request');
1199
- request.continue();
1200
- return;
1308
+ console.log('bypassing a request')
1309
+ request.continue()
1310
+ return
1201
1311
  }
1202
- customName ? fileName = `${customName}.${fileExtension}` : fileName = grabbedFileName;
1203
- request.abort();
1204
- resolve(request);
1205
- });
1206
- });
1312
+ customName ? (fileName = `${customName}.${fileExtension}`) : (fileName = grabbedFileName)
1313
+ request.abort()
1314
+ resolve(request)
1315
+ })
1316
+ })
1207
1317
 
1208
- await this.click(locator);
1318
+ await this.click(locator)
1209
1319
 
1210
1320
  const options = {
1211
1321
  encoding: null,
@@ -1213,10 +1323,10 @@ class Puppeteer extends Helper {
1213
1323
  uri: xRequest._url,
1214
1324
  body: xRequest._postData,
1215
1325
  headers: xRequest._headers,
1216
- };
1326
+ }
1217
1327
 
1218
- const cookies = await this.page.cookies();
1219
- options.headers.Cookie = cookies.map(ck => `${ck.name}=${ck.value}`).join(';');
1328
+ const cookies = await this.page.cookies()
1329
+ options.headers.Cookie = cookies.map(ck => `${ck.name}=${ck.value}`).join(';')
1220
1330
 
1221
1331
  const response = await axios({
1222
1332
  method: options.method,
@@ -1224,24 +1334,26 @@ class Puppeteer extends Helper {
1224
1334
  headers: options.headers,
1225
1335
  responseType: 'arraybuffer',
1226
1336
  onDownloadProgress(e) {
1227
- console.log('+', e);
1337
+ console.log('+', e)
1228
1338
  },
1229
- });
1339
+ })
1230
1340
 
1231
- const outputFile = path.join(`${global.output_dir}/${fileName}`);
1341
+ const outputFile = path.join(`${global.output_dir}/${fileName}`)
1232
1342
 
1233
1343
  try {
1234
1344
  await new Promise((resolve, reject) => {
1235
- const wstream = fs.createWriteStream(outputFile);
1236
- console.log(response);
1237
- wstream.write(response.data);
1238
- wstream.end();
1239
- this.debug(`File is downloaded in ${outputFile}`);
1240
- wstream.on('finish', () => { resolve(fileName); });
1241
- wstream.on('error', reject);
1242
- });
1345
+ const wstream = fs.createWriteStream(outputFile)
1346
+ console.log(response)
1347
+ wstream.write(response.data)
1348
+ wstream.end()
1349
+ this.debug(`File is downloaded in ${outputFile}`)
1350
+ wstream.on('finish', () => {
1351
+ resolve(fileName)
1352
+ })
1353
+ wstream.on('error', reject)
1354
+ })
1243
1355
  } catch (error) {
1244
- throw new Error(`There is something wrong with downloaded file. ${error}`);
1356
+ throw new Error(`There is something wrong with downloaded file. ${error}`)
1245
1357
  }
1246
1358
  }
1247
1359
 
@@ -1251,7 +1363,7 @@ class Puppeteer extends Helper {
1251
1363
  * {{ react }}
1252
1364
  */
1253
1365
  async doubleClick(locator, context = null) {
1254
- return proceedClick.call(this, locator, context, { clickCount: 2 });
1366
+ return proceedClick.call(this, locator, context, { clickCount: 2 })
1255
1367
  }
1256
1368
 
1257
1369
  /**
@@ -1260,20 +1372,70 @@ class Puppeteer extends Helper {
1260
1372
  * {{ react }}
1261
1373
  */
1262
1374
  async rightClick(locator, context = null) {
1263
- return proceedClick.call(this, locator, context, { button: 'right' });
1375
+ return proceedClick.call(this, locator, context, { button: 'right' })
1376
+ }
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()
1264
1419
  }
1265
1420
 
1266
1421
  /**
1267
1422
  * {{> checkOption }}
1268
1423
  */
1269
1424
  async checkOption(field, context = null) {
1270
- const elm = await this._locateCheckable(field, context);
1271
- const curentlyChecked = await elm.getProperty('checked')
1272
- .then(checkedProperty => checkedProperty.jsonValue());
1273
- // Only check if NOT currently checked
1425
+ const elm = await this._locateCheckable(field, context)
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
+
1274
1436
  if (!curentlyChecked) {
1275
- await elm.click();
1276
- return this._waitForAction();
1437
+ await elm.click()
1438
+ return this._waitForAction()
1277
1439
  }
1278
1440
  }
1279
1441
 
@@ -1281,13 +1443,20 @@ class Puppeteer extends Helper {
1281
1443
  * {{> uncheckOption }}
1282
1444
  */
1283
1445
  async uncheckOption(field, context = null) {
1284
- const elm = await this._locateCheckable(field, context);
1285
- const curentlyChecked = await elm.getProperty('checked')
1286
- .then(checkedProperty => checkedProperty.jsonValue());
1287
- // Only uncheck if currently checked
1446
+ const elm = await this._locateCheckable(field, context)
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
+
1288
1457
  if (curentlyChecked) {
1289
- await elm.click();
1290
- return this._waitForAction();
1458
+ await elm.click()
1459
+ return this._waitForAction()
1291
1460
  }
1292
1461
  }
1293
1462
 
@@ -1295,62 +1464,62 @@ class Puppeteer extends Helper {
1295
1464
  * {{> seeCheckboxIsChecked }}
1296
1465
  */
1297
1466
  async seeCheckboxIsChecked(field) {
1298
- return proceedIsChecked.call(this, 'assert', field);
1467
+ return proceedIsChecked.call(this, 'assert', field)
1299
1468
  }
1300
1469
 
1301
1470
  /**
1302
1471
  * {{> dontSeeCheckboxIsChecked }}
1303
1472
  */
1304
1473
  async dontSeeCheckboxIsChecked(field) {
1305
- return proceedIsChecked.call(this, 'negate', field);
1474
+ return proceedIsChecked.call(this, 'negate', field)
1306
1475
  }
1307
1476
 
1308
1477
  /**
1309
1478
  * {{> pressKeyDown }}
1310
1479
  */
1311
1480
  async pressKeyDown(key) {
1312
- key = getNormalizedKey.call(this, key);
1313
- await this.page.keyboard.down(key);
1314
- return this._waitForAction();
1481
+ key = getNormalizedKey.call(this, key)
1482
+ await this.page.keyboard.down(key)
1483
+ return this._waitForAction()
1315
1484
  }
1316
1485
 
1317
1486
  /**
1318
1487
  * {{> pressKeyUp }}
1319
1488
  */
1320
1489
  async pressKeyUp(key) {
1321
- key = getNormalizedKey.call(this, key);
1322
- await this.page.keyboard.up(key);
1323
- return this._waitForAction();
1490
+ key = getNormalizedKey.call(this, key)
1491
+ await this.page.keyboard.up(key)
1492
+ return this._waitForAction()
1324
1493
  }
1325
1494
 
1326
1495
  /**
1327
- * _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)).
1328
1497
  *
1329
1498
  * {{> pressKeyWithKeyNormalization }}
1330
1499
  */
1331
1500
  async pressKey(key) {
1332
- const modifiers = [];
1501
+ const modifiers = []
1333
1502
  if (Array.isArray(key)) {
1334
1503
  for (let k of key) {
1335
- k = getNormalizedKey.call(this, k);
1504
+ k = getNormalizedKey.call(this, k)
1336
1505
  if (isModifierKey(k)) {
1337
- modifiers.push(k);
1506
+ modifiers.push(k)
1338
1507
  } else {
1339
- key = k;
1340
- break;
1508
+ key = k
1509
+ break
1341
1510
  }
1342
1511
  }
1343
1512
  } else {
1344
- key = getNormalizedKey.call(this, key);
1513
+ key = getNormalizedKey.call(this, key)
1345
1514
  }
1346
1515
  for (const modifier of modifiers) {
1347
- await this.page.keyboard.down(modifier);
1516
+ await this.page.keyboard.down(modifier)
1348
1517
  }
1349
- await this.page.keyboard.press(key);
1518
+ await this.page.keyboard.press(key)
1350
1519
  for (const modifier of modifiers) {
1351
- await this.page.keyboard.up(modifier);
1520
+ await this.page.keyboard.up(modifier)
1352
1521
  }
1353
- return this._waitForAction();
1522
+ return this._waitForAction()
1354
1523
  }
1355
1524
 
1356
1525
  /**
@@ -1358,13 +1527,13 @@ class Puppeteer extends Helper {
1358
1527
  */
1359
1528
  async type(keys, delay = null) {
1360
1529
  if (!Array.isArray(keys)) {
1361
- keys = keys.toString();
1362
- keys = keys.split('');
1530
+ keys = keys.toString()
1531
+ keys = keys.split('')
1363
1532
  }
1364
1533
 
1365
1534
  for (const key of keys) {
1366
- await this.page.keyboard.press(key);
1367
- if (delay) await this.wait(delay / 1000);
1535
+ await this.page.keyboard.press(key)
1536
+ if (delay) await this.wait(delay / 1000)
1368
1537
  }
1369
1538
  }
1370
1539
 
@@ -1373,28 +1542,28 @@ class Puppeteer extends Helper {
1373
1542
  * {{ react }}
1374
1543
  */
1375
1544
  async fillField(field, value) {
1376
- const els = await findVisibleFields.call(this, field);
1377
- assertElementExists(els, field, 'Field');
1378
- const el = els[0];
1379
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
1380
- const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
1545
+ const els = await findVisibleFields.call(this, field)
1546
+ assertElementExists(els, field, 'Field')
1547
+ const el = els[0]
1548
+ const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1549
+ const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1381
1550
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
1382
- await this._evaluateHandeInContext(el => el.value = '', el);
1551
+ await this._evaluateHandeInContext(el => (el.value = ''), el)
1383
1552
  } else if (editable) {
1384
- await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1553
+ await this._evaluateHandeInContext(el => (el.innerHTML = ''), el)
1385
1554
  }
1386
1555
 
1387
- highlightActiveElement.call(this, el, await this._getContext());
1388
- await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1556
+ highlightActiveElement.call(this, el, await this._getContext())
1557
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1389
1558
 
1390
- return this._waitForAction();
1559
+ return this._waitForAction()
1391
1560
  }
1392
1561
 
1393
1562
  /**
1394
1563
  * {{> clearField }}
1395
1564
  */
1396
1565
  async clearField(field) {
1397
- return this.fillField(field, '');
1566
+ return this.fillField(field, '')
1398
1567
  }
1399
1568
 
1400
1569
  /**
@@ -1403,28 +1572,28 @@ class Puppeteer extends Helper {
1403
1572
  * {{ react }}
1404
1573
  */
1405
1574
  async appendField(field, value) {
1406
- const els = await findVisibleFields.call(this, field);
1407
- assertElementExists(els, field, 'Field');
1408
- highlightActiveElement.call(this, els[0], await this._getContext());
1409
- await els[0].press('End');
1410
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
1411
- return this._waitForAction();
1575
+ const els = await findVisibleFields.call(this, field)
1576
+ assertElementExists(els, field, 'Field')
1577
+ highlightActiveElement.call(this, els[0], await this._getContext())
1578
+ await els[0].press('End')
1579
+ await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
1580
+ return this._waitForAction()
1412
1581
  }
1413
1582
 
1414
1583
  /**
1415
1584
  * {{> seeInField }}
1416
1585
  */
1417
1586
  async seeInField(field, value) {
1418
- const _value = (typeof value === 'boolean') ? value : value.toString();
1419
- return proceedSeeInField.call(this, 'assert', field, _value);
1587
+ const _value = typeof value === 'boolean' ? value : value.toString()
1588
+ return proceedSeeInField.call(this, 'assert', field, _value)
1420
1589
  }
1421
1590
 
1422
1591
  /**
1423
1592
  * {{> dontSeeInField }}
1424
1593
  */
1425
1594
  async dontSeeInField(field, value) {
1426
- const _value = (typeof value === 'boolean') ? value : value.toString();
1427
- return proceedSeeInField.call(this, 'negate', field, _value);
1595
+ const _value = typeof value === 'boolean' ? value : value.toString()
1596
+ return proceedSeeInField.call(this, 'negate', field, _value)
1428
1597
  }
1429
1598
 
1430
1599
  /**
@@ -1433,48 +1602,48 @@ class Puppeteer extends Helper {
1433
1602
  * {{> attachFile }}
1434
1603
  */
1435
1604
  async attachFile(locator, pathToFile) {
1436
- const file = path.join(global.codecept_dir, pathToFile);
1605
+ const file = path.join(global.codecept_dir, pathToFile)
1437
1606
 
1438
1607
  if (!fileExists(file)) {
1439
- throw new Error(`File at ${file} can not be found on local system`);
1608
+ throw new Error(`File at ${file} can not be found on local system`)
1440
1609
  }
1441
- const els = await findFields.call(this, locator);
1442
- assertElementExists(els, locator, 'Field');
1443
- await els[0].uploadFile(file);
1444
- return this._waitForAction();
1610
+ const els = await findFields.call(this, locator)
1611
+ assertElementExists(els, locator, 'Field')
1612
+ await els[0].uploadFile(file)
1613
+ return this._waitForAction()
1445
1614
  }
1446
1615
 
1447
1616
  /**
1448
1617
  * {{> selectOption }}
1449
1618
  */
1450
1619
  async selectOption(select, option) {
1451
- const els = await findVisibleFields.call(this, select);
1452
- assertElementExists(els, select, 'Selectable field');
1453
- const el = els[0];
1620
+ const els = await findVisibleFields.call(this, select)
1621
+ assertElementExists(els, select, 'Selectable field')
1622
+ const el = els[0]
1454
1623
  if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
1455
- throw new Error('Element is not <select>');
1624
+ throw new Error('Element is not <select>')
1456
1625
  }
1457
- highlightActiveElement.call(this, els[0], await this._getContext());
1458
- if (!Array.isArray(option)) option = [option];
1626
+ highlightActiveElement.call(this, els[0], await this._getContext())
1627
+ if (!Array.isArray(option)) option = [option]
1459
1628
 
1460
1629
  for (const key in option) {
1461
- const opt = xpathLocator.literal(option[key]);
1462
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
1630
+ const opt = xpathLocator.literal(option[key])
1631
+ let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
1463
1632
  if (optEl.length) {
1464
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1465
- continue;
1633
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1634
+ continue
1466
1635
  }
1467
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
1636
+ optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
1468
1637
  if (optEl.length) {
1469
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1638
+ this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1470
1639
  }
1471
1640
  }
1472
- await this._evaluateHandeInContext((element) => {
1473
- element.dispatchEvent(new Event('input', { bubbles: true }));
1474
- element.dispatchEvent(new Event('change', { bubbles: true }));
1475
- }, el);
1641
+ await this._evaluateHandeInContext(element => {
1642
+ element.dispatchEvent(new Event('input', { bubbles: true }))
1643
+ element.dispatchEvent(new Event('change', { bubbles: true }))
1644
+ }, el)
1476
1645
 
1477
- return this._waitForAction();
1646
+ return this._waitForAction()
1478
1647
  }
1479
1648
 
1480
1649
  /**
@@ -1482,40 +1651,40 @@ class Puppeteer extends Helper {
1482
1651
  * {{ react }}
1483
1652
  */
1484
1653
  async grabNumberOfVisibleElements(locator) {
1485
- let els = await this._locate(locator);
1486
- els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
1654
+ let els = await this._locate(locator)
1655
+ els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1487
1656
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1488
- els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
1657
+ els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
1489
1658
 
1490
- return els.filter(v => v).length;
1659
+ return els.filter(v => v).length
1491
1660
  }
1492
1661
 
1493
1662
  /**
1494
1663
  * {{> seeInCurrentUrl }}
1495
1664
  */
1496
1665
  async seeInCurrentUrl(url) {
1497
- stringIncludes('url').assert(url, await this._getPageUrl());
1666
+ stringIncludes('url').assert(url, await this._getPageUrl())
1498
1667
  }
1499
1668
 
1500
1669
  /**
1501
1670
  * {{> dontSeeInCurrentUrl }}
1502
1671
  */
1503
1672
  async dontSeeInCurrentUrl(url) {
1504
- stringIncludes('url').negate(url, await this._getPageUrl());
1673
+ stringIncludes('url').negate(url, await this._getPageUrl())
1505
1674
  }
1506
1675
 
1507
1676
  /**
1508
1677
  * {{> seeCurrentUrlEquals }}
1509
1678
  */
1510
1679
  async seeCurrentUrlEquals(url) {
1511
- urlEquals(this.options.url).assert(url, await this._getPageUrl());
1680
+ urlEquals(this.options.url).assert(url, await this._getPageUrl())
1512
1681
  }
1513
1682
 
1514
1683
  /**
1515
1684
  * {{> dontSeeCurrentUrlEquals }}
1516
1685
  */
1517
1686
  async dontSeeCurrentUrlEquals(url) {
1518
- urlEquals(this.options.url).negate(url, await this._getPageUrl());
1687
+ urlEquals(this.options.url).negate(url, await this._getPageUrl())
1519
1688
  }
1520
1689
 
1521
1690
  /**
@@ -1524,14 +1693,14 @@ class Puppeteer extends Helper {
1524
1693
  * {{ react }}
1525
1694
  */
1526
1695
  async see(text, context = null) {
1527
- return proceedSee.call(this, 'assert', text, context);
1696
+ return proceedSee.call(this, 'assert', text, context)
1528
1697
  }
1529
1698
 
1530
1699
  /**
1531
1700
  * {{> seeTextEquals }}
1532
1701
  */
1533
1702
  async seeTextEquals(text, context = null) {
1534
- return proceedSee.call(this, 'assert', text, context, true);
1703
+ return proceedSee.call(this, 'assert', text, context, true)
1535
1704
  }
1536
1705
 
1537
1706
  /**
@@ -1540,14 +1709,14 @@ class Puppeteer extends Helper {
1540
1709
  * {{ react }}
1541
1710
  */
1542
1711
  async dontSee(text, context = null) {
1543
- return proceedSee.call(this, 'negate', text, context);
1712
+ return proceedSee.call(this, 'negate', text, context)
1544
1713
  }
1545
1714
 
1546
1715
  /**
1547
1716
  * {{> grabSource }}
1548
1717
  */
1549
1718
  async grabSource() {
1550
- return this.page.content();
1719
+ return this.page.content()
1551
1720
  }
1552
1721
 
1553
1722
  /**
@@ -1560,32 +1729,32 @@ class Puppeteer extends Helper {
1560
1729
  * @return {Promise<any[]>}
1561
1730
  */
1562
1731
  async grabBrowserLogs() {
1563
- const logs = consoleLogStore.entries;
1564
- consoleLogStore.clear();
1565
- return logs;
1732
+ const logs = consoleLogStore.entries
1733
+ consoleLogStore.clear()
1734
+ return logs
1566
1735
  }
1567
1736
 
1568
1737
  /**
1569
1738
  * {{> grabCurrentUrl }}
1570
1739
  */
1571
1740
  async grabCurrentUrl() {
1572
- return this._getPageUrl();
1741
+ return this._getPageUrl()
1573
1742
  }
1574
1743
 
1575
1744
  /**
1576
1745
  * {{> seeInSource }}
1577
1746
  */
1578
1747
  async seeInSource(text) {
1579
- const source = await this.page.content();
1580
- stringIncludes('HTML source of a page').assert(text, source);
1748
+ const source = await this.page.content()
1749
+ stringIncludes('HTML source of a page').assert(text, source)
1581
1750
  }
1582
1751
 
1583
1752
  /**
1584
1753
  * {{> dontSeeInSource }}
1585
1754
  */
1586
1755
  async dontSeeInSource(text) {
1587
- const source = await this.page.content();
1588
- stringIncludes('HTML source of a page').negate(text, source);
1756
+ const source = await this.page.content()
1757
+ stringIncludes('HTML source of a page').negate(text, source)
1589
1758
  }
1590
1759
 
1591
1760
  /**
@@ -1594,8 +1763,8 @@ class Puppeteer extends Helper {
1594
1763
  * {{ react }}
1595
1764
  */
1596
1765
  async seeNumberOfElements(locator, num) {
1597
- const elements = await this._locate(locator);
1598
- return equals(`expected number of elements (${(new Locator(locator))}) is ${num}, but found ${elements.length}`).assert(elements.length, num);
1766
+ const elements = await this._locate(locator)
1767
+ return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
1599
1768
  }
1600
1769
 
1601
1770
  /**
@@ -1604,8 +1773,8 @@ class Puppeteer extends Helper {
1604
1773
  * {{ react }}
1605
1774
  */
1606
1775
  async seeNumberOfVisibleElements(locator, num) {
1607
- const res = await this.grabNumberOfVisibleElements(locator);
1608
- return equals(`expected number of visible elements (${(new Locator(locator))}) is ${num}, but found ${res}`).assert(res, num);
1776
+ const res = await this.grabNumberOfVisibleElements(locator)
1777
+ return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
1609
1778
  }
1610
1779
 
1611
1780
  /**
@@ -1613,9 +1782,9 @@ class Puppeteer extends Helper {
1613
1782
  */
1614
1783
  async setCookie(cookie) {
1615
1784
  if (Array.isArray(cookie)) {
1616
- return this.page.setCookie(...cookie);
1785
+ return this.page.setCookie(...cookie)
1617
1786
  }
1618
- return this.page.setCookie(cookie);
1787
+ return this.page.setCookie(cookie)
1619
1788
  }
1620
1789
 
1621
1790
  /**
@@ -1623,16 +1792,16 @@ class Puppeteer extends Helper {
1623
1792
  *
1624
1793
  */
1625
1794
  async seeCookie(name) {
1626
- const cookies = await this.page.cookies();
1627
- empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name));
1795
+ const cookies = await this.page.cookies()
1796
+ empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
1628
1797
  }
1629
1798
 
1630
1799
  /**
1631
1800
  * {{> dontSeeCookie }}
1632
1801
  */
1633
1802
  async dontSeeCookie(name) {
1634
- const cookies = await this.page.cookies();
1635
- empty(`cookie ${name} to be set`).assert(cookies.filter(c => c.name === name));
1803
+ const cookies = await this.page.cookies()
1804
+ empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
1636
1805
  }
1637
1806
 
1638
1807
  /**
@@ -1641,10 +1810,10 @@ class Puppeteer extends Helper {
1641
1810
  * Returns cookie in JSON format. If name not passed returns all cookies for this domain.
1642
1811
  */
1643
1812
  async grabCookie(name) {
1644
- const cookies = await this.page.cookies();
1645
- if (!name) return cookies;
1646
- const cookie = cookies.filter(c => c.name === name);
1647
- if (cookie[0]) return cookie[0];
1813
+ const cookies = await this.page.cookies()
1814
+ if (!name) return cookies
1815
+ const cookie = cookies.filter(c => c.name === name)
1816
+ if (cookie[0]) return cookie[0]
1648
1817
  }
1649
1818
 
1650
1819
  /**
@@ -1652,44 +1821,47 @@ class Puppeteer extends Helper {
1652
1821
  */
1653
1822
  async waitForCookie(name, sec) {
1654
1823
  // by default, we will retry 3 times
1655
- let retries = 3;
1656
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1824
+ let retries = 3
1825
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
1657
1826
 
1658
1827
  if (sec) {
1659
- retries = sec;
1828
+ retries = sec
1660
1829
  } else {
1661
- retries = Math.ceil(waitTimeout / 1000) - 1;
1830
+ retries = Math.ceil(waitTimeout / 1000) - 1
1662
1831
  }
1663
1832
 
1664
- return promiseRetry(async (retry, number) => {
1665
- const _grabCookie = async (name) => {
1666
- const cookies = await this.page.cookies();
1667
- const cookie = cookies.filter(c => c.name === name);
1668
- if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`);
1669
- };
1833
+ return promiseRetry(
1834
+ async (retry, number) => {
1835
+ const _grabCookie = async name => {
1836
+ const cookies = await this.page.cookies()
1837
+ const cookie = cookies.filter(c => c.name === name)
1838
+ if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
1839
+ }
1670
1840
 
1671
- this.debugSection('Wait for cookie: ', name);
1672
- if (number > 1) this.debugSection('Retrying... Attempt #', number);
1841
+ this.debugSection('Wait for cookie: ', name)
1842
+ if (number > 1) this.debugSection('Retrying... Attempt #', number)
1673
1843
 
1674
- try {
1675
- await _grabCookie(name);
1676
- } catch (e) {
1677
- retry(e);
1678
- }
1679
- }, { retries, maxTimeout: 1000 });
1844
+ try {
1845
+ await _grabCookie(name)
1846
+ } catch (e) {
1847
+ retry(e)
1848
+ }
1849
+ },
1850
+ { retries, maxTimeout: 1000 },
1851
+ )
1680
1852
  }
1681
1853
 
1682
1854
  /**
1683
1855
  * {{> clearCookie }}
1684
1856
  */
1685
1857
  async clearCookie(name) {
1686
- const cookies = await this.page.cookies();
1858
+ const cookies = await this.page.cookies()
1687
1859
  if (!name) {
1688
- return this.page.deleteCookie.apply(this.page, cookies);
1860
+ return this.page.deleteCookie.apply(this.page, cookies)
1689
1861
  }
1690
- const cookie = cookies.filter(c => c.name === name);
1691
- if (!cookie[0]) return;
1692
- return this.page.deleteCookie(cookie[0]);
1862
+ const cookie = cookies.filter(c => c.name === name)
1863
+ if (!cookie[0]) return
1864
+ return this.page.deleteCookie(cookie[0])
1693
1865
  }
1694
1866
 
1695
1867
  /**
@@ -1698,11 +1870,11 @@ class Puppeteer extends Helper {
1698
1870
  * {{> executeScript }}
1699
1871
  */
1700
1872
  async executeScript(...args) {
1701
- let context = await this._getContext();
1873
+ let context = await this._getContext()
1702
1874
  if (this.context && this.context.constructor.name === 'CdpFrame') {
1703
- context = this.context; // switching to iframe context
1875
+ context = this.context // switching to iframe context
1704
1876
  }
1705
- return context.evaluate.apply(context, args);
1877
+ return context.evaluate.apply(context, args)
1706
1878
  }
1707
1879
 
1708
1880
  /**
@@ -1711,16 +1883,16 @@ class Puppeteer extends Helper {
1711
1883
  */
1712
1884
  async executeAsyncScript(...args) {
1713
1885
  const asyncFn = function () {
1714
- const args = Array.from(arguments);
1715
- const fn = eval(`(${args.shift()})`); // eslint-disable-line no-eval
1716
- return new Promise((done) => {
1717
- args.push(done);
1718
- fn.apply(null, args);
1719
- });
1720
- };
1721
- args[0] = args[0].toString();
1722
- args.unshift(asyncFn);
1723
- return this.page.evaluate.apply(this.page, args);
1886
+ const args = Array.from(arguments)
1887
+ const fn = eval(`(${args.shift()})`)
1888
+ return new Promise(done => {
1889
+ args.push(done)
1890
+ fn.apply(null, args)
1891
+ })
1892
+ }
1893
+ args[0] = args[0].toString()
1894
+ args.unshift(asyncFn)
1895
+ return this.page.evaluate.apply(this.page, args)
1724
1896
  }
1725
1897
 
1726
1898
  /**
@@ -1728,12 +1900,12 @@ class Puppeteer extends Helper {
1728
1900
  * {{ react }}
1729
1901
  */
1730
1902
  async grabTextFromAll(locator) {
1731
- const els = await this._locate(locator);
1732
- const texts = [];
1903
+ const els = await this._locate(locator)
1904
+ const texts = []
1733
1905
  for (const el of els) {
1734
- texts.push(await (await el.getProperty('innerText')).jsonValue());
1906
+ texts.push(await (await el.getProperty('innerText')).jsonValue())
1735
1907
  }
1736
- return texts;
1908
+ return texts
1737
1909
  }
1738
1910
 
1739
1911
  /**
@@ -1741,60 +1913,60 @@ class Puppeteer extends Helper {
1741
1913
  * {{ react }}
1742
1914
  */
1743
1915
  async grabTextFrom(locator) {
1744
- const texts = await this.grabTextFromAll(locator);
1745
- assertElementExists(texts, locator);
1916
+ const texts = await this.grabTextFromAll(locator)
1917
+ assertElementExists(texts, locator)
1746
1918
  if (texts.length > 1) {
1747
- this.debugSection('GrabText', `Using first element out of ${texts.length}`);
1919
+ this.debugSection('GrabText', `Using first element out of ${texts.length}`)
1748
1920
  }
1749
1921
 
1750
- return texts[0];
1922
+ return texts[0]
1751
1923
  }
1752
1924
 
1753
1925
  /**
1754
1926
  * {{> grabValueFromAll }}
1755
1927
  */
1756
1928
  async grabValueFromAll(locator) {
1757
- const els = await findFields.call(this, locator);
1758
- const values = [];
1929
+ const els = await findFields.call(this, locator)
1930
+ const values = []
1759
1931
  for (const el of els) {
1760
- values.push(await (await el.getProperty('value')).jsonValue());
1932
+ values.push(await (await el.getProperty('value')).jsonValue())
1761
1933
  }
1762
- return values;
1934
+ return values
1763
1935
  }
1764
1936
 
1765
1937
  /**
1766
1938
  * {{> grabValueFrom }}
1767
1939
  */
1768
1940
  async grabValueFrom(locator) {
1769
- const values = await this.grabValueFromAll(locator);
1770
- assertElementExists(values, locator);
1941
+ const values = await this.grabValueFromAll(locator)
1942
+ assertElementExists(values, locator)
1771
1943
  if (values.length > 1) {
1772
- this.debugSection('GrabValue', `Using first element out of ${values.length}`);
1944
+ this.debugSection('GrabValue', `Using first element out of ${values.length}`)
1773
1945
  }
1774
1946
 
1775
- return values[0];
1947
+ return values[0]
1776
1948
  }
1777
1949
 
1778
1950
  /**
1779
1951
  * {{> grabHTMLFromAll }}
1780
1952
  */
1781
1953
  async grabHTMLFromAll(locator) {
1782
- const els = await this._locate(locator);
1783
- const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML, el)));
1784
- return values;
1954
+ const els = await this._locate(locator)
1955
+ const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML)))
1956
+ return values
1785
1957
  }
1786
1958
 
1787
1959
  /**
1788
1960
  * {{> grabHTMLFrom }}
1789
1961
  */
1790
1962
  async grabHTMLFrom(locator) {
1791
- const html = await this.grabHTMLFromAll(locator);
1792
- assertElementExists(html, locator);
1963
+ const html = await this.grabHTMLFromAll(locator)
1964
+ assertElementExists(html, locator)
1793
1965
  if (html.length > 1) {
1794
- this.debugSection('GrabHTML', `Using first element out of ${html.length}`);
1966
+ this.debugSection('GrabHTML', `Using first element out of ${html.length}`)
1795
1967
  }
1796
1968
 
1797
- return html[0];
1969
+ return html[0]
1798
1970
  }
1799
1971
 
1800
1972
  /**
@@ -1802,11 +1974,11 @@ class Puppeteer extends Helper {
1802
1974
  * {{ react }}
1803
1975
  */
1804
1976
  async grabCssPropertyFromAll(locator, cssProperty) {
1805
- const els = await this._locate(locator);
1806
- const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
1807
- const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
1977
+ const els = await this._locate(locator)
1978
+ const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))))))
1979
+ const cssValues = res.map(props => props[toCamelCase(cssProperty)])
1808
1980
 
1809
- return cssValues;
1981
+ return cssValues
1810
1982
  }
1811
1983
 
1812
1984
  /**
@@ -1814,14 +1986,14 @@ class Puppeteer extends Helper {
1814
1986
  * {{ react }}
1815
1987
  */
1816
1988
  async grabCssPropertyFrom(locator, cssProperty) {
1817
- const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty);
1818
- assertElementExists(cssValues, locator);
1989
+ const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
1990
+ assertElementExists(cssValues, locator)
1819
1991
 
1820
1992
  if (cssValues.length > 1) {
1821
- this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`);
1993
+ this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`)
1822
1994
  }
1823
1995
 
1824
- return cssValues[0];
1996
+ return cssValues[0]
1825
1997
  }
1826
1998
 
1827
1999
  /**
@@ -1829,35 +2001,34 @@ class Puppeteer extends Helper {
1829
2001
  * {{ react }}
1830
2002
  */
1831
2003
  async seeCssPropertiesOnElements(locator, cssProperties) {
1832
- const res = await this._locate(locator);
1833
- assertElementExists(res, locator);
2004
+ const res = await this._locate(locator)
2005
+ assertElementExists(res, locator)
1834
2006
 
1835
- const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
1836
- const elemAmount = res.length;
1837
- let props = [];
2007
+ const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties)
2008
+ const elemAmount = res.length
2009
+ let props = []
1838
2010
 
1839
2011
  for (const element of res) {
1840
2012
  for (const prop of Object.keys(cssProperties)) {
1841
- const cssProp = await this.grabCssPropertyFrom(locator, prop);
2013
+ const cssProp = await this.grabCssPropertyFrom(locator, prop)
1842
2014
  if (isColorProperty(prop)) {
1843
- props.push(convertColorToRGBA(cssProp));
2015
+ props.push(convertColorToRGBA(cssProp))
1844
2016
  } else {
1845
- props.push(cssProp);
2017
+ props.push(cssProp)
1846
2018
  }
1847
2019
  }
1848
2020
  }
1849
2021
 
1850
- const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
1851
- if (!Array.isArray(props)) props = [props];
1852
- let chunked = chunkArray(props, values.length);
1853
- chunked = chunked.filter((val) => {
2022
+ const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
2023
+ if (!Array.isArray(props)) props = [props]
2024
+ let chunked = chunkArray(props, values.length)
2025
+ chunked = chunked.filter(val => {
1854
2026
  for (let i = 0; i < val.length; ++i) {
1855
- // eslint-disable-next-line eqeqeq
1856
- if (val[i] != values[i]) return false;
2027
+ if (val[i] != values[i]) return false
1857
2028
  }
1858
- return true;
1859
- });
1860
- return equals(`all elements (${(new Locator(locator))}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount);
2029
+ return true
2030
+ })
2031
+ return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
1861
2032
  }
1862
2033
 
1863
2034
  /**
@@ -1865,34 +2036,37 @@ class Puppeteer extends Helper {
1865
2036
  * {{ react }}
1866
2037
  */
1867
2038
  async seeAttributesOnElements(locator, attributes) {
1868
- const elements = await this._locate(locator);
1869
- assertElementExists(elements, locator);
2039
+ const elements = await this._locate(locator)
2040
+ assertElementExists(elements, locator)
1870
2041
 
1871
- const expectedAttributes = Object.entries(attributes);
2042
+ const expectedAttributes = Object.entries(attributes)
1872
2043
 
1873
- const valuesPromises = elements.map(async (element) => {
1874
- const elementAttributes = {};
1875
- await Promise.all(expectedAttributes.map(async ([attribute, expectedValue]) => {
1876
- const actualValue = await element.evaluate((el, attr) => el[attr] || el.getAttribute(attr), attribute);
1877
- elementAttributes[attribute] = actualValue;
1878
- }));
1879
- return elementAttributes;
1880
- });
2044
+ const valuesPromises = elements.map(async element => {
2045
+ const elementAttributes = {}
2046
+ await Promise.all(
2047
+ expectedAttributes.map(async ([attribute, expectedValue]) => {
2048
+ const actualValue = await element.evaluate((el, attr) => el[attr] || el.getAttribute(attr), attribute)
2049
+ elementAttributes[attribute] = actualValue
2050
+ }),
2051
+ )
2052
+ return elementAttributes
2053
+ })
1881
2054
 
1882
- const actualAttributes = await Promise.all(valuesPromises);
2055
+ const actualAttributes = await Promise.all(valuesPromises)
1883
2056
 
1884
- const matchingElements = actualAttributes.filter((attrs) => expectedAttributes.every(([attribute, expectedValue]) => {
1885
- const actualValue = attrs[attribute];
1886
- if (!actualValue) return false;
1887
- if (actualValue.toString().match(new RegExp(expectedValue.toString()))) return true;
1888
- return expectedValue === actualValue;
1889
- }));
2057
+ const matchingElements = actualAttributes.filter(attrs =>
2058
+ expectedAttributes.every(([attribute, expectedValue]) => {
2059
+ const actualValue = attrs[attribute]
2060
+ if (!actualValue) return false
2061
+ if (actualValue.toString().match(new RegExp(expectedValue.toString()))) return true
2062
+ return expectedValue === actualValue
2063
+ }),
2064
+ )
1890
2065
 
1891
- const elementsCount = elements.length;
1892
- const matchingCount = matchingElements.length;
2066
+ const elementsCount = elements.length
2067
+ const matchingCount = matchingElements.length
1893
2068
 
1894
- return equals(`all elements (${(new Locator(locator))}) to have attributes ${JSON.stringify(attributes)}`)
1895
- .assert(matchingCount, elementsCount);
2069
+ return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(matchingCount, elementsCount)
1896
2070
  }
1897
2071
 
1898
2072
  /**
@@ -1900,21 +2074,21 @@ class Puppeteer extends Helper {
1900
2074
  * {{ react }}
1901
2075
  */
1902
2076
  async dragSlider(locator, offsetX = 0) {
1903
- const src = await this._locate(locator);
1904
- assertElementExists(src, locator, 'Slider Element');
2077
+ const src = await this._locate(locator)
2078
+ assertElementExists(src, locator, 'Slider Element')
1905
2079
 
1906
2080
  // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
1907
- const sliderSource = await getClickablePoint(src[0]);
2081
+ const sliderSource = await getClickablePoint(src[0])
1908
2082
 
1909
2083
  // Drag start point
1910
- await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
1911
- await this.page.mouse.down();
2084
+ await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 })
2085
+ await this.page.mouse.down()
1912
2086
 
1913
2087
  // Drag destination
1914
- await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 });
1915
- await this.page.mouse.up();
2088
+ await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 })
2089
+ await this.page.mouse.up()
1916
2090
 
1917
- await this._waitForAction();
2091
+ await this._waitForAction()
1918
2092
  }
1919
2093
 
1920
2094
  /**
@@ -1922,13 +2096,13 @@ class Puppeteer extends Helper {
1922
2096
  * {{ react }}
1923
2097
  */
1924
2098
  async grabAttributeFromAll(locator, attr) {
1925
- const els = await this._locate(locator);
1926
- const array = [];
2099
+ const els = await this._locate(locator)
2100
+ const array = []
1927
2101
  for (let index = 0; index < els.length; index++) {
1928
- const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr);
1929
- array.push(await a.jsonValue());
2102
+ const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
2103
+ array.push(a)
1930
2104
  }
1931
- return array;
2105
+ return array
1932
2106
  }
1933
2107
 
1934
2108
  /**
@@ -1936,84 +2110,90 @@ class Puppeteer extends Helper {
1936
2110
  * {{ react }}
1937
2111
  */
1938
2112
  async grabAttributeFrom(locator, attr) {
1939
- const attrs = await this.grabAttributeFromAll(locator, attr);
1940
- assertElementExists(attrs, locator);
2113
+ const attrs = await this.grabAttributeFromAll(locator, attr)
2114
+ assertElementExists(attrs, locator)
1941
2115
  if (attrs.length > 1) {
1942
- this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
2116
+ this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`)
1943
2117
  }
1944
2118
 
1945
- return attrs[0];
2119
+ return attrs[0]
1946
2120
  }
1947
2121
 
1948
2122
  /**
1949
2123
  * {{> saveElementScreenshot }}
1950
2124
  */
1951
2125
  async saveElementScreenshot(locator, fileName) {
1952
- const outputFile = screenshotOutputFolder(fileName);
2126
+ const outputFile = screenshotOutputFolder(fileName)
1953
2127
 
1954
- const res = await this._locate(locator);
1955
- assertElementExists(res, locator);
1956
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
1957
- const elem = res[0];
1958
- this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
1959
- return elem.screenshot({ path: outputFile, type: 'png' });
2128
+ const res = await this._locate(locator)
2129
+ assertElementExists(res, locator)
2130
+ if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`)
2131
+ const elem = res[0]
2132
+ this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
2133
+ return elem.screenshot({ path: outputFile, type: 'png' })
1960
2134
  }
1961
2135
 
1962
2136
  /**
1963
2137
  * {{> saveScreenshot }}
1964
2138
  */
1965
2139
  async saveScreenshot(fileName, fullPage) {
1966
- const fullPageOption = fullPage || this.options.fullPageScreenshots;
1967
- let outputFile = screenshotOutputFolder(fileName);
2140
+ const fullPageOption = fullPage || this.options.fullPageScreenshots
2141
+ let outputFile = screenshotOutputFolder(fileName)
2142
+
2143
+ this.debug(`Screenshot is saving to ${outputFile}`)
1968
2144
 
1969
- this.debug(`Screenshot is saving to ${outputFile}`);
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
+ }
1970
2150
 
1971
2151
  await this.page.screenshot({
1972
2152
  path: outputFile,
1973
2153
  fullPage: fullPageOption,
1974
2154
  type: 'png',
1975
- });
2155
+ })
1976
2156
 
1977
2157
  if (this.activeSessionName) {
1978
2158
  for (const sessionName in this.sessionPages) {
1979
- const activeSessionPage = this.sessionPages[sessionName];
1980
- outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2159
+ const activeSessionPage = this.sessionPages[sessionName]
2160
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
1981
2161
 
1982
- this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
2162
+ this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
1983
2163
 
1984
- if (activeSessionPage) {
2164
+ if (activeSessionPage && !activeSessionPage.isClosed?.()) {
1985
2165
  await activeSessionPage.screenshot({
1986
2166
  path: outputFile,
1987
2167
  fullPage: fullPageOption,
1988
2168
  type: 'png',
1989
- });
2169
+ })
1990
2170
  }
1991
2171
  }
1992
2172
  }
1993
2173
  }
1994
2174
 
1995
2175
  async _failed(test) {
1996
- await this._withinEnd();
2176
+ await this._withinEnd()
1997
2177
 
1998
2178
  if (this.options.trace) {
1999
- await this.page.tracing.stop();
2000
- const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.failed.json');
2001
- fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName);
2002
- test.artifacts.trace = _traceName;
2179
+ await this.page.tracing.stop()
2180
+ const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.failed.json')
2181
+ fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
2182
+ test.artifacts.trace = _traceName
2003
2183
  }
2004
2184
  }
2005
2185
 
2006
2186
  async _passed(test) {
2007
- await this._withinEnd();
2187
+ await this._withinEnd()
2008
2188
 
2009
2189
  if (this.options.trace) {
2010
- await this.page.tracing.stop();
2190
+ await this.page.tracing.stop()
2011
2191
  if (this.options.keepTraceForPassedTests) {
2012
- const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.passed.json');
2013
- fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName);
2014
- test.artifacts.trace = _traceName;
2192
+ const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.passed.json')
2193
+ fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
2194
+ test.artifacts.trace = _traceName
2015
2195
  } else {
2016
- fs.unlinkSync(this.currentRunningTest.artifacts.trace);
2196
+ fs.unlinkSync(this.currentRunningTest.artifacts.trace)
2017
2197
  }
2018
2198
  }
2019
2199
  }
@@ -2022,70 +2202,70 @@ class Puppeteer extends Helper {
2022
2202
  * {{> wait }}
2023
2203
  */
2024
2204
  async wait(sec) {
2025
- return new Promise(((done) => {
2026
- setTimeout(done, sec * 1000);
2027
- }));
2205
+ return new Promise(done => {
2206
+ setTimeout(done, sec * 1000)
2207
+ })
2028
2208
  }
2029
2209
 
2030
2210
  /**
2031
2211
  * {{> waitForEnabled }}
2032
2212
  */
2033
2213
  async waitForEnabled(locator, sec) {
2034
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2035
- locator = new Locator(locator, 'css');
2036
- await this.context;
2037
- let waiter;
2038
- const context = await this._getContext();
2214
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2215
+ locator = new Locator(locator, 'css')
2216
+ await this.context
2217
+ let waiter
2218
+ const context = await this._getContext()
2039
2219
  if (locator.isCSS()) {
2040
2220
  const enabledFn = function (locator) {
2041
- const els = document.querySelectorAll(locator);
2221
+ const els = document.querySelectorAll(locator)
2042
2222
  if (!els || els.length === 0) {
2043
- return false;
2223
+ return false
2044
2224
  }
2045
- return Array.prototype.filter.call(els, el => !el.disabled).length > 0;
2046
- };
2047
- waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value);
2225
+ return Array.prototype.filter.call(els, el => !el.disabled).length > 0
2226
+ }
2227
+ waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value)
2048
2228
  } else {
2049
2229
  const enabledFn = function (locator, $XPath) {
2050
- eval($XPath); // eslint-disable-line no-eval
2051
- return $XPath(null, locator).filter(el => !el.disabled).length > 0;
2052
- };
2053
- waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
2230
+ eval($XPath)
2231
+ return $XPath(null, locator).filter(el => !el.disabled).length > 0
2232
+ }
2233
+ waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
2054
2234
  }
2055
- return waiter.catch((err) => {
2056
- throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`);
2057
- });
2235
+ return waiter.catch(err => {
2236
+ throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
2237
+ })
2058
2238
  }
2059
2239
 
2060
2240
  /**
2061
2241
  * {{> waitForValue }}
2062
2242
  */
2063
2243
  async waitForValue(field, value, sec) {
2064
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2065
- const locator = new Locator(field, 'css');
2066
- await this.context;
2067
- let waiter;
2068
- const context = await this._getContext();
2244
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2245
+ const locator = new Locator(field, 'css')
2246
+ await this.context
2247
+ let waiter
2248
+ const context = await this._getContext()
2069
2249
  if (locator.isCSS()) {
2070
2250
  const valueFn = function (locator, value) {
2071
- const els = document.querySelectorAll(locator);
2251
+ const els = document.querySelectorAll(locator)
2072
2252
  if (!els || els.length === 0) {
2073
- return false;
2253
+ return false
2074
2254
  }
2075
- return Array.prototype.filter.call(els, el => (el.value.toString() || '').indexOf(value) !== -1).length > 0;
2076
- };
2077
- waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value);
2255
+ return Array.prototype.filter.call(els, el => (el.value.toString() || '').indexOf(value) !== -1).length > 0
2256
+ }
2257
+ waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value)
2078
2258
  } else {
2079
2259
  const valueFn = function (locator, $XPath, value) {
2080
- eval($XPath); // eslint-disable-line no-eval
2081
- return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
2082
- };
2083
- waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value);
2260
+ eval($XPath)
2261
+ return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
2262
+ }
2263
+ waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value)
2084
2264
  }
2085
- return waiter.catch((err) => {
2086
- const loc = locator.toString();
2087
- throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`);
2088
- });
2265
+ return waiter.catch(err => {
2266
+ const loc = locator.toString()
2267
+ throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`)
2268
+ })
2089
2269
  }
2090
2270
 
2091
2271
  /**
@@ -2093,45 +2273,47 @@ class Puppeteer extends Helper {
2093
2273
  * {{ react }}
2094
2274
  */
2095
2275
  async waitNumberOfVisibleElements(locator, num, sec) {
2096
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2097
- locator = new Locator(locator, 'css');
2098
- let waiter;
2099
- const context = await this._getContext();
2276
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2277
+ locator = new Locator(locator, 'css')
2278
+ let waiter
2279
+ const context = await this._getContext()
2100
2280
  if (locator.isCSS()) {
2101
2281
  const visibleFn = function (locator, num) {
2102
- const els = document.querySelectorAll(locator);
2282
+ const els = document.querySelectorAll(locator)
2103
2283
  if (!els || els.length === 0) {
2104
- return false;
2284
+ return false
2105
2285
  }
2106
- return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num;
2107
- };
2108
- waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num);
2286
+ return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
2287
+ }
2288
+ waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num)
2109
2289
  } else {
2110
2290
  const visibleFn = function (locator, $XPath, num) {
2111
- eval($XPath); // eslint-disable-line no-eval
2112
- return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num;
2113
- };
2114
- waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num);
2291
+ eval($XPath)
2292
+ return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
2293
+ }
2294
+ waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num)
2115
2295
  }
2116
- return waiter.catch((err) => {
2117
- throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`);
2118
- });
2296
+ return waiter.catch(err => {
2297
+ throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
2298
+ })
2119
2299
  }
2120
2300
 
2121
2301
  /**
2122
2302
  * {{> waitForClickable }}
2123
2303
  */
2124
2304
  async waitForClickable(locator, waitTimeout) {
2125
- const els = await this._locate(locator);
2126
- assertElementExists(els, locator);
2305
+ const el = await this._locateElement(locator)
2306
+ if (!el) {
2307
+ throw new ElementNotFound(locator, 'Element to wait for clickable')
2308
+ }
2127
2309
 
2128
- return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async (e) => {
2310
+ return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
2129
2311
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2130
- throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`);
2312
+ throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
2131
2313
  } else {
2132
- throw e;
2314
+ throw e
2133
2315
  }
2134
- });
2316
+ })
2135
2317
  }
2136
2318
 
2137
2319
  /**
@@ -2139,19 +2321,19 @@ class Puppeteer extends Helper {
2139
2321
  * {{ react }}
2140
2322
  */
2141
2323
  async waitForElement(locator, sec) {
2142
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2143
- locator = new Locator(locator, 'css');
2324
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2325
+ locator = new Locator(locator, 'css')
2144
2326
 
2145
- let waiter;
2146
- const context = await this._getContext();
2327
+ let waiter
2328
+ const context = await this._getContext()
2147
2329
  if (locator.isCSS()) {
2148
- waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout });
2330
+ waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout })
2149
2331
  } else {
2150
- waiter = _waitForElement.call(this, locator, { timeout: waitTimeout });
2332
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout })
2151
2333
  }
2152
- return waiter.catch((err) => {
2153
- throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
2154
- });
2334
+ return waiter.catch(err => {
2335
+ throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`)
2336
+ })
2155
2337
  }
2156
2338
 
2157
2339
  /**
@@ -2160,160 +2342,183 @@ class Puppeteer extends Helper {
2160
2342
  * {{ react }}
2161
2343
  */
2162
2344
  async waitForVisible(locator, sec) {
2163
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2164
- locator = new Locator(locator, 'css');
2165
- await this.context;
2166
- let waiter;
2167
- const context = await this._getContext();
2345
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2346
+ locator = new Locator(locator, 'css')
2347
+ await this.context
2348
+ let waiter
2349
+ const context = await this._getContext()
2168
2350
  if (locator.isCSS()) {
2169
- waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true });
2351
+ waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true })
2170
2352
  } else {
2171
- waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true });
2353
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true })
2172
2354
  }
2173
- return waiter.catch((err) => {
2174
- throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
2175
- });
2355
+ return waiter.catch(err => {
2356
+ throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`)
2357
+ })
2176
2358
  }
2177
2359
 
2178
2360
  /**
2179
2361
  * {{> waitForInvisible }}
2180
2362
  */
2181
2363
  async waitForInvisible(locator, sec) {
2182
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2183
- locator = new Locator(locator, 'css');
2184
- await this.context;
2185
- let waiter;
2186
- const context = await this._getContext();
2364
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2365
+ locator = new Locator(locator, 'css')
2366
+ await this.context
2367
+ let waiter
2368
+ const context = await this._getContext()
2187
2369
  if (locator.isCSS()) {
2188
- waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
2370
+ waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
2189
2371
  } else {
2190
- waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
2372
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
2191
2373
  }
2192
- return waiter.catch((err) => {
2193
- throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
2194
- });
2374
+ return waiter.catch(err => {
2375
+ throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`)
2376
+ })
2195
2377
  }
2196
2378
 
2197
2379
  /**
2198
2380
  * {{> waitToHide }}
2199
2381
  */
2200
2382
  async waitToHide(locator, sec) {
2201
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2202
- locator = new Locator(locator, 'css');
2203
- let waiter;
2204
- const context = await this._getContext();
2383
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2384
+ locator = new Locator(locator, 'css')
2385
+ let waiter
2386
+ const context = await this._getContext()
2205
2387
  if (locator.isCSS()) {
2206
- waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
2388
+ waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
2207
2389
  } else {
2208
- waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
2390
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
2209
2391
  }
2210
- return waiter.catch((err) => {
2211
- throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
2212
- });
2392
+ return waiter.catch(err => {
2393
+ throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
2394
+ })
2213
2395
  }
2214
2396
 
2215
2397
  /**
2216
2398
  * {{> waitForNumberOfTabs }}
2217
2399
  */
2218
2400
  async waitForNumberOfTabs(expectedTabs, sec) {
2219
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2220
- let currentTabs;
2221
- let count = 0;
2401
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2402
+ let currentTabs
2403
+ let count = 0
2222
2404
 
2223
2405
  do {
2224
- currentTabs = await this.grabNumberOfOpenTabs();
2225
- await this.wait(1);
2226
- count += 1000;
2227
- if (currentTabs >= expectedTabs) return;
2228
- } while (count <= waitTimeout);
2406
+ currentTabs = await this.grabNumberOfOpenTabs()
2407
+ await this.wait(1)
2408
+ count += 1000
2409
+ if (currentTabs >= expectedTabs) return
2410
+ } while (count <= waitTimeout)
2229
2411
 
2230
- throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`);
2412
+ throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`)
2231
2413
  }
2232
2414
 
2233
2415
  async _getContext() {
2234
2416
  if (this.context && this.context.constructor.name === 'CdpFrame') {
2235
- return this.context;
2417
+ return this.context
2236
2418
  }
2237
- return this.page;
2419
+ return this.page
2238
2420
  }
2239
2421
 
2240
2422
  /**
2241
2423
  * {{> waitInUrl }}
2242
2424
  */
2243
2425
  async waitInUrl(urlPart, sec = null) {
2244
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2245
-
2246
- return this.page.waitForFunction((urlPart) => {
2247
- const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
2248
- return currUrl.indexOf(urlPart) > -1;
2249
- }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
2250
- const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
2251
- if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2252
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
2253
- } else {
2254
- throw e;
2255
- }
2256
- });
2426
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2427
+
2428
+ return this.page
2429
+ .waitForFunction(
2430
+ urlPart => {
2431
+ const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2432
+ return currUrl.indexOf(urlPart) > -1
2433
+ },
2434
+ { timeout: waitTimeout },
2435
+ urlPart,
2436
+ )
2437
+ .catch(async e => {
2438
+ const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2439
+ if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2440
+ throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2441
+ } else {
2442
+ throw e
2443
+ }
2444
+ })
2257
2445
  }
2258
2446
 
2259
2447
  /**
2260
2448
  * {{> waitUrlEquals }}
2261
2449
  */
2262
2450
  async waitUrlEquals(urlPart, sec = null) {
2263
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2451
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2264
2452
 
2265
- const baseUrl = this.options.url;
2453
+ const baseUrl = this.options.url
2266
2454
  if (urlPart.indexOf('http') < 0) {
2267
- urlPart = baseUrl + urlPart;
2268
- }
2269
-
2270
- return this.page.waitForFunction((urlPart) => {
2271
- const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
2272
- return currUrl.indexOf(urlPart) > -1;
2273
- }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
2274
- const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
2275
- if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2276
- throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
2277
- } else {
2278
- throw e;
2279
- }
2280
- });
2455
+ urlPart = baseUrl + urlPart
2456
+ }
2457
+
2458
+ return this.page
2459
+ .waitForFunction(
2460
+ urlPart => {
2461
+ const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
2462
+ return currUrl.indexOf(urlPart) > -1
2463
+ },
2464
+ { timeout: waitTimeout },
2465
+ urlPart,
2466
+ )
2467
+ .catch(async e => {
2468
+ const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2469
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2470
+ throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
2471
+ } else {
2472
+ throw e
2473
+ }
2474
+ })
2281
2475
  }
2282
2476
 
2283
2477
  /**
2284
2478
  * {{> waitForText }}
2285
2479
  */
2286
2480
  async waitForText(text, sec = null, context = null) {
2287
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2288
- let waiter;
2481
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2482
+ let waiter
2289
2483
 
2290
- const contextObject = await this._getContext();
2484
+ const contextObject = await this._getContext()
2291
2485
 
2292
2486
  if (context) {
2293
- const locator = new Locator(context, 'css');
2487
+ const locator = new Locator(context, 'css')
2294
2488
  if (locator.isCSS()) {
2295
- waiter = contextObject.waitForFunction((locator, text) => {
2296
- const el = document.querySelector(locator);
2297
- if (!el) return false;
2298
- return el.innerText.indexOf(text) > -1;
2299
- }, { timeout: waitTimeout }, locator.value, text);
2489
+ waiter = contextObject.waitForFunction(
2490
+ (locator, text) => {
2491
+ const el = document.querySelector(locator)
2492
+ if (!el) return false
2493
+ return el.innerText.indexOf(text) > -1
2494
+ },
2495
+ { timeout: waitTimeout },
2496
+ locator.value,
2497
+ text,
2498
+ )
2300
2499
  }
2301
2500
 
2302
2501
  if (locator.isXPath()) {
2303
- waiter = contextObject.waitForFunction((locator, text, $XPath) => {
2304
- eval($XPath); // eslint-disable-line no-eval
2305
- const el = $XPath(null, locator);
2306
- if (!el.length) return false;
2307
- return el[0].innerText.indexOf(text) > -1;
2308
- }, { timeout: waitTimeout }, locator.value, text, $XPath.toString());
2502
+ waiter = contextObject.waitForFunction(
2503
+ (locator, text, $XPath) => {
2504
+ eval($XPath)
2505
+ const el = $XPath(null, locator)
2506
+ if (!el.length) return false
2507
+ return el[0].innerText.indexOf(text) > -1
2508
+ },
2509
+ { timeout: waitTimeout },
2510
+ locator.value,
2511
+ text,
2512
+ $XPath.toString(),
2513
+ )
2309
2514
  }
2310
2515
  } else {
2311
- waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text);
2516
+ waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text)
2312
2517
  }
2313
2518
 
2314
- return waiter.catch((err) => {
2315
- throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
2316
- });
2519
+ return waiter.catch(err => {
2520
+ throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`)
2521
+ })
2317
2522
  }
2318
2523
 
2319
2524
  /**
@@ -2328,8 +2533,8 @@ class Puppeteer extends Helper {
2328
2533
  * @param {?number} [sec=null] seconds to wait
2329
2534
  */
2330
2535
  async waitForRequest(urlOrPredicate, sec = null) {
2331
- const timeout = sec ? sec * 1000 : this.options.waitForTimeout;
2332
- return this.page.waitForRequest(urlOrPredicate, { timeout });
2536
+ const timeout = sec ? sec * 1000 : this.options.waitForTimeout
2537
+ return this.page.waitForRequest(urlOrPredicate, { timeout })
2333
2538
  }
2334
2539
 
2335
2540
  /**
@@ -2344,8 +2549,8 @@ class Puppeteer extends Helper {
2344
2549
  * @param {?number} [sec=null] number of seconds to wait
2345
2550
  */
2346
2551
  async waitForResponse(urlOrPredicate, sec = null) {
2347
- const timeout = sec ? sec * 1000 : this.options.waitForTimeout;
2348
- return this.page.waitForResponse(urlOrPredicate, { timeout });
2552
+ const timeout = sec ? sec * 1000 : this.options.waitForTimeout
2553
+ return this.page.waitForResponse(urlOrPredicate, { timeout })
2349
2554
  }
2350
2555
 
2351
2556
  /**
@@ -2354,37 +2559,37 @@ class Puppeteer extends Helper {
2354
2559
  async switchTo(locator) {
2355
2560
  if (Number.isInteger(locator)) {
2356
2561
  // Select by frame index of current context
2357
- let frames = [];
2562
+ let frames = []
2358
2563
  if (this.context && typeof this.context.childFrames === 'function') {
2359
- frames = await this.context.childFrames();
2564
+ frames = await this.context.childFrames()
2360
2565
  } else {
2361
- frames = await this.page.mainFrame().childFrames();
2566
+ frames = await this.page.mainFrame().childFrames()
2362
2567
  }
2363
2568
 
2364
2569
  if (locator >= 0 && locator < frames.length) {
2365
- this.context = frames[locator];
2570
+ this.context = frames[locator]
2366
2571
  } else {
2367
- throw new Error('Frame index out of bounds');
2572
+ throw new Error('Frame index out of bounds')
2368
2573
  }
2369
- return;
2574
+ return
2370
2575
  }
2371
2576
 
2372
2577
  if (!locator) {
2373
- this.context = await this.page.mainFrame();
2374
- return;
2578
+ this.context = await this.page.mainFrame()
2579
+ return
2375
2580
  }
2376
2581
 
2377
2582
  // Select iframe by selector
2378
- const els = await this._locate(locator);
2379
- assertElementExists(els, locator);
2583
+ const els = await this._locate(locator)
2584
+ assertElementExists(els, locator)
2380
2585
 
2381
- const iframeElement = els[0];
2382
- const contentFrame = await iframeElement.contentFrame();
2586
+ const iframeElement = els[0]
2587
+ const contentFrame = await iframeElement.contentFrame()
2383
2588
 
2384
2589
  if (contentFrame) {
2385
- this.context = contentFrame;
2590
+ this.context = contentFrame
2386
2591
  } else {
2387
- throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath');
2592
+ throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath')
2388
2593
  }
2389
2594
  }
2390
2595
 
@@ -2392,23 +2597,23 @@ class Puppeteer extends Helper {
2392
2597
  * {{> waitForFunction }}
2393
2598
  */
2394
2599
  async waitForFunction(fn, argsOrSec = null, sec = null) {
2395
- let args = [];
2600
+ let args = []
2396
2601
  if (argsOrSec) {
2397
2602
  if (Array.isArray(argsOrSec)) {
2398
- args = argsOrSec;
2603
+ args = argsOrSec
2399
2604
  } else if (typeof argsOrSec === 'number') {
2400
- sec = argsOrSec;
2605
+ sec = argsOrSec
2401
2606
  }
2402
2607
  }
2403
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2404
- const context = await this._getContext();
2405
- return context.waitForFunction(fn, { timeout: waitTimeout }, ...args);
2608
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2609
+ const context = await this._getContext()
2610
+ return context.waitForFunction(fn, { timeout: waitTimeout }, ...args)
2406
2611
  }
2407
2612
 
2408
2613
  /**
2409
2614
  * Waits for navigation to finish. By default, takes configured `waitForNavigation` option.
2410
2615
  *
2411
- * 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)
2412
2617
  *
2413
2618
  * @param {*} opts
2414
2619
  */
@@ -2417,87 +2622,87 @@ class Puppeteer extends Helper {
2417
2622
  timeout: this.options.getPageTimeout,
2418
2623
  waitUntil: this.options.waitForNavigation,
2419
2624
  ...opts,
2420
- };
2421
- return this.page.waitForNavigation(opts);
2625
+ }
2626
+ return this.page.waitForNavigation(opts)
2422
2627
  }
2423
2628
 
2424
2629
  async waitUntilExists(locator, sec) {
2425
2630
  console.log(`waitUntilExists deprecated:
2426
2631
  * use 'waitForElement' to wait for element to be attached
2427
- * use 'waitForDetached to wait for element to be removed'`);
2428
- return this.waitForDetached(locator, sec);
2632
+ * use 'waitForDetached to wait for element to be removed'`)
2633
+ return this.waitForDetached(locator, sec)
2429
2634
  }
2430
2635
 
2431
2636
  /**
2432
2637
  * {{> waitForDetached }}
2433
2638
  */
2434
2639
  async waitForDetached(locator, sec) {
2435
- const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
2436
- locator = new Locator(locator, 'css');
2640
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2641
+ locator = new Locator(locator, 'css')
2437
2642
 
2438
- let waiter;
2439
- const context = await this._getContext();
2643
+ let waiter
2644
+ const context = await this._getContext()
2440
2645
  if (locator.isCSS()) {
2441
2646
  const visibleFn = function (locator) {
2442
- return document.querySelector(locator) === null;
2443
- };
2444
- waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value);
2647
+ return document.querySelector(locator) === null
2648
+ }
2649
+ waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value)
2445
2650
  } else {
2446
2651
  const visibleFn = function (locator, $XPath) {
2447
- eval($XPath); // eslint-disable-line no-eval
2448
- return $XPath(null, locator).length === 0;
2449
- };
2450
- waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
2652
+ eval($XPath)
2653
+ return $XPath(null, locator).length === 0
2654
+ }
2655
+ waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
2451
2656
  }
2452
- return waiter.catch((err) => {
2453
- throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
2454
- });
2657
+ return waiter.catch(err => {
2658
+ throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
2659
+ })
2455
2660
  }
2456
2661
 
2457
2662
  async _waitForAction() {
2458
- return this.wait(this.options.waitForAction / 1000);
2663
+ return this.wait(this.options.waitForAction / 1000)
2459
2664
  }
2460
2665
 
2461
2666
  /**
2462
2667
  * {{> grabDataFromPerformanceTiming }}
2463
2668
  */
2464
2669
  async grabDataFromPerformanceTiming() {
2465
- return perfTiming;
2670
+ return perfTiming
2466
2671
  }
2467
2672
 
2468
2673
  /**
2469
2674
  * {{> grabElementBoundingRect }}
2470
2675
  */
2471
2676
  async grabElementBoundingRect(locator, prop) {
2472
- const els = await this._locate(locator);
2473
- assertElementExists(els, locator);
2474
- const rect = await els[0].boundingBox();
2475
- if (prop) return rect[prop];
2476
- return rect;
2677
+ const els = await this._locate(locator)
2678
+ assertElementExists(els, locator)
2679
+ const rect = await els[0].boundingBox()
2680
+ if (prop) return rect[prop]
2681
+ return rect
2477
2682
  }
2478
2683
 
2479
2684
  /**
2480
- * Mocks network request using [`Request Interception`](https://pptr.dev/next/guides/request-interception)
2685
+ * Mocks network request using [`Request Interception`](https://pptr.dev/guides/network-interception)
2481
2686
  *
2482
2687
  * ```js
2483
2688
  * I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
2484
2689
  * ```
2485
- * This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/next/guides/request-interception)
2690
+ * This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/guides/network-interception)
2486
2691
  *
2487
2692
  * @param {string|RegExp} [url] URL, regex or pattern for to match URL
2488
2693
  * @param {function} [handler] a function to process request
2489
2694
  */
2490
2695
  async mockRoute(url, handler) {
2491
- await this.page.setRequestInterception(true);
2696
+ await this.page.setRequestInterception(true)
2492
2697
 
2493
2698
  this.page.on('request', interceptedRequest => {
2494
2699
  if (interceptedRequest.url().match(url)) {
2495
2700
  // @ts-ignore
2496
- handler(interceptedRequest);
2701
+ handler(interceptedRequest)
2497
2702
  } else {
2498
- interceptedRequest.continue();
2703
+ interceptedRequest.continue()
2499
2704
  }
2500
- });
2705
+ })
2501
2706
  }
2502
2707
 
2503
2708
  /**
@@ -2510,18 +2715,18 @@ class Puppeteer extends Helper {
2510
2715
  * @param {string|RegExp} [url] URL, regex or pattern for to match URL
2511
2716
  */
2512
2717
  async stopMockingRoute(url) {
2513
- await this.page.setRequestInterception(true);
2718
+ await this.page.setRequestInterception(true)
2514
2719
 
2515
- this.page.off('request');
2720
+ this.page.off('request')
2516
2721
 
2517
2722
  // Resume normal request handling for the given URL
2518
2723
  this.page.on('request', interceptedRequest => {
2519
2724
  if (interceptedRequest.url().includes(url)) {
2520
- interceptedRequest.continue();
2725
+ interceptedRequest.continue()
2521
2726
  } else {
2522
- interceptedRequest.continue();
2727
+ interceptedRequest.continue()
2523
2728
  }
2524
- });
2729
+ })
2525
2730
  }
2526
2731
 
2527
2732
  /**
@@ -2529,15 +2734,16 @@ class Puppeteer extends Helper {
2529
2734
  * {{> flushNetworkTraffics }}
2530
2735
  */
2531
2736
  flushNetworkTraffics() {
2532
- flushNetworkTraffics.call(this);
2737
+ flushNetworkTraffics.call(this)
2533
2738
  }
2534
2739
 
2535
2740
  /**
2536
2741
  *
2537
2742
  * {{> stopRecordingTraffic }}
2538
2743
  */
2539
- stopRecordingTraffic() {
2540
- stopRecordingTraffic.call(this);
2744
+ async stopRecordingTraffic() {
2745
+ await this.page.setRequestInterception(false)
2746
+ stopRecordingTraffic.call(this)
2541
2747
  }
2542
2748
 
2543
2749
  /**
@@ -2545,29 +2751,29 @@ class Puppeteer extends Helper {
2545
2751
  *
2546
2752
  */
2547
2753
  async startRecordingTraffic() {
2548
- this.flushNetworkTraffics();
2549
- this.recording = true;
2550
- this.recordedAtLeastOnce = true;
2754
+ this.flushNetworkTraffics()
2755
+ this.recording = true
2756
+ this.recordedAtLeastOnce = true
2551
2757
 
2552
- await this.page.setRequestInterception(true);
2758
+ await this.page.setRequestInterception(true)
2553
2759
 
2554
- this.page.on('request', (request) => {
2760
+ this.page.on('request', request => {
2555
2761
  const information = {
2556
2762
  url: request.url(),
2557
2763
  method: request.method(),
2558
2764
  requestHeaders: request.headers(),
2559
2765
  requestPostData: request.postData(),
2560
2766
  response: request.response(),
2561
- };
2767
+ }
2562
2768
 
2563
- this.debugSection('REQUEST: ', JSON.stringify(information));
2769
+ this.debugSection('REQUEST: ', JSON.stringify(information))
2564
2770
 
2565
2771
  if (typeof information.requestPostData === 'object') {
2566
- information.requestPostData = JSON.parse(information.requestPostData);
2772
+ information.requestPostData = JSON.parse(information.requestPostData)
2567
2773
  }
2568
- request.continue();
2569
- this.requests.push(information);
2570
- });
2774
+ request.continue()
2775
+ this.requests.push(information)
2776
+ })
2571
2777
  }
2572
2778
 
2573
2779
  /**
@@ -2575,17 +2781,15 @@ class Puppeteer extends Helper {
2575
2781
  * {{> grabRecordedNetworkTraffics }}
2576
2782
  */
2577
2783
  async grabRecordedNetworkTraffics() {
2578
- return grabRecordedNetworkTraffics.call(this);
2784
+ return grabRecordedNetworkTraffics.call(this)
2579
2785
  }
2580
2786
 
2581
2787
  /**
2582
2788
  *
2583
2789
  * {{> seeTraffic }}
2584
2790
  */
2585
- async seeTraffic({
2586
- name, url, parameters, requestPostData, timeout = 10,
2587
- }) {
2588
- await seeTraffic.call(this, ...arguments);
2791
+ async seeTraffic({ name, url, parameters, requestPostData, timeout = 10 }) {
2792
+ await seeTraffic.call(this, ...arguments)
2589
2793
  }
2590
2794
 
2591
2795
  /**
@@ -2594,56 +2798,47 @@ class Puppeteer extends Helper {
2594
2798
  *
2595
2799
  */
2596
2800
  dontSeeTraffic({ name, url }) {
2597
- dontSeeTraffic.call(this, ...arguments);
2801
+ dontSeeTraffic.call(this, ...arguments)
2598
2802
  }
2599
2803
 
2600
2804
  async getNewCDPSession() {
2601
- const client = await this.page.target().createCDPSession();
2602
- return client;
2805
+ const client = await this.page.target().createCDPSession()
2806
+ return client
2603
2807
  }
2604
2808
 
2605
2809
  /**
2606
2810
  * {{> startRecordingWebSocketMessages }}
2607
2811
  */
2608
2812
  async startRecordingWebSocketMessages() {
2609
- this.flushWebSocketMessages();
2610
- this.recordingWebSocketMessages = true;
2611
- this.recordedWebSocketMessagesAtLeastOnce = true;
2612
-
2613
- this.cdpSession = await this.getNewCDPSession();
2614
- await this.cdpSession.send('Network.enable');
2615
- await this.cdpSession.send('Page.enable');
2616
-
2617
- this.cdpSession.on(
2618
- 'Network.webSocketFrameReceived',
2619
- (payload) => {
2620
- this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
2621
- },
2622
- );
2813
+ this.flushWebSocketMessages()
2814
+ this.recordingWebSocketMessages = true
2815
+ this.recordedWebSocketMessagesAtLeastOnce = true
2623
2816
 
2624
- this.cdpSession.on(
2625
- 'Network.webSocketFrameSent',
2626
- (payload) => {
2627
- this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
2628
- },
2629
- );
2817
+ this.cdpSession = await this.getNewCDPSession()
2818
+ await this.cdpSession.send('Network.enable')
2819
+ await this.cdpSession.send('Page.enable')
2630
2820
 
2631
- this.cdpSession.on(
2632
- 'Network.webSocketFrameError',
2633
- (payload) => {
2634
- this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
2635
- },
2636
- );
2821
+ this.cdpSession.on('Network.webSocketFrameReceived', payload => {
2822
+ this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
2823
+ })
2824
+
2825
+ this.cdpSession.on('Network.webSocketFrameSent', payload => {
2826
+ this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
2827
+ })
2828
+
2829
+ this.cdpSession.on('Network.webSocketFrameError', payload => {
2830
+ this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
2831
+ })
2637
2832
  }
2638
2833
 
2639
2834
  /**
2640
2835
  * {{> stopRecordingWebSocketMessages }}
2641
2836
  */
2642
2837
  async stopRecordingWebSocketMessages() {
2643
- await this.cdpSession.send('Network.disable');
2644
- await this.cdpSession.send('Page.disable');
2645
- this.page.removeAllListeners('Network');
2646
- this.recordingWebSocketMessages = false;
2838
+ await this.cdpSession.send('Network.disable')
2839
+ await this.cdpSession.send('Page.disable')
2840
+ this.page.removeAllListeners('Network')
2841
+ this.recordingWebSocketMessages = false
2647
2842
  }
2648
2843
 
2649
2844
  /**
@@ -2655,394 +2850,537 @@ class Puppeteer extends Helper {
2655
2850
  grabWebSocketMessages() {
2656
2851
  if (!this.recordingWebSocketMessages) {
2657
2852
  if (!this.recordedWebSocketMessagesAtLeastOnce) {
2658
- throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
2853
+ throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
2659
2854
  }
2660
2855
  }
2661
- return this.webSocketMessages;
2856
+ return this.webSocketMessages
2662
2857
  }
2663
2858
 
2664
2859
  /**
2665
2860
  * Resets all recorded WS messages.
2666
2861
  */
2667
2862
  flushWebSocketMessages() {
2668
- this.webSocketMessages = [];
2863
+ this.webSocketMessages = []
2669
2864
  }
2670
2865
 
2671
2866
  _getWebSocketMessage(payload) {
2672
2867
  if (payload.errorMessage) {
2673
- return payload.errorMessage;
2868
+ return payload.errorMessage
2674
2869
  }
2675
2870
 
2676
- return payload.response.payloadData;
2871
+ return payload.response.payloadData
2677
2872
  }
2678
2873
 
2679
2874
  _getWebSocketLog(prefix, payload) {
2680
- return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
2875
+ return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`
2681
2876
  }
2682
2877
 
2683
2878
  _logWebsocketMessages(message) {
2684
- this.webSocketMessages += message;
2879
+ this.webSocketMessages.push(message)
2685
2880
  }
2686
2881
  }
2687
2882
 
2688
- export default Puppeteer;
2883
+ export default Puppeteer
2689
2884
 
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
+ */
2690
2892
  async function findElements(matcher, locator) {
2691
- if (locator.react) return findReactElements.call(this, locator);
2692
- locator = new Locator(locator, 'css');
2693
- if (!locator.isXPath()) return matcher.$$(locator.simplify());
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
+
2694
2905
  // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2695
2906
  if (puppeteer.default?.defaultBrowserRevision) {
2696
- return matcher.$$(`xpath/${locator.value}`);
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}`)
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}`)
2697
2942
  }
2698
- return matcher.$x(locator.value);
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]
2699
2975
  }
2700
2976
 
2701
2977
  async function proceedClick(locator, context = null, options = {}) {
2702
- let matcher = await this.context;
2978
+ let matcher = await this.context
2703
2979
  if (context) {
2704
- const els = await this._locate(context);
2705
- assertElementExists(els, context);
2706
- matcher = els[0];
2980
+ const els = await this._locate(context)
2981
+ assertElementExists(els, context)
2982
+ matcher = els[0]
2707
2983
  }
2708
- const els = await findClickable.call(this, matcher, locator);
2984
+ const els = await findClickable.call(this, matcher, locator)
2709
2985
  if (context) {
2710
- assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`);
2986
+ assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
2711
2987
  } else {
2712
- assertElementExists(els, locator, 'Clickable element');
2988
+ assertElementExists(els, locator, 'Clickable element')
2713
2989
  }
2714
2990
 
2715
- highlightActiveElement.call(this, els[0], await this._getContext());
2991
+ highlightActiveElement.call(this, els[0], await this._getContext())
2716
2992
 
2717
- await els[0].click(options);
2718
- const promises = [];
2993
+ await els[0].click(options)
2994
+ const promises = []
2719
2995
  if (options.waitForNavigation) {
2720
- promises.push(this.waitForNavigation());
2996
+ promises.push(this.waitForNavigation())
2721
2997
  }
2722
- promises.push(this._waitForAction());
2998
+ promises.push(this._waitForAction())
2723
2999
 
2724
- return Promise.all(promises);
3000
+ return Promise.all(promises)
2725
3001
  }
2726
3002
 
2727
3003
  async function findClickable(matcher, locator) {
2728
- if (locator.react) return findReactElements.call(this, locator);
2729
- locator = new Locator(locator);
2730
- 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)
2731
3007
 
2732
- let els;
2733
- const literal = xpathLocator.literal(locator.value);
3008
+ let els
3009
+ const literal = xpathLocator.literal(matchedLocator.value)
2734
3010
 
2735
- els = await findElements.call(this, matcher, Locator.clickable.narrow(literal));
2736
- if (els.length) return els;
3011
+ els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
3012
+ if (els.length) return els
2737
3013
 
2738
- els = await findElements.call(this, matcher, Locator.clickable.wide(literal));
2739
- if (els.length) return els;
3014
+ els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
3015
+ if (els.length) return els
2740
3016
 
2741
3017
  try {
2742
- els = await findElements.call(this, matcher, Locator.clickable.self(literal));
2743
- if (els.length) return els;
3018
+ els = await findElements.call(this, matcher, Locator.clickable.self(literal))
3019
+ if (els.length) return els
2744
3020
  } catch (err) {
2745
3021
  // Do nothing
2746
3022
  }
2747
3023
 
2748
- 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
2749
3033
  }
2750
3034
 
2751
3035
  async function proceedSee(assertType, text, context, strict = false) {
2752
- let description;
2753
- let allText;
3036
+ let description
3037
+ let allText
2754
3038
  if (!context) {
2755
- let el = await this.context;
3039
+ let el = await this.context
2756
3040
 
2757
3041
  if (el && !el.getProperty) {
2758
3042
  // Fallback to body
2759
- el = await this.context.$('body');
3043
+ el = await this.context.$('body')
2760
3044
  }
2761
3045
 
2762
- allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
2763
- description = 'web application';
3046
+ allText = [await el.getProperty('innerText').then(p => p.jsonValue())]
3047
+ description = 'web application'
2764
3048
  } else {
2765
- const locator = new Locator(context, 'css');
2766
- description = `element ${locator.toString()}`;
2767
- const els = await this._locate(locator);
2768
- assertElementExists(els, locator.toString());
2769
- allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
3049
+ const locator = new Locator(context, 'css')
3050
+ description = `element ${locator.toString()}`
3051
+ const els = await this._locate(locator)
3052
+ assertElementExists(els, locator.toString())
3053
+ allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
3054
+ }
3055
+
3056
+ if (store?.currentStep?.opts?.ignoreCase === true) {
3057
+ text = text.toLowerCase()
3058
+ allText = allText.map(elText => elText.toLowerCase())
2770
3059
  }
2771
3060
 
2772
3061
  if (strict) {
2773
- return allText.map(elText => equals(description)[assertType](text, elText));
3062
+ return allText.map(elText => equals(description)[assertType](text, elText))
2774
3063
  }
2775
- return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
3064
+ return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
2776
3065
  }
2777
3066
 
2778
3067
  async function findCheckable(locator, context) {
2779
- let contextEl = await this.context;
3068
+ let contextEl = await this.context
2780
3069
  if (typeof context === 'string') {
2781
- contextEl = await findElements.call(this, contextEl, (new Locator(context, 'css')).simplify());
2782
- contextEl = contextEl[0];
3070
+ contextEl = await findElements.call(this, contextEl, new Locator(context, 'css').simplify())
3071
+ contextEl = contextEl[0]
2783
3072
  }
2784
3073
 
2785
- const matchedLocator = new Locator(locator);
3074
+ const matchedLocator = new Locator(locator)
2786
3075
  if (!matchedLocator.isFuzzy()) {
2787
- return findElements.call(this, contextEl, matchedLocator.simplify());
3076
+ return findElements.call(this, contextEl, matchedLocator)
2788
3077
  }
2789
3078
 
2790
- const literal = xpathLocator.literal(locator);
2791
- let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal));
3079
+ const literal = xpathLocator.literal(matchedLocator.value)
3080
+ let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
2792
3081
  if (els.length) {
2793
- return els;
3082
+ return els
2794
3083
  }
2795
- els = await findElements.call(this, contextEl, Locator.checkable.byName(literal));
3084
+ els = await findElements.call(this, contextEl, Locator.checkable.byName(literal))
2796
3085
  if (els.length) {
2797
- return els;
3086
+ return els
3087
+ }
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
2798
3095
  }
2799
- return findElements.call(this, contextEl, locator);
3096
+
3097
+ return findElements.call(this, contextEl, matchedLocator.value)
2800
3098
  }
2801
3099
 
2802
3100
  async function proceedIsChecked(assertType, option) {
2803
- let els = await findCheckable.call(this, option);
2804
- assertElementExists(els, option, 'Checkable');
2805
- els = await Promise.all(els.map(el => el.getProperty('checked')));
2806
- els = await Promise.all(els.map(el => el.jsonValue()));
2807
- const selected = els.reduce((prev, cur) => prev || cur);
2808
- return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
3101
+ let els = await findCheckable.call(this, option)
3102
+ assertElementExists(els, option, 'Checkable')
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)
3121
+ return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
2809
3122
  }
2810
3123
 
2811
3124
  async function findVisibleFields(locator) {
2812
- const els = await findFields.call(this, locator);
2813
- const visible = await Promise.all(els.map(el => el.boundingBox()));
2814
- return els.filter((el, index) => visible[index]);
3125
+ const els = await findFields.call(this, locator)
3126
+ const visible = await Promise.all(els.map(el => el.boundingBox()))
3127
+ return els.filter((el, index) => visible[index])
2815
3128
  }
2816
3129
 
2817
3130
  async function findFields(locator) {
2818
- const matchedLocator = new Locator(locator);
3131
+ const matchedLocator = new Locator(locator)
2819
3132
  if (!matchedLocator.isFuzzy()) {
2820
- return this._locate(matchedLocator);
3133
+ return this._locate(matchedLocator)
2821
3134
  }
2822
- const literal = xpathLocator.literal(locator);
3135
+ const literal = xpathLocator.literal(matchedLocator.value)
2823
3136
 
2824
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) });
3137
+ let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
2825
3138
  if (els.length) {
2826
- return els;
3139
+ return els
2827
3140
  }
2828
3141
 
2829
- els = await this._locate({ xpath: Locator.field.labelContains(literal) });
3142
+ els = await this._locate({ xpath: Locator.field.labelContains(literal) })
2830
3143
  if (els.length) {
2831
- return els;
3144
+ return els
2832
3145
  }
2833
- els = await this._locate({ xpath: Locator.field.byName(literal) });
3146
+ els = await this._locate({ xpath: Locator.field.byName(literal) })
2834
3147
  if (els.length) {
2835
- return els;
3148
+ return els
3149
+ }
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
2836
3158
  }
2837
- return this._locate({ css: locator });
3159
+
3160
+ return this._locate({ css: matchedLocator.value })
2838
3161
  }
2839
3162
 
2840
3163
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2841
- const src = await this._locate(sourceLocator);
2842
- assertElementExists(src, sourceLocator, 'Source Element');
3164
+ const src = await this._locateElement(sourceLocator)
3165
+ if (!src) {
3166
+ throw new ElementNotFound(sourceLocator, 'Source Element')
3167
+ }
2843
3168
 
2844
- const dst = await this._locate(destinationLocator);
2845
- assertElementExists(dst, destinationLocator, 'Destination Element');
3169
+ const dst = await this._locateElement(destinationLocator)
3170
+ if (!dst) {
3171
+ throw new ElementNotFound(destinationLocator, 'Destination Element')
3172
+ }
2846
3173
 
2847
- // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
2848
- const dragSource = await getClickablePoint(src[0]);
2849
- 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)
2850
3177
 
2851
3178
  // Drag start point
2852
- await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 });
2853
- await this.page.mouse.down();
3179
+ await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
3180
+ await this.page.mouse.down()
2854
3181
 
2855
3182
  // Drag destination
2856
- await this.page.mouse.move(dragDestination.x, dragDestination.y, { steps: 5 });
2857
- await this.page.mouse.up();
3183
+ await this.page.mouse.move(dragDestination.x, dragDestination.y, { steps: 5 })
3184
+ await this.page.mouse.up()
2858
3185
 
2859
- await this._waitForAction();
3186
+ await this._waitForAction()
2860
3187
  }
2861
3188
 
2862
3189
  async function proceedSeeInField(assertType, field, value) {
2863
- const els = await findVisibleFields.call(this, field);
2864
- assertElementExists(els, field, 'Field');
2865
- const el = els[0];
2866
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
2867
- const fieldType = await el.getProperty('type').then(el => el.jsonValue());
3190
+ const els = await findVisibleFields.call(this, field)
3191
+ assertElementExists(els, field, 'Field')
3192
+ const el = els[0]
3193
+ const tag = await el.getProperty('tagName').then(el => el.jsonValue())
3194
+ const fieldType = await el.getProperty('type').then(el => el.jsonValue())
2868
3195
 
2869
- const proceedMultiple = async (elements) => {
2870
- const fields = Array.isArray(elements) ? elements : [elements];
3196
+ const proceedMultiple = async elements => {
3197
+ const fields = Array.isArray(elements) ? elements : [elements]
2871
3198
 
2872
- const elementValues = [];
3199
+ const elementValues = []
2873
3200
  for (const element of fields) {
2874
- elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
3201
+ elementValues.push(await element.getProperty('value').then(el => el.jsonValue()))
2875
3202
  }
2876
3203
 
2877
3204
  if (typeof value === 'boolean') {
2878
- equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length);
3205
+ equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length)
2879
3206
  } else {
2880
3207
  if (assertType === 'assert') {
2881
- equals(`select option by ${field}`)[assertType](true, elementValues.length > 0);
3208
+ equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
2882
3209
  }
2883
- elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val));
3210
+ elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
2884
3211
  }
2885
- };
3212
+ }
2886
3213
 
2887
3214
  if (tag === 'SELECT') {
2888
- const selectedOptions = await el.$$('option:checked');
3215
+ const selectedOptions = await el.$$('option:checked')
2889
3216
  // locate option by values and check them
2890
3217
  if (value === '') {
2891
- return proceedMultiple(selectedOptions);
3218
+ return proceedMultiple(selectedOptions)
2892
3219
  }
2893
3220
 
2894
- const options = await filterFieldsByValue(selectedOptions, value, true);
2895
- return proceedMultiple(options);
3221
+ const options = await filterFieldsByValue(selectedOptions, value, true)
3222
+ return proceedMultiple(options)
2896
3223
  }
2897
3224
 
2898
3225
  if (tag === 'INPUT') {
2899
3226
  if (fieldType === 'checkbox' || fieldType === 'radio') {
2900
3227
  if (typeof value === 'boolean') {
2901
3228
  // Filter by values
2902
- const options = await filterFieldsBySelectionState(els, true);
2903
- return proceedMultiple(options);
3229
+ const options = await filterFieldsBySelectionState(els, true)
3230
+ return proceedMultiple(options)
2904
3231
  }
2905
3232
 
2906
- const options = await filterFieldsByValue(els, value, true);
2907
- return proceedMultiple(options);
3233
+ const options = await filterFieldsByValue(els, value, true)
3234
+ return proceedMultiple(options)
2908
3235
  }
2909
- return proceedMultiple(els[0]);
3236
+ return proceedMultiple(els[0])
3237
+ }
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)
2910
3243
  }
2911
- const fieldVal = await el.getProperty('value').then(el => el.jsonValue());
2912
- return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
3244
+
3245
+ return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
2913
3246
  }
2914
3247
 
2915
3248
  async function filterFieldsByValue(elements, value, onlySelected) {
2916
- const matches = [];
3249
+ const matches = []
2917
3250
  for (const element of elements) {
2918
- const val = await element.getProperty('value').then(el => el.jsonValue());
2919
- let isSelected = true;
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
+
3257
+ let isSelected = true
2920
3258
  if (onlySelected) {
2921
- isSelected = await elementSelected(element);
3259
+ isSelected = await elementSelected(element)
2922
3260
  }
2923
- if ((value == null || val.indexOf(value) > -1) && isSelected) {
2924
- matches.push(element);
3261
+ if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
3262
+ matches.push(element)
2925
3263
  }
2926
3264
  }
2927
- return matches;
3265
+ return matches
2928
3266
  }
2929
3267
 
2930
3268
  async function filterFieldsBySelectionState(elements, state) {
2931
- const matches = [];
3269
+ const matches = []
2932
3270
  for (const element of elements) {
2933
- const isSelected = await elementSelected(element);
3271
+ const isSelected = await elementSelected(element)
2934
3272
  if (isSelected === state) {
2935
- matches.push(element);
3273
+ matches.push(element)
2936
3274
  }
2937
3275
  }
2938
- return matches;
3276
+ return matches
2939
3277
  }
2940
3278
 
2941
3279
  async function elementSelected(element) {
2942
- const type = await element.getProperty('type').then(el => el.jsonValue());
3280
+ const type = await element.getProperty('type').then(el => el.jsonValue())
2943
3281
 
2944
3282
  if (type === 'checkbox' || type === 'radio') {
2945
- return element.getProperty('checked').then(el => el.jsonValue());
3283
+ return element.getProperty('checked').then(el => el.jsonValue())
2946
3284
  }
2947
- return element.getProperty('selected').then(el => el.jsonValue());
3285
+ return element.getProperty('selected').then(el => el.jsonValue())
2948
3286
  }
2949
3287
 
2950
3288
  function isFrameLocator(locator) {
2951
- locator = new Locator(locator);
3289
+ locator = new Locator(locator)
2952
3290
  if (locator.isFrame()) {
2953
- const _locator = new Locator(locator);
2954
- return _locator.value;
3291
+ const _locator = new Locator(locator)
3292
+ return _locator.value
2955
3293
  }
2956
- return false;
3294
+ return false
2957
3295
  }
2958
3296
 
2959
3297
  function assertElementExists(res, locator, prefix, suffix) {
2960
3298
  if (!res || res.length === 0) {
2961
- throw new ElementNotFound(locator, prefix, suffix);
3299
+ throw new ElementNotFound(locator, prefix, suffix)
2962
3300
  }
2963
3301
  }
2964
3302
 
2965
3303
  function $XPath(element, selector) {
2966
- const found = document.evaluate(selector, element || document.body, null, 5, null);
2967
- const res = [];
2968
- let current = null;
2969
- while (current = found.iterateNext()) {
2970
- res.push(current);
3304
+ const found = document.evaluate(selector, element || document.body, null, 5, null)
3305
+ const res = []
3306
+ let current = null
3307
+ while ((current = found.iterateNext())) {
3308
+ res.push(current)
2971
3309
  }
2972
- return res;
3310
+ return res
2973
3311
  }
2974
3312
 
2975
3313
  async function targetCreatedHandler(page) {
2976
- if (!page) return;
2977
- this.withinLocator = null;
3314
+ if (!page) return
3315
+ this.withinLocator = null
2978
3316
  page.on('load', () => {
2979
- page.$('body')
3317
+ page
3318
+ .$('body')
2980
3319
  .catch(() => null)
2981
- .then(context => this.context = context);
2982
- });
2983
- page.on('console', (msg) => {
2984
- this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '));
2985
- consoleLogStore.add(msg);
2986
- });
3320
+ .then(context => (this.context = context))
3321
+ })
3322
+ page.on('console', msg => {
3323
+ this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '))
3324
+ consoleLogStore.add(msg)
3325
+ })
2987
3326
 
2988
3327
  if (this.options.userAgent) {
2989
- await page.setUserAgent(this.options.userAgent);
3328
+ await page.setUserAgent(this.options.userAgent)
2990
3329
  }
2991
3330
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
2992
- const dimensions = this.options.windowSize.split('x');
2993
- const width = parseInt(dimensions[0], 10);
2994
- const height = parseInt(dimensions[1], 10);
2995
- await page.setViewport({ width, height });
3331
+ const dimensions = this.options.windowSize.split('x')
3332
+ const width = parseInt(dimensions[0], 10)
3333
+ const height = parseInt(dimensions[1], 10)
3334
+ await page.setViewport({ width, height })
2996
3335
  }
2997
3336
  }
2998
3337
 
2999
3338
  // BC compatibility for Puppeteer < 10
3000
3339
  async function getClickablePoint(el) {
3001
- if (el.clickablePoint) return el.clickablePoint();
3002
- if (el._clickablePoint) return el._clickablePoint();
3003
- return null;
3340
+ if (el.clickablePoint) return el.clickablePoint()
3341
+ if (el._clickablePoint) return el._clickablePoint()
3342
+ return null
3004
3343
  }
3005
3344
 
3006
3345
  // List of key values to key definitions
3007
- // 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
3008
3347
  const keyDefinitionMap = {
3009
- /* eslint-disable quote-props */
3010
- '0': 'Digit0',
3011
- '1': 'Digit1',
3012
- '2': 'Digit2',
3013
- '3': 'Digit3',
3014
- '4': 'Digit4',
3015
- '5': 'Digit5',
3016
- '6': 'Digit6',
3017
- '7': 'Digit7',
3018
- '8': 'Digit8',
3019
- '9': 'Digit9',
3020
- 'a': 'KeyA',
3021
- 'b': 'KeyB',
3022
- 'c': 'KeyC',
3023
- 'd': 'KeyD',
3024
- 'e': 'KeyE',
3025
- 'f': 'KeyF',
3026
- 'g': 'KeyG',
3027
- 'h': 'KeyH',
3028
- 'i': 'KeyI',
3029
- 'j': 'KeyJ',
3030
- 'k': 'KeyK',
3031
- 'l': 'KeyL',
3032
- 'm': 'KeyM',
3033
- 'n': 'KeyN',
3034
- 'o': 'KeyO',
3035
- 'p': 'KeyP',
3036
- 'q': 'KeyQ',
3037
- 'r': 'KeyR',
3038
- 's': 'KeyS',
3039
- 't': 'KeyT',
3040
- 'u': 'KeyU',
3041
- 'v': 'KeyV',
3042
- 'w': 'KeyW',
3043
- 'x': 'KeyX',
3044
- 'y': 'KeyY',
3045
- 'z': 'KeyZ',
3348
+ 0: 'Digit0',
3349
+ 1: 'Digit1',
3350
+ 2: 'Digit2',
3351
+ 3: 'Digit3',
3352
+ 4: 'Digit4',
3353
+ 5: 'Digit5',
3354
+ 6: 'Digit6',
3355
+ 7: 'Digit7',
3356
+ 8: 'Digit8',
3357
+ 9: 'Digit9',
3358
+ a: 'KeyA',
3359
+ b: 'KeyB',
3360
+ c: 'KeyC',
3361
+ d: 'KeyD',
3362
+ e: 'KeyE',
3363
+ f: 'KeyF',
3364
+ g: 'KeyG',
3365
+ h: 'KeyH',
3366
+ i: 'KeyI',
3367
+ j: 'KeyJ',
3368
+ k: 'KeyK',
3369
+ l: 'KeyL',
3370
+ m: 'KeyM',
3371
+ n: 'KeyN',
3372
+ o: 'KeyO',
3373
+ p: 'KeyP',
3374
+ q: 'KeyQ',
3375
+ r: 'KeyR',
3376
+ s: 'KeyS',
3377
+ t: 'KeyT',
3378
+ u: 'KeyU',
3379
+ v: 'KeyV',
3380
+ w: 'KeyW',
3381
+ x: 'KeyX',
3382
+ y: 'KeyY',
3383
+ z: 'KeyZ',
3046
3384
  ';': 'Semicolon',
3047
3385
  '=': 'Equal',
3048
3386
  ',': 'Comma',
@@ -3053,91 +3391,150 @@ const keyDefinitionMap = {
3053
3391
  '[': 'BracketLeft',
3054
3392
  '\\': 'Backslash',
3055
3393
  ']': 'BracketRight',
3056
- '\'': 'Quote',
3057
- /* eslint-enable quote-props */
3058
- };
3394
+ "'": 'Quote',
3395
+ }
3059
3396
 
3060
3397
  function getNormalizedKey(key) {
3061
- const normalizedKey = getNormalizedKeyAttributeValue(key);
3398
+ const normalizedKey = getNormalizedKeyAttributeValue(key)
3062
3399
  if (key !== normalizedKey) {
3063
- this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`);
3400
+ this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`)
3064
3401
  }
3065
3402
  // Use key definition to ensure correct key is displayed when Shift modifier is active
3066
3403
  if (Object.prototype.hasOwnProperty.call(keyDefinitionMap, normalizedKey)) {
3067
- return keyDefinitionMap[normalizedKey];
3404
+ return keyDefinitionMap[normalizedKey]
3068
3405
  }
3069
- return normalizedKey;
3406
+ return normalizedKey
3070
3407
  }
3071
3408
 
3072
3409
  function highlightActiveElement(element, context) {
3073
3410
  if (this.options.highlightElement && global.debugMode) {
3074
- highlightElement(element, context);
3411
+ highlightElement(element, context)
3075
3412
  }
3076
3413
  }
3077
3414
 
3078
3415
  function _waitForElement(locator, options) {
3079
3416
  try {
3080
- return this.context.waitForXPath(locator.value, options);
3417
+ return this.context.waitForXPath(locator.value, options)
3081
3418
  } catch (e) {
3082
- return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options);
3419
+ return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options)
3083
3420
  }
3084
3421
  }
3085
3422
 
3086
- async function findReactElements(locator, props = {}, state = {}) {
3087
- const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8');
3088
- await this.page.evaluate(resqScript.toString());
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)
3431
+ const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
3432
+ await this.page.evaluate(resqScript.toString())
3433
+
3434
+ await this.page.evaluate(() => window.resq.waitToLoadReact())
3435
+ const arrayHandle = await this.page.evaluateHandle(
3436
+ obj => {
3437
+ const { selector, props, state } = obj
3438
+ let elements = window.resq.resq$$(selector)
3439
+ if (Object.keys(props).length) {
3440
+ elements = elements.byProps(props)
3441
+ }
3442
+ if (Object.keys(state).length) {
3443
+ elements = elements.byState(state)
3444
+ }
3445
+
3446
+ if (!elements.length) {
3447
+ return []
3448
+ }
3449
+
3450
+ // resq returns an array of HTMLElements if the React component is a fragment
3451
+ // this avoids having nested arrays of nodes which the driver does not understand
3452
+ // [[div, div], [div, div]] => [div, div, div, div]
3453
+ let nodes = []
3089
3454
 
3090
- await this.page.evaluate(() => window.resq.waitToLoadReact());
3091
- const arrayHandle = await this.page.evaluateHandle((obj) => {
3092
- const { selector, props, state } = obj;
3093
- let elements = window.resq.resq$$(selector);
3094
- if (Object.keys(props).length) {
3095
- elements = elements.byProps(props);
3096
- }
3097
- if (Object.keys(state).length) {
3098
- elements = elements.byState(state);
3099
- }
3455
+ elements.forEach(element => {
3456
+ let { node, isFragment } = element
3100
3457
 
3101
- if (!elements.length) {
3102
- return [];
3458
+ if (!node) {
3459
+ isFragment = true
3460
+ node = element.children
3461
+ }
3462
+
3463
+ if (isFragment) {
3464
+ nodes = nodes.concat(node)
3465
+ } else {
3466
+ nodes.push(node)
3467
+ }
3468
+ })
3469
+
3470
+ return [...nodes]
3471
+ },
3472
+ {
3473
+ selector: resolved.react,
3474
+ props: resolved.props || {},
3475
+ state: resolved.state || {},
3476
+ },
3477
+ )
3478
+
3479
+ const properties = await arrayHandle.getProperties()
3480
+ const result = []
3481
+ for (const property of properties.values()) {
3482
+ const elementHandle = property.asElement()
3483
+ if (elementHandle) {
3484
+ result.push(elementHandle)
3103
3485
  }
3486
+ }
3104
3487
 
3105
- // resq returns an array of HTMLElements if the React component is a fragment
3106
- // this avoids having nested arrays of nodes which the driver does not understand
3107
- // [[div, div], [div, div]] => [div, div, div, div]
3108
- let nodes = [];
3488
+ await arrayHandle.dispose()
3489
+ return result
3490
+ }
3109
3491
 
3110
- elements.forEach((element) => {
3111
- let { node, isFragment } = element;
3492
+ async function findByRole(matcher, locator) {
3493
+ const resolved = toLocatorConfig(locator, 'role')
3494
+ const roleSelector = buildRoleSelector(resolved)
3112
3495
 
3113
- if (!node) {
3114
- isFragment = true;
3115
- node = element.children;
3116
- }
3496
+ if (!resolved.text && !resolved.name) {
3497
+ return matcher.$$(roleSelector)
3498
+ }
3117
3499
 
3118
- if (isFragment) {
3119
- nodes = nodes.concat(node);
3120
- } else {
3121
- nodes.push(node);
3122
- }
3123
- });
3500
+ const allElements = await matcher.$$(roleSelector)
3501
+ const filtered = []
3502
+ const accessibleName = resolved.text ?? resolved.name
3503
+ const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
3124
3504
 
3125
- return [...nodes];
3126
- }, {
3127
- selector: locator.react,
3128
- props: locator.props || {},
3129
- state: locator.state || {},
3130
- });
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
+ })
3131
3513
 
3132
- const properties = await arrayHandle.getProperties();
3133
- const result = [];
3134
- for (const property of properties.values()) {
3135
- const elementHandle = property.asElement();
3136
- if (elementHandle) {
3137
- result.push(elementHandle);
3138
- }
3514
+ if (texts.some(text => matcherFn(text))) filtered.push(el)
3139
3515
  }
3140
3516
 
3141
- await arrayHandle.dispose();
3142
- return result;
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)
3143
3539
  }
3540
+