browsertime 26.3.2 → 27.0.0

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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +1 -1
  3. package/browserscripts/timings/softNavigations.js +94 -0
  4. package/lib/chrome/har.js +36 -5
  5. package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
  6. package/lib/chrome/webdriver/chromium.js +39 -0
  7. package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
  8. package/lib/core/engine/collector.js +340 -317
  9. package/lib/core/engine/command/addText.js +93 -12
  10. package/lib/core/engine/command/click.js +239 -139
  11. package/lib/core/engine/command/commandHelper.js +45 -0
  12. package/lib/core/engine/command/cookie.js +85 -0
  13. package/lib/core/engine/command/element.js +37 -2
  14. package/lib/core/engine/command/javaScript.js +14 -24
  15. package/lib/core/engine/command/measure/screenshots.js +114 -0
  16. package/lib/core/engine/command/measure/video.js +85 -0
  17. package/lib/core/engine/command/measure.js +104 -180
  18. package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
  19. package/lib/core/engine/command/mouse/contextClick.js +56 -4
  20. package/lib/core/engine/command/mouse/doubleClick.js +49 -1
  21. package/lib/core/engine/command/mouse/mouseMove.js +50 -1
  22. package/lib/core/engine/command/mouse/singleClick.js +165 -129
  23. package/lib/core/engine/command/navigation.js +6 -6
  24. package/lib/core/engine/command/select.js +95 -3
  25. package/lib/core/engine/command/selectorParser.js +58 -0
  26. package/lib/core/engine/command/set.js +55 -1
  27. package/lib/core/engine/command/wait.js +41 -2
  28. package/lib/core/engine/commands.js +550 -42
  29. package/lib/core/engine/index.js +206 -150
  30. package/lib/core/engine/iteration.js +269 -137
  31. package/lib/firefox/networkManager.js +22 -30
  32. package/lib/firefox/webdriver/builder.js +14 -0
  33. package/lib/safari/har.js +131 -0
  34. package/lib/safari/native/ios-capture +0 -0
  35. package/lib/safari/native/ios-capture.m +340 -0
  36. package/lib/safari/safariInspectorClient.js +322 -0
  37. package/lib/safari/webdriver/safari.js +303 -13
  38. package/lib/safari/webkitToCdpAdapter.js +241 -0
  39. package/lib/support/cli.js +13 -0
  40. package/lib/support/engineUtils.js +16 -5
  41. package/lib/support/getViewPort.js +5 -0
  42. package/lib/support/har/index.js +41 -3
  43. package/lib/support/usbPower.js +2 -3
  44. package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
  45. package/lib/video/postprocessing/finetune/getFont.js +3 -3
  46. package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
  47. package/lib/video/postprocessing/finetune/index.js +26 -9
  48. package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
  49. package/lib/video/screenRecording/recorder.js +2 -2
  50. package/lib/video/screenRecording/setOrangeBackground.js +3 -0
  51. package/package.json +11 -11
  52. package/types/core/engine/command/addText.d.ts +35 -6
  53. package/types/core/engine/command/addText.d.ts.map +1 -1
  54. package/types/core/engine/command/click.d.ts +87 -16
  55. package/types/core/engine/command/click.d.ts.map +1 -1
  56. package/types/core/engine/command/commandHelper.d.ts +16 -0
  57. package/types/core/engine/command/commandHelper.d.ts.map +1 -0
  58. package/types/core/engine/command/cookie.d.ts +65 -0
  59. package/types/core/engine/command/cookie.d.ts.map +1 -0
  60. package/types/core/engine/command/element.d.ts +19 -1
  61. package/types/core/engine/command/element.d.ts.map +1 -1
  62. package/types/core/engine/command/javaScript.d.ts +4 -0
  63. package/types/core/engine/command/javaScript.d.ts.map +1 -1
  64. package/types/core/engine/command/measure/screenshots.d.ts +15 -0
  65. package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
  66. package/types/core/engine/command/measure/video.d.ts +26 -0
  67. package/types/core/engine/command/measure/video.d.ts.map +1 -0
  68. package/types/core/engine/command/measure.d.ts +59 -33
  69. package/types/core/engine/command/measure.d.ts.map +1 -1
  70. package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
  71. package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
  72. package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
  73. package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
  74. package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
  75. package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
  76. package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
  77. package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
  78. package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
  79. package/types/core/engine/command/navigation.d.ts +3 -3
  80. package/types/core/engine/command/select.d.ts +45 -8
  81. package/types/core/engine/command/select.d.ts.map +1 -1
  82. package/types/core/engine/command/selectorParser.d.ts +20 -0
  83. package/types/core/engine/command/selectorParser.d.ts.map +1 -0
  84. package/types/core/engine/command/set.d.ts +34 -7
  85. package/types/core/engine/command/set.d.ts.map +1 -1
  86. package/types/core/engine/command/wait.d.ts +29 -6
  87. package/types/core/engine/command/wait.d.ts.map +1 -1
  88. package/types/core/engine/commands.d.ts +296 -28
  89. package/types/core/engine/commands.d.ts.map +1 -1
  90. package/types/support/engineUtils.d.ts.map +1 -1
  91. package/types/support/getViewPort.d.ts.map +1 -1
  92. package/types/support/usbPower.d.ts.map +1 -1
  93. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  94. package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
  95. package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
  96. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
  97. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
  98. package/types/video/screenRecording/recorder.d.ts +2 -2
  99. package/types/video/screenRecording/recorder.d.ts.map +1 -1
  100. package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
  101. package/types/video/video.d.ts +1 -1
  102. package/types/video/video.d.ts.map +1 -1
  103. package/visualmetrics/visualmetrics-portable.py +136 -136
  104. package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
  105. package/lib/video/screenRecording/ios/iosRecorder.js +0 -68
@@ -1,3 +1,6 @@
1
+ import webdriver from 'selenium-webdriver';
2
+
3
+ import { parseSelector } from './command/selectorParser.js';
1
4
  import { Actions } from './command/actions.js';
2
5
  import { AddText } from './command/addText.js';
3
6
  import { Click } from './command/click.js';
@@ -9,6 +12,7 @@ import { Switch } from './command/switch.js';
9
12
  import { Screenshot } from './command/screenshot.js';
10
13
  import { Set } from './command/set.js';
11
14
  import { Cache } from './command/cache.js';
15
+ import { Cookie } from './command/cookie.js';
12
16
  import { Meta } from './command/meta.js';
13
17
  import { Watch as StopWatch } from './command/stopWatch.js';
14
18
  import { Select } from './command/select.js';
@@ -36,7 +40,7 @@ import { SimplePerfProfiler } from './command/simpleperf.js';
36
40
  * @hideconstructor
37
41
  */
38
42
  export class Commands {
39
- constructor(
43
+ constructor({
40
44
  browser,
41
45
  engineDelegate,
42
46
  index,
@@ -50,8 +54,8 @@ export class Commands {
50
54
  asyncScriptsByCategory,
51
55
  postURLScripts,
52
56
  options
53
- ) {
54
- const measure = new Measure(
57
+ }) {
58
+ const measure = new Measure({
55
59
  browser,
56
60
  index,
57
61
  pageCompleteCheck,
@@ -65,7 +69,7 @@ export class Commands {
65
69
  context,
66
70
  screenshotManager,
67
71
  options
68
- );
72
+ });
69
73
 
70
74
  /**
71
75
  * Provides functionality to collect perfetto traces.
@@ -118,9 +122,36 @@ export class Commands {
118
122
 
119
123
  /**
120
124
  * Provides functionality to perform click actions on elements in a web page using various selectors.
121
- * @type {Click}
125
+ * Can be called directly as a function with a unified selector string, or use the
126
+ * existing by* methods for backward compatibility.
127
+ * @type {Function & Click}
128
+ */
129
+ const clickInstance = new Click(browser, pageCompleteCheck, options);
130
+
131
+ /**
132
+ * Click on an element using a unified selector string.
133
+ * Supports CSS selectors (default), and prefixes: 'id:', 'xpath:', 'text:', 'link:', 'name:', 'class:'.
134
+ * @async
135
+ * @example await commands.click('#login-btn');
136
+ * @example await commands.click('id:login-btn');
137
+ * @example await commands.click('text:Submit', { waitForNavigation: true });
138
+ * @param {string} selector - The selector string.
139
+ * @param {Object} [clickOptions] - Options for the click.
140
+ * @param {boolean} [clickOptions.waitForNavigation=false] - If true, waits for the page to complete loading after clicking.
141
+ * @returns {Promise<void>}
122
142
  */
123
- this.click = new Click(browser, pageCompleteCheck);
143
+ this.click = async (selector, clickOptions) =>
144
+ clickInstance.run(selector, clickOptions);
145
+ // Preserve all existing by* methods for backward compatibility
146
+ for (const key of Object.getOwnPropertyNames(
147
+ Object.getPrototypeOf(clickInstance)
148
+ )) {
149
+ if (key !== 'constructor' && typeof clickInstance[key] === 'function') {
150
+ this.click[key] = clickInstance[key].bind(clickInstance);
151
+ }
152
+ }
153
+
154
+ measure.setClick(this.click);
124
155
 
125
156
  /**
126
157
  * Provides functionality to control page scrolling in the browser.
@@ -130,15 +161,61 @@ export class Commands {
130
161
 
131
162
  /**
132
163
  * Provides functionality to add text to elements on a web page using various selectors.
133
- * @type {AddText}
164
+ * Can be called directly as a function with a unified selector string, or use the
165
+ * existing by* methods for backward compatibility.
166
+ * @type {Function & AddText}
134
167
  */
135
- this.addText = new AddText(browser);
168
+ const addTextInstance = new AddText(browser, options);
169
+
170
+ /**
171
+ * Add text to an element using a unified selector string.
172
+ * @async
173
+ * @example await commands.addText('#search-input', 'search term');
174
+ * @example await commands.addText('id:username', 'myuser');
175
+ * @param {string} selector - The selector string.
176
+ * @param {string} text - The text to add.
177
+ * @returns {Promise<void>}
178
+ */
179
+ this.addText = async (selector, text) =>
180
+ addTextInstance.run(selector, text);
181
+ // Preserve all existing by* methods for backward compatibility
182
+ for (const key of Object.getOwnPropertyNames(
183
+ Object.getPrototypeOf(addTextInstance)
184
+ )) {
185
+ if (key !== 'constructor' && typeof addTextInstance[key] === 'function') {
186
+ this.addText[key] = addTextInstance[key].bind(addTextInstance);
187
+ }
188
+ }
136
189
 
137
190
  /**
138
191
  * Provides functionality to wait for different conditions in the browser.
139
- * @type {Wait}
192
+ * Can be called directly as a function with a unified selector string, or use the
193
+ * existing by* methods for backward compatibility.
194
+ * @type {Function & Wait}
195
+ */
196
+ const waitInstance = new Wait(browser, pageCompleteCheck);
197
+
198
+ /**
199
+ * Wait for an element using a unified selector string.
200
+ * @async
201
+ * @example await commands.wait('#my-element', { timeout: 5000 });
202
+ * @example await commands.wait('id:loaded', { timeout: 3000, visible: true });
203
+ * @param {string} selector - The selector string.
204
+ * @param {Object} [waitOptions] - Options for waiting.
205
+ * @param {number} [waitOptions.timeout=6000] - Maximum time to wait in ms.
206
+ * @param {boolean} [waitOptions.visible=false] - Wait for visibility.
207
+ * @returns {Promise<void>}
140
208
  */
141
- this.wait = new Wait(browser, pageCompleteCheck);
209
+ this.wait = async (selector, waitOptions) =>
210
+ waitInstance.run(selector, waitOptions);
211
+ // Preserve all existing by* methods for backward compatibility
212
+ for (const key of Object.getOwnPropertyNames(
213
+ Object.getPrototypeOf(waitInstance)
214
+ )) {
215
+ if (key !== 'constructor' && typeof waitInstance[key] === 'function') {
216
+ this.wait[key] = waitInstance[key].bind(waitInstance);
217
+ }
218
+ }
142
219
 
143
220
  /**
144
221
  * Provides functionality for measuring a navigation.
@@ -197,9 +274,32 @@ export class Commands {
197
274
 
198
275
  /**
199
276
  * Sets values on HTML elements in the page.
200
- * @type {Set}
277
+ * Can be called directly as a function with a unified selector string, or use the
278
+ * existing by* methods for backward compatibility.
279
+ * @type {Function & Set}
280
+ */
281
+ const setInstance = new Set(browser, options);
282
+
283
+ /**
284
+ * Set a property on an element using a unified selector string.
285
+ * @async
286
+ * @example await commands.set('#field', 'new value');
287
+ * @example await commands.set('id:title', '<h1>Hello</h1>', 'innerHTML');
288
+ * @param {string} selector - The selector string for the element.
289
+ * @param {string} setValue - The value to set.
290
+ * @param {string} [property='value'] - The property: 'value', 'innerText', or 'innerHTML'.
291
+ * @returns {Promise<void>}
201
292
  */
202
- this.set = new Set(browser);
293
+ this.set = async (selector, setValue, property) =>
294
+ setInstance.run(selector, setValue, property);
295
+ // Preserve all existing by* methods for backward compatibility
296
+ for (const key of Object.getOwnPropertyNames(
297
+ Object.getPrototypeOf(setInstance)
298
+ )) {
299
+ if (key !== 'constructor' && typeof setInstance[key] === 'function') {
300
+ this.set[key] = setInstance[key].bind(setInstance);
301
+ }
302
+ }
203
303
 
204
304
  /**
205
305
  * Stopwatch utility for measuring time intervals.
@@ -213,6 +313,12 @@ export class Commands {
213
313
  */
214
314
  this.cache = new Cache(browser, options.browser, cdp);
215
315
 
316
+ /**
317
+ * Manages browser cookies — get, set, delete individual or all cookies.
318
+ * @type {Cookie}
319
+ */
320
+ this.cookie = new Cookie(browser);
321
+
216
322
  /**
217
323
  * Adds metadata to the user journey.
218
324
  * @type {Meta}
@@ -255,39 +361,112 @@ export class Commands {
255
361
  * Interact with the page using the mouse.
256
362
  * @type {Object}
257
363
  */
364
+ const singleClickInstance = new SingleClick(
365
+ browser,
366
+ pageCompleteCheck,
367
+ options
368
+ );
369
+ const doubleClickInstance = new DoubleClick(
370
+ browser,
371
+ pageCompleteCheck,
372
+ options
373
+ );
374
+ const contextClickInstance = new ContextClick(browser, options);
375
+ const mouseMoveInstance = new MouseMove(browser, options);
376
+ const clickAndHoldInstance = new ClickAndHold(browser);
377
+
258
378
  this.mouse = {
259
- /**
260
- * Move the mouse cursor to elements or specific positions on a web page.
261
- * @type {MouseMove}
262
- */
263
- moveTo: new MouseMove(browser),
264
- /**
265
- * Perform a context click (right-click) on elements in a web page.
266
- * @type {ContextClick}
267
- */
268
- contextClick: new ContextClick(browser),
269
- /**
270
- * Provides functionality to perform a single click action on elements or at specific positions in a web page.
271
- * @type {SingleClick}
272
- */
273
- singleClick: new SingleClick(browser, pageCompleteCheck),
274
- /**
275
- * Provides functionality to perform a double-click action on elements in a web page.
276
- * @type {DoubleClick}
277
- */
278
- doubleClick: new DoubleClick(browser, pageCompleteCheck),
279
- /**
280
- * Provides functionality to click and hold elements on a web page using different strategies.
281
- * @type {ClickAndHold}
282
- */
283
- clickAndHold: new ClickAndHold(browser)
379
+ moveTo: async selector => mouseMoveInstance.run(selector),
380
+ singleClick: async (selector, mouseOptions) =>
381
+ singleClickInstance.run(selector, mouseOptions),
382
+ doubleClick: async selector => doubleClickInstance.run(selector),
383
+ contextClick: async selector => contextClickInstance.run(selector),
384
+ clickAndHold: clickAndHoldInstance
284
385
  };
285
386
 
387
+ // Preserve backward-compatible methods on each mouse command
388
+ for (const key of Object.getOwnPropertyNames(
389
+ Object.getPrototypeOf(mouseMoveInstance)
390
+ )) {
391
+ if (
392
+ key !== 'constructor' &&
393
+ typeof mouseMoveInstance[key] === 'function'
394
+ ) {
395
+ this.mouse.moveTo[key] = mouseMoveInstance[key].bind(mouseMoveInstance);
396
+ }
397
+ }
398
+ for (const key of Object.getOwnPropertyNames(
399
+ Object.getPrototypeOf(singleClickInstance)
400
+ )) {
401
+ if (
402
+ key !== 'constructor' &&
403
+ typeof singleClickInstance[key] === 'function'
404
+ ) {
405
+ this.mouse.singleClick[key] =
406
+ singleClickInstance[key].bind(singleClickInstance);
407
+ }
408
+ }
409
+ for (const key of Object.getOwnPropertyNames(
410
+ Object.getPrototypeOf(doubleClickInstance)
411
+ )) {
412
+ if (
413
+ key !== 'constructor' &&
414
+ typeof doubleClickInstance[key] === 'function'
415
+ ) {
416
+ this.mouse.doubleClick[key] =
417
+ doubleClickInstance[key].bind(doubleClickInstance);
418
+ }
419
+ }
420
+ for (const key of Object.getOwnPropertyNames(
421
+ Object.getPrototypeOf(contextClickInstance)
422
+ )) {
423
+ if (
424
+ key !== 'constructor' &&
425
+ typeof contextClickInstance[key] === 'function'
426
+ ) {
427
+ this.mouse.contextClick[key] =
428
+ contextClickInstance[key].bind(contextClickInstance);
429
+ }
430
+ }
431
+
286
432
  /**
287
433
  * Interact with a select element.
288
- * @type {Select}
434
+ * Can be called directly as a function with a unified selector string, or use the
435
+ * existing by* methods for backward compatibility.
436
+ * @type {Function & Select}
437
+ */
438
+ const selectInstance = new Select(browser, options);
439
+
440
+ /**
441
+ * Select an option in a select element using a unified selector string.
442
+ * @async
443
+ * @example await commands.select('#country', 'SE');
444
+ * @example await commands.select('id:language', 'en');
445
+ * @param {string} selector - The selector string for the select element.
446
+ * @param {string} selectValue - The value of the option to select.
447
+ * @returns {Promise<void>}
448
+ */
449
+ this.select = async (selector, selectValue) =>
450
+ selectInstance.run(selector, selectValue);
451
+ // Preserve all existing by* methods for backward compatibility
452
+ for (const key of Object.getOwnPropertyNames(
453
+ Object.getPrototypeOf(selectInstance)
454
+ )) {
455
+ if (key !== 'constructor' && typeof selectInstance[key] === 'function') {
456
+ this.select[key] = selectInstance[key].bind(selectInstance);
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Select an option in a select element by its visible text.
462
+ * @async
463
+ * @example await commands.select.byText('#country', 'Sweden');
464
+ * @param {string} selector - The selector string for the select element.
465
+ * @param {string} text - The visible text of the option to select.
466
+ * @returns {Promise<void>}
289
467
  */
290
- this.select = new Select(browser);
468
+ this.select.byText = async (selector, text) =>
469
+ selectInstance.runByText(selector, text);
291
470
 
292
471
  /**
293
472
  * Selenium's action sequence functionality.
@@ -297,9 +476,338 @@ export class Commands {
297
476
  this.action = new Actions(browser);
298
477
 
299
478
  /**
300
- * Get Selenium's WebElements.
301
- * @type {Element}
479
+ * Types text into an element identified by a CSS selector.
480
+ * This is a convenience method that wraps {@link AddText#bySelector addText.bySelector}
481
+ * with a more conventional parameter order (selector first, then text).
482
+ * @async
483
+ * @example await commands.type('#search-input', 'search term');
484
+ * @param {string} selector - The CSS selector of the element.
485
+ * @param {string} text - The text to type into the element.
486
+ * @returns {Promise<void>} A promise that resolves when the text has been added.
487
+ * @throws {Error} Throws an error if the element is not found.
488
+ * @type {Function}
489
+ */
490
+ this.type = async (selector, text) => {
491
+ return this.addText(selector, text);
492
+ };
493
+ this.element = new Element(browser, options);
494
+
495
+ /**
496
+ * Finds an element using a CSS selector, with optional waiting and visibility check.
497
+ * @async
498
+ * @example const element = await commands.find('.my-element', { timeout: 5000, visible: true });
499
+ * @param {string} selector - The CSS selector of the element.
500
+ * @param {Object} [options] - Options for finding the element.
501
+ * @param {number} [options.timeout] - Maximum time in milliseconds to wait for the element. Defaults to the configured --timeouts.elementWait value.
502
+ * @param {boolean} [options.visible=false] - If true, waits for the element to be visible.
503
+ * @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
504
+ * @throws {Error} Throws an error if the element is not found.
505
+ * @type {Function}
506
+ */
507
+ this.find = this.element.find.bind(this.element);
508
+
509
+ /**
510
+ * Checks if an element matching the selector exists in the DOM.
511
+ * Unlike find(), this does not throw if the element is not found.
512
+ * @async
513
+ * @example
514
+ * if (await commands.exists('#cookie-banner')) {
515
+ * await commands.click('#accept-cookies');
516
+ * }
517
+ * @param {string} selector - The CSS selector or prefixed selector.
518
+ * @param {Object} [existsOptions] - Options.
519
+ * @param {number} [existsOptions.timeout=0] - Maximum time to wait for the element. Default 0 (no wait).
520
+ * @returns {Promise<boolean>} True if the element exists.
521
+ * @type {Function}
522
+ */
523
+ this.exists = async (selector, existsOptions = {}) => {
524
+ const { locator } = parseSelector(selector);
525
+ const timeout = existsOptions.timeout ?? 0;
526
+ const driver = browser.getDriver();
527
+ try {
528
+ await (timeout > 0
529
+ ? driver.wait(webdriver.until.elementLocated(locator), timeout)
530
+ : driver.findElement(locator));
531
+ return true;
532
+ } catch {
533
+ return false;
534
+ }
535
+ };
536
+
537
+ const findElement = async selector => {
538
+ const { locator } = parseSelector(selector);
539
+ const driver = browser.getDriver();
540
+ const timeout = options?.timeouts?.elementWait ?? 0;
541
+ if (timeout > 0) {
542
+ await driver.wait(webdriver.until.elementLocated(locator), timeout);
543
+ }
544
+ return driver.findElement(locator);
545
+ };
546
+
547
+ /**
548
+ * Gets the visible text of an element matching the selector.
549
+ * @async
550
+ * @example const text = await commands.getText('#greeting');
551
+ * @example const text = await commands.getText('id:heading');
552
+ * @param {string} selector - The CSS selector or prefixed selector.
553
+ * @returns {Promise<string>} The visible text content.
554
+ * @throws {Error} Throws an error if the element is not found.
555
+ * @type {Function}
556
+ */
557
+ this.getText = async selector => {
558
+ const element = await findElement(selector);
559
+ return element.getText();
560
+ };
561
+
562
+ /**
563
+ * Gets the value of a form element matching the selector.
564
+ * @async
565
+ * @example const value = await commands.getValue('#price');
566
+ * @example const value = await commands.getValue('id:email');
567
+ * @param {string} selector - The CSS selector or prefixed selector.
568
+ * @returns {Promise<string>} The element's value.
569
+ * @throws {Error} Throws an error if the element is not found.
570
+ * @type {Function}
571
+ */
572
+ this.getValue = async selector => {
573
+ const element = await findElement(selector);
574
+ return element.getAttribute('value');
575
+ };
576
+
577
+ /**
578
+ * Checks if an element matching the selector is visible/displayed.
579
+ * @async
580
+ * @example const visible = await commands.isVisible('#error-message');
581
+ * @param {string} selector - The CSS selector or prefixed selector.
582
+ * @returns {Promise<boolean>} True if the element is visible.
583
+ * @throws {Error} Throws an error if the element is not found.
584
+ * @type {Function}
585
+ */
586
+ this.isVisible = async selector => {
587
+ const element = await findElement(selector);
588
+ return element.isDisplayed();
589
+ };
590
+
591
+ /**
592
+ * Gets an attribute value of an element matching the selector.
593
+ * @async
594
+ * @example const href = await commands.getAttribute('#my-link', 'href');
595
+ * @example const dataId = await commands.getAttribute('id:item', 'data-id');
596
+ * @param {string} selector - The CSS selector or prefixed selector.
597
+ * @param {string} attribute - The attribute name.
598
+ * @returns {Promise<string>} The attribute value.
599
+ * @throws {Error} Throws an error if the element is not found.
600
+ * @type {Function}
601
+ */
602
+ this.getAttribute = async (selector, attribute) => {
603
+ const element = await findElement(selector);
604
+ return element.getAttribute(attribute);
605
+ };
606
+
607
+ /**
608
+ * Checks if a form element matching the selector is enabled.
609
+ * @async
610
+ * @example const enabled = await commands.isEnabled('#submit-btn');
611
+ * @param {string} selector - The CSS selector or prefixed selector.
612
+ * @returns {Promise<boolean>} True if the element is enabled.
613
+ * @throws {Error} Throws an error if the element is not found.
614
+ * @type {Function}
615
+ */
616
+ this.isEnabled = async selector => {
617
+ const element = await findElement(selector);
618
+ return element.isEnabled();
619
+ };
620
+
621
+ /**
622
+ * Checks if a checkbox or radio button is selected/checked.
623
+ * @async
624
+ * @example const checked = await commands.isChecked('#agree-checkbox');
625
+ * @param {string} selector - The CSS selector or prefixed selector.
626
+ * @returns {Promise<boolean>} True if the element is checked/selected.
627
+ * @throws {Error} Throws an error if the element is not found.
628
+ * @type {Function}
629
+ */
630
+ this.isChecked = async selector => {
631
+ const element = await findElement(selector);
632
+ return element.isSelected();
633
+ };
634
+
635
+ /**
636
+ * Clears the content of a form element matching the selector.
637
+ * @async
638
+ * @example await commands.clear('#search-input');
639
+ * @example await commands.clear('id:email');
640
+ * @param {string} selector - The CSS selector or prefixed selector.
641
+ * @returns {Promise<void>} A promise that resolves when the element is cleared.
642
+ * @throws {Error} Throws an error if the element is not found.
643
+ * @type {Function}
644
+ */
645
+ this.clear = async selector => {
646
+ const { locator } = parseSelector(selector);
647
+ const driver = browser.getDriver();
648
+ const timeout = options?.timeouts?.elementWait ?? 0;
649
+ if (timeout > 0) {
650
+ await driver.wait(webdriver.until.elementLocated(locator), timeout);
651
+ }
652
+ const element = await driver.findElement(locator);
653
+ return element.clear();
654
+ };
655
+
656
+ /**
657
+ * Fills multiple form fields at once. Each key is a selector, each value is the text to type.
658
+ * @async
659
+ * @example
660
+ * await commands.fill({
661
+ * '#username': 'admin',
662
+ * '#password': 'secret',
663
+ * 'id:email': 'user@example.com'
664
+ * });
665
+ * @param {Object<string, string>} fields - An object mapping selectors to values.
666
+ * @returns {Promise<void>} A promise that resolves when all fields are filled.
667
+ * @throws {Error} Throws an error if any element is not found.
668
+ * @type {Function}
669
+ */
670
+ this.fill = async fields => {
671
+ for (const [selector, text] of Object.entries(fields)) {
672
+ await this.addText(selector, text);
673
+ }
674
+ };
675
+
676
+ /**
677
+ * Hovers over an element matching the selector. This is a convenience
678
+ * alias for commands.mouse.moveTo(selector).
679
+ * @async
680
+ * @example await commands.hover('#menu-item');
681
+ * @example await commands.hover('id:tooltip-trigger');
682
+ * @param {string} selector - The CSS selector or prefixed selector.
683
+ * @returns {Promise<void>}
684
+ * @throws {Error} Throws an error if the element is not found.
685
+ * @type {Function}
686
+ */
687
+ this.hover = async selector => {
688
+ return this.mouse.moveTo(selector);
689
+ };
690
+
691
+ /**
692
+ * Presses a keyboard key. Use key names like 'Enter', 'Tab', 'Escape',
693
+ * 'Backspace', 'ArrowUp', 'ArrowDown', etc.
694
+ * @async
695
+ * @example await commands.press('Enter');
696
+ * @example await commands.press('Tab');
697
+ * @example await commands.press('Escape');
698
+ * @param {string} key - The key name to press (e.g. 'Enter', 'Tab', 'Escape').
699
+ * @returns {Promise<void>}
700
+ * @type {Function}
701
+ */
702
+ this.press = async key => {
703
+ const keyValue = webdriver.Key[key.toUpperCase()] || key;
704
+ const driver = browser.getDriver();
705
+ await driver.actions({ async: true }).sendKeys(keyValue).perform();
706
+ return driver.actions().clear();
707
+ };
708
+
709
+ /**
710
+ * Gets the title of the current page.
711
+ * @async
712
+ * @example const title = await commands.getTitle();
713
+ * @returns {Promise<string>} The page title.
714
+ * @type {Function}
715
+ */
716
+ this.getTitle = async () => {
717
+ return browser.getDriver().getTitle();
718
+ };
719
+
720
+ /**
721
+ * Gets the URL of the current page.
722
+ * @async
723
+ * @example const url = await commands.getUrl();
724
+ * @returns {Promise<string>} The current URL.
725
+ * @type {Function}
726
+ */
727
+ this.getUrl = async () => {
728
+ return browser.getDriver().getCurrentUrl();
729
+ };
730
+
731
+ /**
732
+ * Checks a checkbox or radio button. Does nothing if already checked.
733
+ * @async
734
+ * @example await commands.check('#agree-terms');
735
+ * @example await commands.check('id:newsletter-opt-in');
736
+ * @param {string} selector - The CSS selector or prefixed selector.
737
+ * @returns {Promise<void>}
738
+ * @throws {Error} Throws an error if the element is not found.
739
+ * @type {Function}
740
+ */
741
+ this.check = async selector => {
742
+ const element = await findElement(selector);
743
+ const checked = await element.isSelected();
744
+ if (!checked) {
745
+ return element.click();
746
+ }
747
+ };
748
+
749
+ /**
750
+ * Unchecks a checkbox. Does nothing if already unchecked.
751
+ * @async
752
+ * @example await commands.uncheck('#agree-terms');
753
+ * @example await commands.uncheck('id:newsletter-opt-in');
754
+ * @param {string} selector - The CSS selector or prefixed selector.
755
+ * @returns {Promise<void>}
756
+ * @throws {Error} Throws an error if the element is not found.
757
+ * @type {Function}
302
758
  */
303
- this.element = new Element(browser);
759
+ this.uncheck = async selector => {
760
+ const element = await findElement(selector);
761
+ const checked = await element.isSelected();
762
+ if (checked) {
763
+ return element.click();
764
+ }
765
+ };
766
+
767
+ /**
768
+ * Scrolls the page so that the element matching the selector is visible in the viewport.
769
+ * @async
770
+ * @example await commands.scrollIntoView('#footer');
771
+ * @example await commands.scrollIntoView('id:comments-section');
772
+ * @param {string} selector - The CSS selector or prefixed selector.
773
+ * @returns {Promise<void>}
774
+ * @throws {Error} Throws an error if the element is not found.
775
+ * @type {Function}
776
+ */
777
+ this.scrollIntoView = async selector => {
778
+ const element = await findElement(selector);
779
+ return browser
780
+ .getDriver()
781
+ .executeScript(
782
+ 'arguments[0].scrollIntoView({block: "center", behavior: "instant"})',
783
+ element
784
+ );
785
+ };
786
+
787
+ /**
788
+ * Waits until the browser's current URL contains the given string.
789
+ * Useful after form submissions, login redirects, or SPA navigation.
790
+ * @async
791
+ * @example await commands.waitForUrl('dashboard');
792
+ * @example await commands.waitForUrl('/account', { timeout: 10000 });
793
+ * @param {string} pattern - The string to match against the current URL.
794
+ * @param {Object} [urlOptions] - Options.
795
+ * @param {number} [urlOptions.timeout=10000] - Maximum time to wait in milliseconds.
796
+ * @returns {Promise<void>}
797
+ * @throws {Error} Throws an error if the URL does not match within the timeout.
798
+ * @type {Function}
799
+ */
800
+ this.waitForUrl = async (pattern, urlOptions = {}) => {
801
+ const timeout = urlOptions.timeout ?? 10_000;
802
+ const driver = browser.getDriver();
803
+ try {
804
+ await driver.wait(webdriver.until.urlContains(pattern), timeout);
805
+ } catch {
806
+ const currentUrl = await driver.getCurrentUrl();
807
+ throw new Error(
808
+ `URL did not contain '${pattern}' within ${timeout} ms. Current URL: ${currentUrl}`
809
+ );
810
+ }
811
+ };
304
812
  }
305
813
  }