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
@@ -0,0 +1,85 @@
1
+ import { getLogger } from '@sitespeed.io/log';
2
+ const log = getLogger('browsertime.command.cookie');
3
+
4
+ /**
5
+ * Provides functionality to manage browser cookies.
6
+ *
7
+ * @class
8
+ * @hideconstructor
9
+ */
10
+ export class Cookie {
11
+ constructor(browser) {
12
+ /**
13
+ * @private
14
+ */
15
+ this.driver = browser.getDriver();
16
+ }
17
+
18
+ /**
19
+ * Gets all cookies for the current page.
20
+ *
21
+ * @async
22
+ * @returns {Promise<Array>} An array of cookie objects.
23
+ */
24
+ async getAll() {
25
+ return this.driver.manage().getCookies();
26
+ }
27
+
28
+ /**
29
+ * Gets a specific cookie by name.
30
+ *
31
+ * @async
32
+ * @param {string} name - The name of the cookie.
33
+ * @returns {Promise<Object|undefined>} The cookie object, or undefined if not found.
34
+ */
35
+ async get(name) {
36
+ try {
37
+ return await this.driver.manage().getCookie(name);
38
+ } catch {
39
+ // Cookie not found
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Sets a cookie.
45
+ *
46
+ * @async
47
+ * @param {string} name - The name of the cookie.
48
+ * @param {string} value - The value of the cookie.
49
+ * @param {Object} [options] - Optional cookie properties.
50
+ * @param {string} [options.domain] - The domain the cookie is visible to.
51
+ * @param {string} [options.path] - The cookie path.
52
+ * @param {boolean} [options.secure] - Whether the cookie is secure.
53
+ * @param {boolean} [options.httpOnly] - Whether the cookie is HTTP only.
54
+ * @param {Date} [options.expiry] - When the cookie expires.
55
+ * @returns {Promise<void>}
56
+ */
57
+ async set(name, value, options = {}) {
58
+ const cookie = { name, value, ...options };
59
+ log.debug('Setting cookie %s', name);
60
+ return this.driver.manage().addCookie(cookie);
61
+ }
62
+
63
+ /**
64
+ * Deletes a specific cookie by name.
65
+ *
66
+ * @async
67
+ * @param {string} name - The name of the cookie to delete.
68
+ * @returns {Promise<void>}
69
+ */
70
+ async delete(name) {
71
+ log.debug('Deleting cookie %s', name);
72
+ return this.driver.manage().deleteCookie(name);
73
+ }
74
+
75
+ /**
76
+ * Deletes all cookies for the current page.
77
+ *
78
+ * @async
79
+ * @returns {Promise<void>}
80
+ */
81
+ async deleteAll() {
82
+ log.debug('Deleting all cookies');
83
+ return this.driver.manage().deleteAllCookies();
84
+ }
85
+ }
@@ -1,17 +1,21 @@
1
1
  // We disable these because they are needed for code completion
2
2
  /* eslint no-unused-vars: "off" */
3
- import { By, WebElement } from 'selenium-webdriver';
3
+ import webdriver, { By, WebElement } from 'selenium-webdriver';
4
4
  /**
5
5
  * This class provides a way to get hokld of Seleniums WebElements.
6
6
  * @class
7
7
  * @hideconstructor
8
8
  */
9
9
  export class Element {
10
- constructor(browser) {
10
+ constructor(browser, options) {
11
11
  /**
12
12
  * @private
13
13
  */
14
14
  this.driver = browser.getDriver();
15
+ /**
16
+ * @private
17
+ */
18
+ this.options = options;
15
19
  }
16
20
 
17
21
  /**
@@ -63,4 +67,35 @@ export class Element {
63
67
  async getByName(name) {
64
68
  return this.driver.findElement(By.name(name));
65
69
  }
70
+
71
+ /**
72
+ * Finds an element using a CSS selector, with optional waiting and visibility check.
73
+ *
74
+ * @param {string} selector - The CSS selector of the element.
75
+ * @param {Object} [options] - Options for finding the element.
76
+ * @param {number} [options.timeout] - Maximum time in milliseconds to wait for the element. Defaults to the configured --timeouts.elementWait value.
77
+ * @param {boolean} [options.visible=false] - If true, waits for the element to be visible, not just present.
78
+ * @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
79
+ * @throws {Error} Throws an error if the element is not found within the timeout.
80
+ */
81
+ async find(selector, options = {}) {
82
+ const timeout = options.timeout ?? this.options?.timeouts?.elementWait ?? 0;
83
+ const visible = options.visible ?? false;
84
+ const locator = By.css(selector);
85
+
86
+ if (timeout > 0) {
87
+ const element = await this.driver.wait(
88
+ webdriver.until.elementLocated(locator),
89
+ timeout
90
+ );
91
+ if (visible) {
92
+ await this.driver.wait(
93
+ webdriver.until.elementIsVisible(element),
94
+ timeout
95
+ );
96
+ }
97
+ return element;
98
+ }
99
+ return this.driver.findElement(locator);
100
+ }
66
101
  }
@@ -1,4 +1,5 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
+ import { executeCommand } from './commandHelper.js';
2
3
  const log = getLogger('browsertime.command.javascript');
3
4
  /**
4
5
  * Provides functionality to execute JavaScript code in the context of the current page.
@@ -19,6 +20,14 @@ export class JavaScript {
19
20
  this.pageCompleteCheck = pageCompleteCheck;
20
21
  }
21
22
 
23
+ /**
24
+ * @private
25
+ */
26
+ async _andWait(baseMethod, ...args) {
27
+ await baseMethod.apply(this, args);
28
+ return this.browser.extraWait(this.pageCompleteCheck);
29
+ }
30
+
22
31
  /**
23
32
  * Executes a JavaScript script.
24
33
  *
@@ -28,14 +37,9 @@ export class JavaScript {
28
37
  * @throws {Error} Throws an error if the JavaScript cannot be executed.
29
38
  */
30
39
  async run(js) {
31
- try {
32
- const value = await this.browser.runScript(js, 'CUSTOM');
33
- return value;
34
- } catch (error) {
35
- log.error('Could not run JavaScript %s ', js);
36
- log.verbose(error);
37
- throw new Error(`Could not run JavaScript ${js}`);
38
- }
40
+ return executeCommand(log, 'Could not run JavaScript %s', js, () =>
41
+ this.browser.runScript(js, 'CUSTOM')
42
+ );
39
43
  }
40
44
 
41
45
  /**
@@ -47,14 +51,7 @@ export class JavaScript {
47
51
  * @throws {Error} Throws an error if the JavaScript cannot be executed.
48
52
  */
49
53
  async runAndWait(js) {
50
- try {
51
- await this.browser.runScript(js, 'CUSTOM');
52
- return this.browser.extraWait(this.pageCompleteCheck);
53
- } catch (error) {
54
- log.error('Could not run JavaScript %s ', js);
55
- log.verbose(error);
56
- throw new Error(`Could not run JavaScript ${js}`);
57
- }
54
+ return this._andWait(this.run, js);
58
55
  }
59
56
 
60
57
  /**
@@ -88,14 +85,7 @@ export class JavaScript {
88
85
  * @throws {Error} Throws an error if the privileged JavaScript cannot be executed.
89
86
  */
90
87
  async runPrivilegedAndWait(js) {
91
- try {
92
- await this.browser.runPrivilegedScript(js, 'CUSTOM PRIVILEGED');
93
- return this.browser.extraWait(this.pageCompleteCheck);
94
- } catch (error) {
95
- log.error('Could not run privileged JavaScript %s ', js);
96
- log.verbose(error);
97
- throw error;
98
- }
88
+ return this._andWait(this.runPrivileged, js);
99
89
  }
100
90
 
101
91
  /**
@@ -0,0 +1,114 @@
1
+ import { getLogger } from '@sitespeed.io/log';
2
+ import { lcpHighlightScript as highlightLargestContentfulPaint } from '../util/lcpHighlightScript.js';
3
+ import { clsHighlightScript as highlightLS } from '../util/lsHighlightScript.js';
4
+ const log = getLogger('browsertime.command.measure');
5
+
6
+ /**
7
+ * Handles screenshot capture during measurement collection.
8
+ * @class
9
+ * @private
10
+ */
11
+ export class MeasureScreenshots {
12
+ constructor(browser, screenshotManager, options) {
13
+ this.browser = browser;
14
+ this.screenshotManager = screenshotManager;
15
+ this.options = options;
16
+ }
17
+
18
+ async afterPageCompleteCheck(url, index) {
19
+ if (!this.options.screenshot) {
20
+ return;
21
+ }
22
+ log.info('Take after page complete check screenshot');
23
+ try {
24
+ const screenshot = await this.browser.takeScreenshot(url);
25
+ await this.screenshotManager.save(
26
+ 'afterPageCompleteCheck',
27
+ screenshot,
28
+ url,
29
+ index
30
+ );
31
+ } catch {
32
+ // not getting screenshots shouldn't result in a failed test.
33
+ }
34
+ }
35
+
36
+ async layoutShift(url, index) {
37
+ if (!this.options.screenshotLS) {
38
+ return;
39
+ }
40
+
41
+ const supportLS = await this.browser.runScript(
42
+ `
43
+ const supported = PerformanceObserver.supportedEntryTypes;
44
+ if (!supported || supported.indexOf('layout-shift') === -1) {
45
+ return false;
46
+ } else return true;
47
+ `,
48
+ 'SUPPORT_LS'
49
+ );
50
+
51
+ if (!supportLS) {
52
+ return;
53
+ }
54
+
55
+ log.info('Take cumulative layout shift screenshot');
56
+ await this.browser.runScript(highlightLS, 'HIGHLIGHT_LS', {
57
+ color: this.options.screenshotLSColor || 'red',
58
+ limit: this.options.screenshotLSLimit || 0.01
59
+ });
60
+ const screenshot = await this.browser.takeScreenshot(url);
61
+ await this.screenshotManager.save('layoutShift', screenshot, url, index);
62
+ const lsScriptClean = `
63
+ const c = document.getElementById("browsertime-ls");
64
+ if (c) c.remove();
65
+ `;
66
+ await this.browser.runScript(lsScriptClean, 'CLEAN_LS');
67
+ }
68
+
69
+ async largestContentfulPaint(url, index) {
70
+ if (!this.options.screenshotLCP) {
71
+ return;
72
+ }
73
+
74
+ const supportLCP = await this.browser.runScript(
75
+ `
76
+ const supported = PerformanceObserver.supportedEntryTypes;
77
+ if (!supported || supported.indexOf('largest-contentful-paint') === -1) {
78
+ return false;
79
+ } else return true;
80
+ `,
81
+ 'SUPPORT_LCP'
82
+ );
83
+
84
+ if (!supportLCP) {
85
+ return;
86
+ }
87
+
88
+ log.info('Take largest contentful paint screenshot');
89
+ try {
90
+ const lcpScriptClean = `
91
+ const c = document.getElementById("browsertime-lcp");
92
+ if (c) c.remove();
93
+ `;
94
+ const message = await this.browser.runScript(
95
+ highlightLargestContentfulPaint,
96
+ 'HIGHLIGHT_LCP',
97
+ this.options.screenshotLCPColor || 'red'
98
+ );
99
+ if (message != '') {
100
+ log.info(message);
101
+ }
102
+ const screenshot = await this.browser.takeScreenshot(url);
103
+ await this.screenshotManager.save(
104
+ 'largestContentfulPaint',
105
+ screenshot,
106
+ url,
107
+ index
108
+ );
109
+ await this.browser.runScript(lcpScriptClean, 'CLEAN_LCP');
110
+ } catch (error) {
111
+ log.error('Could not get LCP screenshot', error);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,85 @@
1
+ import { Video } from '../../../../video/video.js';
2
+ import { setOrangeBackground } from '../../../../video/screenRecording/setOrangeBackground.js';
3
+ import { isAndroidConfigured } from '../../../../android/index.js';
4
+ import { getProperty } from '../../../../support/util.js';
5
+ const delay = ms => new Promise(res => setTimeout(res, ms));
6
+
7
+ /**
8
+ * Handles video recording during measurement.
9
+ * @class
10
+ * @private
11
+ */
12
+ export class MeasureVideo {
13
+ constructor(storageManager, browser, videos, options) {
14
+ this.storageManager = storageManager;
15
+ this.browser = browser;
16
+ this.videos = videos;
17
+ this.options = options;
18
+ this.recordVideo = options.visualMetrics || options.video;
19
+ this.ANDROID_DELAY_TIME = getProperty(
20
+ options,
21
+ 'orangeAndroidDelayTime',
22
+ 2000
23
+ );
24
+ this.IOS_DELAY_TIME = getProperty(options, 'orangeIosDelayTime', 1000);
25
+ this.DESKTOP_DELAY_TIME = getProperty(
26
+ options,
27
+ 'orangeDesktopDelayTime',
28
+ 800
29
+ );
30
+ }
31
+
32
+ async start(numberOfMeasuredPages, index) {
33
+ this.video = new Video(this.storageManager, this.options, this.browser);
34
+
35
+ if (this.options.firefox && this.options.firefox.windowRecorder) {
36
+ // The Firefox window recorder will only record subsequent
37
+ // changes after recording has begun. So the orange frame needs
38
+ // to come after we start recording.
39
+ await this.video.record(numberOfMeasuredPages, index);
40
+ await setOrangeBackground(this.browser.getDriver(), this.options);
41
+ } else {
42
+ await setOrangeBackground(this.browser.getDriver(), this.options);
43
+ await this.video.record(numberOfMeasuredPages, index);
44
+ }
45
+
46
+ // Give ffmpeg/video on phone time to settle
47
+ if (isAndroidConfigured(this.options)) {
48
+ return delay(this.ANDROID_DELAY_TIME);
49
+ } else if (this.options.safari && this.options.safari.ios) {
50
+ return delay(this.IOS_DELAY_TIME);
51
+ } else if (this.options.safari && this.options.safari.useSimulator) {
52
+ return delay(this.IOS_DELAY_TIME);
53
+ } else {
54
+ return delay(this.DESKTOP_DELAY_TIME);
55
+ }
56
+ }
57
+
58
+ async stop(url) {
59
+ if (this.recordVideo && !this.options.videoParams.debug) {
60
+ await this.video.stop(url);
61
+ this.videos.push(this.video);
62
+ }
63
+ }
64
+
65
+ getRecordingMetadata() {
66
+ if (
67
+ this.recordVideo &&
68
+ !this.options.videoParams.debug &&
69
+ this.video &&
70
+ this.video.getRecordingStartTime()
71
+ ) {
72
+ return {
73
+ recordingStartTime: Number.parseFloat(
74
+ this.video.getRecordingStartTime()
75
+ ),
76
+ timeToFirstFrame: Number.parseInt(this.video.getTimeToFirstFrame(), 10)
77
+ };
78
+ }
79
+ return;
80
+ }
81
+
82
+ shouldRecord() {
83
+ return this.recordVideo && !this.options.videoParams.debug;
84
+ }
85
+ }