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
@@ -2,9 +2,7 @@ import path from 'node:path';
2
2
  import { getLogger } from '@sitespeed.io/log';
3
3
  import merge from 'lodash.merge';
4
4
  import { timestamp as _timestamp } from '../../../support/engineUtils.js';
5
- import { Video } from '../../../video/video.js';
6
5
  import { pathToFolder } from '../../../support/pathToFolder.js';
7
- import { setOrangeBackground } from '../../../video/screenRecording/setOrangeBackground.js';
8
6
  import {
9
7
  filterAllowlisted,
10
8
  filterBlocklisted
@@ -12,9 +10,8 @@ import {
12
10
  import { isAndroidConfigured, Android } from '../../../android/index.js';
13
11
  import { TCPDump } from '../../../support/tcpdump.js';
14
12
  import { getProperty } from '../../../support/util.js';
15
- import { lcpHighlightScript as highlightLargestContentfulPaint } from './util/lcpHighlightScript.js';
16
- import { clsHighlightScript as highlightLS } from './util/lsHighlightScript.js';
17
- const delay = ms => new Promise(res => setTimeout(res, ms));
13
+ import { MeasureScreenshots } from './measure/screenshots.js';
14
+ import { MeasureVideo } from './measure/video.js';
18
15
  const log = getLogger('browsertime.command.measure');
19
16
 
20
17
  // In some cases we could have one alias mapped to multiple URLs
@@ -38,7 +35,7 @@ function getNewResult() {
38
35
  * @hideconstructor
39
36
  */
40
37
  export class Measure {
41
- constructor(
38
+ constructor({
42
39
  browser,
43
40
  index,
44
41
  pageCompleteCheck,
@@ -52,7 +49,7 @@ export class Measure {
52
49
  context,
53
50
  screenshotManager,
54
51
  options
55
- ) {
52
+ }) {
56
53
  /**
57
54
  * @private
58
55
  */
@@ -77,22 +74,10 @@ export class Measure {
77
74
  * @private
78
75
  */
79
76
  this.options = options;
80
- /**
81
- * @private
82
- */
83
- this.screenshotManager = screenshotManager;
84
77
  /**
85
78
  * @private
86
79
  */
87
80
  this.storageManager = storageManager;
88
- /**
89
- * @private
90
- */
91
- this.recordVideo = options.visualMetrics || options.video;
92
- /**
93
- * @private
94
- */
95
- this.videos = videos;
96
81
  /**
97
82
  * @private
98
83
  */
@@ -132,64 +117,22 @@ export class Measure {
132
117
  /**
133
118
  * @private
134
119
  */
135
- this.ANDROID_DELAY_TIME = getProperty(
136
- options,
137
- 'orangeAndroidDelayTime',
138
- 2000
120
+ this.measureVideo = new MeasureVideo(
121
+ storageManager,
122
+ browser,
123
+ videos,
124
+ options
139
125
  );
140
126
  /**
141
127
  * @private
142
128
  */
143
- this.IOS_DELAY_TIME = getProperty(options, 'orangeIosDelayTime', 1000);
144
- /**
145
- * @private
146
- */
147
- this.DESKTOP_DELAY_TIME = getProperty(
148
- options,
149
- 'orangeDesktopDelayTime',
150
- 800
129
+ this.screenshots = new MeasureScreenshots(
130
+ browser,
131
+ screenshotManager,
132
+ options
151
133
  );
152
134
  }
153
135
 
154
- /**
155
- * Have a consistent way of starting the video
156
- * @private
157
- */
158
- async _startVideo(numberOfMeasuredPages, index) {
159
- this.video = new Video(this.storageManager, this.options, this.browser);
160
-
161
- if (this.options.firefox && this.options.firefox.windowRecorder) {
162
- // The Firefox window recorder will only record subsequent
163
- // changes after recording has begun. So the orange frame needs
164
- // to come after we start recording.
165
- await this.video.record(numberOfMeasuredPages, index);
166
- await setOrangeBackground(this.browser.getDriver(), this.options);
167
- } else {
168
- await setOrangeBackground(this.browser.getDriver(), this.options);
169
- await this.video.record(numberOfMeasuredPages, index);
170
- }
171
-
172
- // Give ffmpeg/video on phone time to settle
173
- if (isAndroidConfigured(this.options)) {
174
- return delay(this.ANDROID_DELAY_TIME);
175
- } else if (this.options.safari && this.options.safari.useSimulator) {
176
- return delay(this.IOS_DELAY_TIME);
177
- } else {
178
- return delay(this.DESKTOP_DELAY_TIME);
179
- }
180
- }
181
-
182
- /**
183
- * Have a consistent way of starting stopping the video
184
- * @private
185
- */
186
- async _stopVideo(url) {
187
- if (this.recordVideo && !this.options.videoParams.debug) {
188
- await this.video.stop(url);
189
- this.videos.push(this.video);
190
- }
191
- }
192
-
193
136
  /**
194
137
  * Have a consistent way of adding an error
195
138
  * @private
@@ -322,8 +265,16 @@ export class Measure {
322
265
  .executeScript('window.performance.clearResourceTimings();');
323
266
  }
324
267
 
325
- if (this.recordVideo && !this.options.videoParams.debug) {
326
- await this._startVideo(this.numberOfMeasuredPages, this.index);
268
+ // Record current soft navigation count so browser scripts
269
+ // only collect entries from this measurement
270
+ await this.browser
271
+ .getDriver()
272
+ .executeScript(
273
+ 'window.__bt_softNavCount = (performance.getEntriesByType("soft-navigation") || []).length;'
274
+ );
275
+
276
+ if (this.measureVideo.shouldRecord()) {
277
+ await this.measureVideo.start(this.numberOfMeasuredPages, this.index);
327
278
  // if we do not have the URL, we should remove the orange and
328
279
  // start meausuring because the user themself will navigate to
329
280
  // the URL that will be tested.
@@ -363,7 +314,7 @@ export class Measure {
363
314
  this.engineDelegate.failing(url);
364
315
  // If we got an error we still should stop the video to make sure we can use it to
365
316
  // try to understand what went wrong
366
- await this._stopVideo(url);
317
+ await this.measureVideo.stop(url);
367
318
  throw error;
368
319
  }
369
320
  } else {
@@ -387,7 +338,7 @@ export class Measure {
387
338
  // Remove the last result
388
339
  this.result.pop();
389
340
 
390
- await this._stopVideo();
341
+ await this.measureVideo.stop();
391
342
 
392
343
  if (this.options.tcpdump) {
393
344
  await this.tcpDump.stop();
@@ -395,6 +346,31 @@ export class Measure {
395
346
 
396
347
  return;
397
348
  }
349
+
350
+ /**
351
+ * @private
352
+ */
353
+ setClick(click) {
354
+ this._click = click;
355
+ }
356
+
357
+ /**
358
+ * Starts a measurement, clicks on an element matching the given CSS selector,
359
+ * waits for the page complete check, and stops the measurement. This is
360
+ * a shortcut for the common pattern of measure.start/click/measure.stop.
361
+ *
362
+ * @async
363
+ * @param {string} alias - The friendly name for this measurement.
364
+ * @param {string} selector - The CSS selector of the element to click.
365
+ * @returns {Promise} A promise that resolves with the collected metrics data.
366
+ * @throws {Error} Throws an error if the element is not found or the measurement fails.
367
+ */
368
+ async clickAndMeasure(alias, selector) {
369
+ await this.start(alias);
370
+ await this._click.bySelectorAndWait(selector);
371
+ return this.stop();
372
+ }
373
+
398
374
  /**
399
375
  * Stops the measurement process, collects metrics, and handles any post-measurement tasks.
400
376
  * It finalizes the URL being tested, manages any URL-specific metadata, stops any ongoing video recordings,
@@ -442,7 +418,7 @@ export class Measure {
442
418
  await this.storageManager.createSubDataDir(
443
419
  path.join(pathToFolder(url, this.options))
444
420
  );
445
- await this._stopVideo(url);
421
+ await this.measureVideo.stop(url);
446
422
 
447
423
  const alias = this.options.urlMetaData
448
424
  ? this.options.urlMetaData[url]
@@ -473,7 +449,7 @@ export class Measure {
473
449
  await this.tcpDump.stop();
474
450
  }
475
451
 
476
- return this.collect(url);
452
+ return this._collect(url);
477
453
  }
478
454
 
479
455
  /**
@@ -515,125 +491,39 @@ export class Measure {
515
491
  }
516
492
 
517
493
  /**
518
- *
519
494
  * @private
520
495
  */
521
- async collect(url) {
522
- // This stops collecting trace logs in Chrome etc
523
- // await this.engineDelegate.beforeCollect();
524
-
525
- log.verbose('Collecting metrics');
526
-
527
- // If we have an alias for the URL, use that URL
496
+ _resolveAlias(url) {
528
497
  if (
529
498
  this.result[this.numberOfMeasuredPages] &&
530
499
  this.result[this.numberOfMeasuredPages].alias
531
500
  ) {
532
501
  const alias = this.result[this.numberOfMeasuredPages].alias;
533
502
  if (aliasAndUrl[alias]) {
534
- url = aliasAndUrl[alias];
535
- } else {
536
- aliasAndUrl[alias] = url;
503
+ return aliasAndUrl[alias];
537
504
  }
505
+ aliasAndUrl[alias] = url;
538
506
  }
507
+ return url;
508
+ }
539
509
 
540
- this.result[this.numberOfMeasuredPages].url = url;
541
-
542
- // when we have the URL we can use that to stop the video and put it where we want it
543
- if (
544
- this.recordVideo &&
545
- !this.options.videoParams.debug &&
546
- this.video.getRecordingStartTime()
547
- ) {
510
+ /**
511
+ * @private
512
+ */
513
+ _attachVideoMetadata() {
514
+ const metadata = this.measureVideo.getRecordingMetadata();
515
+ if (metadata) {
548
516
  this.result[this.numberOfMeasuredPages].recordingStartTime =
549
- Number.parseFloat(this.video.getRecordingStartTime());
517
+ metadata.recordingStartTime;
550
518
  this.result[this.numberOfMeasuredPages].timeToFirstFrame =
551
- Number.parseInt(this.video.getTimeToFirstFrame(), 10);
552
- }
553
-
554
- if (this.options.screenshot) {
555
- log.info('Take after page complete check screenshot');
556
- try {
557
- const screenshot = await this.browser.takeScreenshot(url);
558
- await this.screenshotManager.save(
559
- 'afterPageCompleteCheck',
560
- screenshot,
561
- url,
562
- this.index
563
- );
564
- } catch {
565
- // not getting screenshots shouldn't result in a failed test.
566
- }
567
- }
568
-
569
- const supportLS = await this.browser.runScript(
570
- `
571
- const supported = PerformanceObserver.supportedEntryTypes;
572
- if (!supported || supported.indexOf('layout-shift') === -1) {
573
- return false;
574
- } else return true;
575
- `,
576
- 'SUPPORT_LS'
577
- );
578
-
579
- if (this.options.screenshotLS && supportLS) {
580
- log.info('Take cumulative layout shift screenshot');
581
- await this.browser.runScript(highlightLS, 'HIGHLIGHT_LS', {
582
- color: this.options.screenshotLSColor || 'red',
583
- limit: this.options.screenshotLSLimit || 0.01
584
- });
585
- const screenshot = await this.browser.takeScreenshot(url);
586
- await this.screenshotManager.save(
587
- 'layoutShift',
588
- screenshot,
589
- url,
590
- this.index
591
- );
592
- const lsScriptClean = `
593
- const c = document.getElementById("browsertime-ls");
594
- if (c) c.remove();
595
- `;
596
- await this.browser.runScript(lsScriptClean, 'CLEAN_LS');
597
- }
598
-
599
- const supportLCP = await this.browser.runScript(
600
- `
601
- const supported = PerformanceObserver.supportedEntryTypes;
602
- if (!supported || supported.indexOf('largest-contentful-paint') === -1) {
603
- return false;
604
- } else return true;
605
- `,
606
- 'SUPPORT_LCP'
607
- );
608
-
609
- if (this.options.screenshotLCP && supportLCP) {
610
- log.info('Take largest contentful paint screenshot');
611
- try {
612
- const lcpScriptClean = `
613
- const c = document.getElementById("browsertime-lcp");
614
- if (c) c.remove();
615
- `;
616
- const message = await this.browser.runScript(
617
- highlightLargestContentfulPaint,
618
- 'HIGHLIGHT_LCP',
619
- this.options.screenshotLCPColor || 'red'
620
- );
621
- if (message != '') {
622
- log.info(message);
623
- }
624
- const screenshot = await this.browser.takeScreenshot(url);
625
- await this.screenshotManager.save(
626
- 'largestContentfulPaint',
627
- screenshot,
628
- url,
629
- this.index
630
- );
631
- await this.browser.runScript(lcpScriptClean, 'CLEAN_LCP');
632
- } catch (error) {
633
- log.error('Could not get LCP screenshot', error);
634
- }
519
+ metadata.timeToFirstFrame;
635
520
  }
521
+ }
636
522
 
523
+ /**
524
+ * @private
525
+ */
526
+ async _collectBrowserScripts() {
637
527
  const syncScripts = this.scriptsByCategory
638
528
  ? await this.browser.runScripts(this.scriptsByCategory)
639
529
  : {},
@@ -646,7 +536,12 @@ export class Measure {
646
536
  syncScripts,
647
537
  asyncScripts
648
538
  );
539
+ }
649
540
 
541
+ /**
542
+ * @private
543
+ */
544
+ _filterUserTimings() {
650
545
  // Some sites has crazy amount of user timings:
651
546
  // strip them if you want
652
547
  if (this.options.userTimingAllowList) {
@@ -664,7 +559,12 @@ export class Measure {
664
559
  this.options.userTimingBlockList
665
560
  );
666
561
  }
562
+ }
667
563
 
564
+ /**
565
+ * @private
566
+ */
567
+ async _collectAndroidProcessTime() {
668
568
  if (isAndroidConfigured(this.options) && this.options.processStartTime) {
669
569
  const packageName =
670
570
  this.options.browser === 'firefox'
@@ -685,10 +585,34 @@ export class Measure {
685
585
  ].browserScripts.browser.processStartTime = processStartTime;
686
586
  }
687
587
  }
588
+ }
688
589
 
590
+ /**
591
+ * @private
592
+ */
593
+ async _runPostURLScripts() {
689
594
  for (const postURLScript of this.postURLScripts) {
690
595
  await postURLScript(this.context);
691
596
  }
597
+ }
598
+
599
+ /**
600
+ * @private
601
+ */
602
+ async _collect(url) {
603
+ log.verbose('Collecting metrics');
604
+
605
+ url = this._resolveAlias(url);
606
+ this.result[this.numberOfMeasuredPages].url = url;
607
+
608
+ this._attachVideoMetadata();
609
+ await this.screenshots.afterPageCompleteCheck(url, this.index);
610
+ await this.screenshots.layoutShift(url, this.index);
611
+ await this.screenshots.largestContentfulPaint(url, this.index);
612
+ await this._collectBrowserScripts();
613
+ this._filterUserTimings();
614
+ await this._collectAndroidProcessTime();
615
+ await this._runPostURLScripts();
692
616
 
693
617
  if (this.options.tcpdump) {
694
618
  await this.tcpDump.mv(url, this.index);
@@ -117,7 +117,8 @@ export class ClickAndHold {
117
117
  async releaseAtXpath(xpath) {
118
118
  try {
119
119
  const element = await this.driver.findElement(By.xpath(xpath));
120
- return this.actions.move({ origin: element }).release().perform();
120
+ await this.actions.move({ origin: element }).release().perform();
121
+ return this.actions.clear();
121
122
  } catch (error) {
122
123
  log.error('Could not release on xpath %s', xpath);
123
124
  log.verbose(error);
@@ -136,7 +137,8 @@ export class ClickAndHold {
136
137
  async releaseAtSelector(selector) {
137
138
  try {
138
139
  const element = await this.driver.findElement(By.css(selector));
139
- return this.actions.move({ origin: element }).release().perform();
140
+ await this.actions.move({ origin: element }).release().perform();
141
+ return this.actions.clear();
140
142
  } catch (error) {
141
143
  log.error('Could not release on selector %s', selector);
142
144
  log.verbose(error);
@@ -155,10 +157,11 @@ export class ClickAndHold {
155
157
  */
156
158
  async releaseAtPosition(xPos, yPos) {
157
159
  try {
158
- return this.actions
160
+ await this.actions
159
161
  .move({ x: xPos, y: yPos, origin: Origin.VIEWPORT })
160
162
  .release()
161
163
  .perform();
164
+ return this.actions.clear();
162
165
  } catch (error) {
163
166
  log.error('Could not release at position (%d,%d)', xPos, yPos);
164
167
  log.verbose(error);
@@ -1,5 +1,8 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
2
  import { By } from 'selenium-webdriver';
3
+ import webdriver from 'selenium-webdriver';
4
+ import { executeCommand } from '../commandHelper.js';
5
+ import { parseSelector } from '../selectorParser.js';
3
6
  const log = getLogger('browsertime.command.mouse');
4
7
 
5
8
  /**
@@ -9,7 +12,11 @@ const log = getLogger('browsertime.command.mouse');
9
12
  * @hideconstructor
10
13
  */
11
14
  export class ContextClick {
12
- constructor(browser) {
15
+ constructor(browser, options) {
16
+ /**
17
+ * @private
18
+ */
19
+ this.browser = browser;
13
20
  /**
14
21
  * @private
15
22
  */
@@ -18,9 +25,49 @@ export class ContextClick {
18
25
  * @private
19
26
  */
20
27
  this.actions = this.driver.actions({ async: true });
28
+ /**
29
+ * @private
30
+ */
31
+ this.options = options;
32
+ }
33
+
34
+ /**
35
+ * @private
36
+ */
37
+ async _waitForElement(driver, locator) {
38
+ const timeout = this.options?.timeouts?.elementWait ?? 0;
39
+ if (timeout > 0) {
40
+ await driver.wait(webdriver.until.elementLocated(locator), timeout);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Performs a context click (right-click) on an element using a unified selector string.
46
+ *
47
+ * @async
48
+ * @param {string} selector - The selector string. CSS by default, or use a prefix.
49
+ * @returns {Promise<void>}
50
+ * @throws {Error} Throws an error if the element is not found.
51
+ */
52
+ async run(selector) {
53
+ const { locator, description } = parseSelector(selector);
54
+ return executeCommand(
55
+ log,
56
+ 'Could not context click on element %s',
57
+ description,
58
+ async () => {
59
+ const driver = this.browser.getDriver();
60
+ await this._waitForElement(driver, locator);
61
+ const element = await driver.findElement(locator);
62
+ await this.actions.contextClick(element).perform();
63
+ await this.actions.clear();
64
+ },
65
+ this.browser
66
+ );
21
67
  }
22
68
 
23
69
  /**
70
+ * @private
24
71
  * Performs a context click (right-click) on an element that matches a given XPath selector.
25
72
  *
26
73
  * @async
@@ -31,7 +78,8 @@ export class ContextClick {
31
78
  async byXpath(xpath) {
32
79
  try {
33
80
  const element = await this.driver.findElement(By.xpath(xpath));
34
- return this.actions.contextClick(element).perform();
81
+ await this.actions.contextClick(element).perform();
82
+ return this.actions.clear();
35
83
  } catch (error) {
36
84
  log.error('Could not context click on element with xpath %s', xpath);
37
85
  log.verbose(error);
@@ -40,6 +88,7 @@ export class ContextClick {
40
88
  }
41
89
 
42
90
  /**
91
+ * @private
43
92
  * Performs a context click (right-click) on an element that matches a given CSS selector.
44
93
  *
45
94
  * @async
@@ -50,7 +99,8 @@ export class ContextClick {
50
99
  async bySelector(selector) {
51
100
  try {
52
101
  const element = await this.driver.findElement(By.css(selector));
53
- return this.actions.contextClick(element).perform();
102
+ await this.actions.contextClick(element).perform();
103
+ return this.actions.clear();
54
104
  } catch (error) {
55
105
  log.error('Could not context click on element with css %s', selector);
56
106
  log.verbose(error);
@@ -61,6 +111,7 @@ export class ContextClick {
61
111
  }
62
112
 
63
113
  /**
114
+ * @private
64
115
  * Performs a context click (right-click) at the current cursor position.
65
116
  *
66
117
  * @async
@@ -69,7 +120,8 @@ export class ContextClick {
69
120
  */
70
121
  async atCursor() {
71
122
  try {
72
- return this.actions.contextClick().perform();
123
+ await this.actions.contextClick().perform();
124
+ return this.actions.clear();
73
125
  } catch (error) {
74
126
  log.error('Could not perform context click');
75
127
  log.verbose(error);
@@ -1,5 +1,8 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
2
  import { By } from 'selenium-webdriver';
3
+ import webdriver from 'selenium-webdriver';
4
+ import { executeCommand } from '../commandHelper.js';
5
+ import { parseSelector } from '../selectorParser.js';
3
6
  const log = getLogger('browsertime.command.mouse');
4
7
  /**
5
8
  * Provides functionality to perform a double-click action on elements in a web page.
@@ -8,7 +11,7 @@ const log = getLogger('browsertime.command.mouse');
8
11
  * @hideconstructor
9
12
  */
10
13
  export class DoubleClick {
11
- constructor(browser, pageCompleteCheck) {
14
+ constructor(browser, pageCompleteCheck, options) {
12
15
  /**
13
16
  * @private
14
17
  */
@@ -21,9 +24,49 @@ export class DoubleClick {
21
24
  * @private
22
25
  */
23
26
  this.pageCompleteCheck = pageCompleteCheck;
27
+ /**
28
+ * @private
29
+ */
30
+ this.options = options;
31
+ }
32
+
33
+ /**
34
+ * @private
35
+ */
36
+ async _waitForElement(driver, locator) {
37
+ const timeout = this.options?.timeouts?.elementWait ?? 0;
38
+ if (timeout > 0) {
39
+ await driver.wait(webdriver.until.elementLocated(locator), timeout);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Performs a double click on an element using a unified selector string.
45
+ *
46
+ * @async
47
+ * @param {string} selector - The selector string. CSS by default, or use a prefix.
48
+ * @returns {Promise<void>}
49
+ * @throws {Error} Throws an error if the element is not found.
50
+ */
51
+ async run(selector) {
52
+ const { locator, description } = parseSelector(selector);
53
+ return executeCommand(
54
+ log,
55
+ 'Could not double click on element %s',
56
+ description,
57
+ async () => {
58
+ const driver = this.browser.getDriver();
59
+ await this._waitForElement(driver, locator);
60
+ const element = await driver.findElement(locator);
61
+ await this.actions.doubleClick(element).perform();
62
+ await this.actions.clear();
63
+ },
64
+ this.browser
65
+ );
24
66
  }
25
67
 
26
68
  /**
69
+ * @private
27
70
  * Performs a mouse double-click on an element matching a given XPath selector.
28
71
  *
29
72
  * @async
@@ -38,6 +81,7 @@ export class DoubleClick {
38
81
  .getDriver()
39
82
  .findElement(By.xpath(xpath));
40
83
  await this.actions.doubleClick(element).perform();
84
+ await this.actions.clear();
41
85
  if (options && 'wait' in options && options.wait === true) {
42
86
  return this.browser.extraWait(this.pageCompleteCheck);
43
87
  }
@@ -49,6 +93,7 @@ export class DoubleClick {
49
93
  }
50
94
 
51
95
  /**
96
+ * @private
52
97
  * Performs a mouse double-click on an element matching a given CSS selector.
53
98
  *
54
99
  * @async
@@ -63,6 +108,7 @@ export class DoubleClick {
63
108
  .getDriver()
64
109
  .findElement(By.css(selector));
65
110
  await this.actions.doubleClick(element).perform();
111
+ await this.actions.clear();
66
112
  if (options && 'wait' in options && options.wait === true) {
67
113
  return this.browser.extraWait(this.pageCompleteCheck);
68
114
  }
@@ -76,6 +122,7 @@ export class DoubleClick {
76
122
  }
77
123
 
78
124
  /**
125
+ * @private
79
126
  * Performs a mouse double-click at the current cursor position.
80
127
  *
81
128
  * @async
@@ -86,6 +133,7 @@ export class DoubleClick {
86
133
  async atCursor(options) {
87
134
  try {
88
135
  await this.actions.doubleClick().perform();
136
+ await this.actions.clear();
89
137
  if (options && 'wait' in options && options.wait === true) {
90
138
  return this.browser.extraWait(this.pageCompleteCheck);
91
139
  }