browsertime 27.2.0 → 27.4.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.
- package/CHANGELOG.md +20 -0
- package/bin/browsertime.js +31 -5
- package/bin/browsertimeWebPageReplay.js +1 -1
- package/lib/chrome/coverage.js +198 -0
- package/lib/chrome/webdriver/chromium.js +14 -0
- package/lib/core/engine/collector.js +8 -0
- package/lib/core/engine/command/measure.js +4 -5
- package/lib/core/engine/commands.js +14 -26
- package/lib/core/engine/context.js +4 -9
- package/lib/core/engine/index.js +6 -2
- package/lib/core/seleniumRunner.js +1 -2
- package/lib/safari/safariInspectorClient.js +0 -1
- package/lib/screenshot/index.js +1 -1
- package/lib/support/browserScript.js +2 -4
- package/lib/support/cli.js +56 -21
- package/lib/support/har/index.js +6 -3
- package/lib/support/helpTopics.js +174 -0
- package/lib/support/util.js +46 -0
- package/lib/video/postprocessing/finetune/addTextToVideo.js +1 -1
- package/package.json +16 -15
- package/scripting.d.ts +21 -0
- package/types/android/index.d.ts +2 -2
- package/types/android/index.d.ts.map +1 -1
- package/types/core/engine/command/measure.d.ts +6 -6
- package/types/core/engine/command/measure.d.ts.map +1 -1
- package/types/core/engine/command/simpleperf.d.ts +1 -1
- package/types/core/engine/command/simpleperf.d.ts.map +1 -1
- package/types/core/engine/commands.d.ts +51 -48
- package/types/core/engine/commands.d.ts.map +1 -1
- package/types/core/engine/context.d.ts +9 -8
- package/types/core/engine/context.d.ts.map +1 -1
- package/types/support/tcpdump.d.ts +1 -1
- package/types/support/tcpdump.d.ts.map +1 -1
- package/types/support/util.d.ts +5 -0
- package/types/support/util.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/addTextToVideo.d.ts +1 -1
- package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/convertFps.d.ts +1 -1
- package/types/video/postprocessing/finetune/convertFps.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/removeOrange.d.ts +1 -1
- package/types/video/postprocessing/finetune/removeOrange.d.ts.map +1 -1
- package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts +1 -1
- package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts.map +1 -1
- package/types/video/screenRecording/desktop/convert.d.ts +1 -1
- package/types/video/screenRecording/desktop/convert.d.ts.map +1 -1
- package/types/video/screenRecording/desktop/desktopRecorder.d.ts +2 -2
- package/types/video/screenRecording/desktop/ffmpegRecorder.d.ts +1 -1
- package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts +1 -1
- package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts.map +1 -1
- package/types/video/screenRecording/desktop/osx/getScreen.d.ts +1 -1
- package/types/video/screenRecording/desktop/osx/getScreen.d.ts.map +1 -1
- package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts +1 -1
- package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts.map +1 -1
- package/types/video/screenRecording/iosSimulator/recorder.d.ts +2 -6
- package/types/video/screenRecording/iosSimulator/recorder.d.ts.map +1 -1
- package/types/scripting.d.ts +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 27.4.0 - 2026-05-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
* JS/CSS coverage collection for Chrome. `--chrome.coverage` turns it on for every iteration for users who want the data and accept that detailed V8 coverage deoptimizes scripts and will skew their timing metrics. `--enableProfileRun` also turns coverage on for Chrome alongside the existing trace, so users who want coverage without affecting their timings can lean on the same extra-iteration pattern they already use for tracing — the result is merged back into the main `browsertime.json` so consumers like sitespeed.io see the data in the same place they read every other metric. Each entry has a per-file breakdown (`url`, `totalBytes`, `usedBytes`, `unusedBytes`, `unusedPercent`) [#2480](https://github.com/sitespeedio/browsertime/pull/2480).
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
* HAR post-processing no longer throws when a page has no matching entries. Occasionally a HAR contains a page whose `pageref` has no entries — typically a navigation that didn't produce any requests, or an extension-injected page. `getDocumentRequests` and `getFinalURL` dereferenced `shift()` / `pop()` without a guard, and `getMainDocumentTimings` wrapped its entire per-page loop in a single try/catch so one bad page wiped out the main-document timings for every other page in the same HAR. Downstream this surfaced as a noisy triplet of warnings followed by an empty coach-data payload in sitespeed.io. The three crash points are now guarded and unresolvable pages are skipped [#2483](https://github.com/sitespeedio/browsertime/pull/2483).
|
|
10
|
+
* Bumped `geckodriver` to a version with native ARM-64 support [#2482](https://github.com/sitespeedio/browsertime/pull/2482).
|
|
11
|
+
|
|
12
|
+
## 27.3.0 - 2026-05-20
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
* `browsertime --help` is now navigable by topic. The default view shows a short curated list of common options plus the available topic names. `browsertime --help <topic>` shows just that topic, and `browsertime --help-all` reproduces the historical full dump for scripts and power users. The old single-screen dump of every option from all 13 topic groups was overwhelming for new users and slow to skim; the topic-filtered view follows the curl/ffmpeg pattern [#2467](https://github.com/sitespeedio/browsertime/pull/2467).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
* TypeScript types and IntelliSense for navigation scripts are tightened across the script-facing surface. A new `BrowsertimeScript` type alias lets scripts annotate their default export and get both `context` and `commands` typed in one line. The high-level command helpers (`navigate`, `find`, `getText`, `fill`, `hover`, `press`, `getTitle`, `waitForUrl`, …) now surface their JSDoc parameter and return types in IntelliSense instead of being typed as `Function`, and `commands.find` resolves to a real `WebElement` so chained calls get completion from selenium-webdriver [#2476](https://github.com/sitespeedio/browsertime/pull/2476). `context.options`, `context.result`, and `context.taskData` are typed as `Record<string, any>` so editors offer completion on `context.options.` etc. [#2478](https://github.com/sitespeedio/browsertime/pull/2478). `commands.mouse` is now a proper shape (selector typed as `string`, `singleClick`'s options surface `waitForNavigation`) instead of an anonymous object literal, and `measure.stop` / `measure.clickAndMeasure` / `measure.stopAsError` return types are explicit (`Promise<void>` / `Promise<unknown>`) instead of `Promise<any>` [#2479](https://github.com/sitespeedio/browsertime/pull/2479).
|
|
19
|
+
* How Browsertime publishes its TypeScript types is now safer. `scripting.d.ts` moved to the repo root next to `package.json` so it can't be clobbered by a future `tsc` outDir change, the `types` condition comes first under `exports` to match TypeScript's documented resolution rules, and `publint` runs as part of `prepublishOnly` so common package.json mistakes fail before a release goes out [#2477](https://github.com/sitespeedio/browsertime/pull/2477).
|
|
20
|
+
* Replaced the direct `lodash.merge` dependency with a small in-tree `merge()` helper in `lib/support/util.js`. The helper matches lodash's actual semantics for the cases Browsertime relies on — arrays merged by index, `undefined` source values skipped unless the key is missing on the target, `Date`/`RegExp`/class instances passed by reference, prototype-pollution keys rejected — which finishes the lodash unbundling the same way sitespeed.io did [#2468](https://github.com/sitespeedio/browsertime/pull/2468).
|
|
21
|
+
* Bumped dependencies: `chrome-har` 1.3.1 [#2469](https://github.com/sitespeedio/browsertime/pull/2469), `chrome-remote-interface` and `selenium-webdriver` [#2475](https://github.com/sitespeedio/browsertime/pull/2475), `@sitespeed.io/log` 2.0 [#2471](https://github.com/sitespeedio/browsertime/pull/2471), `chromedriver` / `edgedriver` / `geckodriver` [#2472](https://github.com/sitespeedio/browsertime/pull/2472).
|
|
22
|
+
|
|
3
23
|
## 27.2.0 - 2026-05-12
|
|
4
24
|
|
|
5
25
|
### Added
|
package/bin/browsertime.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import merge from 'lodash.merge';
|
|
3
2
|
import { getLogger } from '@sitespeed.io/log';
|
|
4
3
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
4
|
import path from 'node:path';
|
|
@@ -13,6 +12,7 @@ import { configure } from '../lib/support/logging.js';
|
|
|
13
12
|
import { parseCommandLine } from '../lib/support/cli.js';
|
|
14
13
|
import { StorageManager } from '../lib/support/storageManager.js';
|
|
15
14
|
import { loadScript } from '../lib/support/engineUtils.js';
|
|
15
|
+
import { merge } from '../lib/support/util.js';
|
|
16
16
|
import { setProperty, getProperty } from '../lib/support/util.js';
|
|
17
17
|
import { isAndroidConfigured } from '../lib/android/index.js';
|
|
18
18
|
|
|
@@ -95,12 +95,16 @@ async function run(urls, options) {
|
|
|
95
95
|
scriptsByCategory = merge(scriptsByCategory, userScripts);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
let result;
|
|
99
|
+
let storageManager;
|
|
100
|
+
let jsonName;
|
|
101
|
+
|
|
98
102
|
try {
|
|
99
103
|
if (options.preWarmServer) {
|
|
100
104
|
await preWarmServer(urls, options);
|
|
101
105
|
}
|
|
102
106
|
await engine.start();
|
|
103
|
-
|
|
107
|
+
result = await engine.runMultiple(urls, scriptsByCategory);
|
|
104
108
|
let saveOperations = [];
|
|
105
109
|
|
|
106
110
|
// TODO setup by name
|
|
@@ -109,9 +113,9 @@ async function run(urls, options) {
|
|
|
109
113
|
if (Array.isArray(firstUrl)) {
|
|
110
114
|
firstUrl = firstUrl[0];
|
|
111
115
|
}
|
|
112
|
-
|
|
116
|
+
storageManager = new StorageManager(firstUrl, options);
|
|
113
117
|
const harName = options.har ?? 'browsertime';
|
|
114
|
-
|
|
118
|
+
jsonName = options.output ?? 'browsertime';
|
|
115
119
|
|
|
116
120
|
saveOperations.push(storageManager.writeJson(jsonName + '.json', result));
|
|
117
121
|
|
|
@@ -169,6 +173,7 @@ async function run(urls, options) {
|
|
|
169
173
|
options.chrome.traceCategory = [
|
|
170
174
|
'disabled-by-default-v8.cpu_profiler'
|
|
171
175
|
];
|
|
176
|
+
options.chrome.coverage = true;
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
if (options.enableVideoRun) {
|
|
@@ -186,9 +191,30 @@ async function run(urls, options) {
|
|
|
186
191
|
}
|
|
187
192
|
const traceEngine = new Engine(options);
|
|
188
193
|
await traceEngine.start();
|
|
189
|
-
await traceEngine.runMultiple(
|
|
194
|
+
const profileResult = await traceEngine.runMultiple(
|
|
195
|
+
urls,
|
|
196
|
+
scriptsByCategory
|
|
197
|
+
);
|
|
190
198
|
await traceEngine.stop();
|
|
191
199
|
log.info('Extra run finished');
|
|
200
|
+
|
|
201
|
+
// Fill the main JSON's coverage from the profile run only when
|
|
202
|
+
// the main iterations didn't collect any (--enableProfileRun
|
|
203
|
+
// without --chrome.coverage). Otherwise the main run's
|
|
204
|
+
// per-iteration samples are what the user asked for.
|
|
205
|
+
let mergedCoverage = false;
|
|
206
|
+
for (const [i, pr] of profileResult.entries()) {
|
|
207
|
+
const main = result[i];
|
|
208
|
+
if (!main || !pr.coverage) continue;
|
|
209
|
+
if (main.coverage.js.length > 0 || main.coverage.css.length > 0) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
main.coverage = pr.coverage;
|
|
213
|
+
mergedCoverage = true;
|
|
214
|
+
}
|
|
215
|
+
if (mergedCoverage) {
|
|
216
|
+
await storageManager.writeJson(jsonName + '.json', result);
|
|
217
|
+
}
|
|
192
218
|
}
|
|
193
219
|
} catch (error) {
|
|
194
220
|
log.error('Error running browsertime', error);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import merge from 'lodash.merge';
|
|
3
2
|
import yargs from 'yargs';
|
|
4
3
|
import { hideBin } from 'yargs/helpers';
|
|
5
4
|
|
|
6
5
|
import { Engine } from '../lib/core/engine/index.js';
|
|
7
6
|
import { configure as logging } from '../lib/support/logging.js';
|
|
7
|
+
import { merge } from '../lib/support/util.js';
|
|
8
8
|
|
|
9
9
|
async function runBrowsertime() {
|
|
10
10
|
let yargsInstance = yargs(hideBin(process.argv));
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { getLogger } from '@sitespeed.io/log';
|
|
2
|
+
|
|
3
|
+
const log = getLogger('browsertime.chrome.coverage');
|
|
4
|
+
|
|
5
|
+
// Sum the union length of half-open [start, end) ranges.
|
|
6
|
+
function unionLength(ranges) {
|
|
7
|
+
if (ranges.length === 0) return 0;
|
|
8
|
+
const sorted = ranges
|
|
9
|
+
.map(r => [r.startOffset, r.endOffset])
|
|
10
|
+
.toSorted((a, b) => a[0] - b[0]);
|
|
11
|
+
let total = 0;
|
|
12
|
+
let [curStart, curEnd] = sorted[0];
|
|
13
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
14
|
+
const [s, e] = sorted[i];
|
|
15
|
+
if (s <= curEnd) {
|
|
16
|
+
if (e > curEnd) curEnd = e;
|
|
17
|
+
} else {
|
|
18
|
+
total += curEnd - curStart;
|
|
19
|
+
curStart = s;
|
|
20
|
+
curEnd = e;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
total += curEnd - curStart;
|
|
24
|
+
return total;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function summarize(files) {
|
|
28
|
+
let totalBytes = 0;
|
|
29
|
+
let usedBytes = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
totalBytes += file.totalBytes;
|
|
32
|
+
usedBytes += file.usedBytes;
|
|
33
|
+
}
|
|
34
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
35
|
+
return {
|
|
36
|
+
totalBytes,
|
|
37
|
+
usedBytes,
|
|
38
|
+
unusedBytes,
|
|
39
|
+
unusedPercent: totalBytes > 0 ? (unusedBytes / totalBytes) * 100 : 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class Coverage {
|
|
44
|
+
constructor(cdp) {
|
|
45
|
+
this.rawClient = cdp.getRawClient();
|
|
46
|
+
this.started = false;
|
|
47
|
+
this.styleSheetUrls = new Map();
|
|
48
|
+
this.disposeStyleSheetListener = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async start() {
|
|
52
|
+
const client = this.rawClient;
|
|
53
|
+
this.styleSheetUrls.clear();
|
|
54
|
+
this.disposeStyleSheetListener = client.CSS.styleSheetAdded(
|
|
55
|
+
({ header }) => {
|
|
56
|
+
if (header && header.styleSheetId) {
|
|
57
|
+
this.styleSheetUrls.set(
|
|
58
|
+
header.styleSheetId,
|
|
59
|
+
header.sourceURL || header.sourceMapURL || ''
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await client.Debugger.enable();
|
|
66
|
+
await client.Profiler.enable();
|
|
67
|
+
await client.DOM.enable();
|
|
68
|
+
await client.CSS.enable();
|
|
69
|
+
await client.Profiler.startPreciseCoverage({
|
|
70
|
+
callCount: false,
|
|
71
|
+
detailed: true,
|
|
72
|
+
allowTriggeredUpdates: false
|
|
73
|
+
});
|
|
74
|
+
await client.CSS.startRuleUsageTracking();
|
|
75
|
+
this.started = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async collect() {
|
|
79
|
+
if (!this.started) return;
|
|
80
|
+
const client = this.rawClient;
|
|
81
|
+
|
|
82
|
+
let js;
|
|
83
|
+
let css;
|
|
84
|
+
try {
|
|
85
|
+
const jsResult = await client.Profiler.takePreciseCoverage();
|
|
86
|
+
js = await this.#processJs(jsResult.result || []);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
log.warn('Could not collect JS coverage: %s', error.message);
|
|
89
|
+
js = {
|
|
90
|
+
totalBytes: 0,
|
|
91
|
+
usedBytes: 0,
|
|
92
|
+
unusedBytes: 0,
|
|
93
|
+
unusedPercent: 0,
|
|
94
|
+
files: []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const cssResult = await client.CSS.stopRuleUsageTracking();
|
|
100
|
+
css = await this.#processCss(cssResult.ruleUsage || []);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
log.warn('Could not collect CSS coverage: %s', error.message);
|
|
103
|
+
css = {
|
|
104
|
+
totalBytes: 0,
|
|
105
|
+
usedBytes: 0,
|
|
106
|
+
unusedBytes: 0,
|
|
107
|
+
unusedPercent: 0,
|
|
108
|
+
files: []
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await client.Profiler.stopPreciseCoverage();
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore — collection already returned data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.disposeStyleSheetListener) {
|
|
119
|
+
this.disposeStyleSheetListener();
|
|
120
|
+
this.disposeStyleSheetListener = undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.started = false;
|
|
124
|
+
return { js, css };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async #processJs(scripts) {
|
|
128
|
+
const client = this.rawClient;
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const script of scripts) {
|
|
131
|
+
if (!script.url || !/^https?:/i.test(script.url)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
let source;
|
|
135
|
+
try {
|
|
136
|
+
const r = await client.Debugger.getScriptSource({
|
|
137
|
+
scriptId: script.scriptId
|
|
138
|
+
});
|
|
139
|
+
source = r.scriptSource || '';
|
|
140
|
+
} catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const totalBytes = source.length;
|
|
144
|
+
if (totalBytes === 0) continue;
|
|
145
|
+
|
|
146
|
+
const usedRanges = [];
|
|
147
|
+
for (const fn of script.functions) {
|
|
148
|
+
for (const r of fn.ranges) {
|
|
149
|
+
if (r.count > 0) usedRanges.push(r);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const usedBytes = unionLength(usedRanges);
|
|
153
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
154
|
+
files.push({
|
|
155
|
+
url: script.url,
|
|
156
|
+
totalBytes,
|
|
157
|
+
usedBytes,
|
|
158
|
+
unusedBytes,
|
|
159
|
+
unusedPercent: (unusedBytes / totalBytes) * 100
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return { ...summarize(files), files };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async #processCss(ruleUsage) {
|
|
166
|
+
const client = this.rawClient;
|
|
167
|
+
const byStylesheet = new Map();
|
|
168
|
+
for (const u of ruleUsage) {
|
|
169
|
+
const list = byStylesheet.get(u.styleSheetId) || [];
|
|
170
|
+
list.push(u);
|
|
171
|
+
byStylesheet.set(u.styleSheetId, list);
|
|
172
|
+
}
|
|
173
|
+
const files = [];
|
|
174
|
+
for (const [styleSheetId, usages] of byStylesheet) {
|
|
175
|
+
let text;
|
|
176
|
+
try {
|
|
177
|
+
const r = await client.CSS.getStyleSheetText({ styleSheetId });
|
|
178
|
+
text = r.text || '';
|
|
179
|
+
} catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const totalBytes = text.length;
|
|
183
|
+
if (totalBytes === 0) continue;
|
|
184
|
+
|
|
185
|
+
const usedBytes = unionLength(usages.filter(u => u.used));
|
|
186
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
187
|
+
files.push({
|
|
188
|
+
url: this.styleSheetUrls.get(styleSheetId) || '',
|
|
189
|
+
styleSheetId,
|
|
190
|
+
totalBytes,
|
|
191
|
+
usedBytes,
|
|
192
|
+
unusedBytes,
|
|
193
|
+
unusedPercent: (unusedBytes / totalBytes) * 100
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return { ...summarize(files), files };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -15,6 +15,7 @@ import { parse } from '../traceCategoriesParser.js';
|
|
|
15
15
|
import { pathToFolder } from '../../support/pathToFolder.js';
|
|
16
16
|
import { loadUsbPowerProfiler } from '../../support/usbPower.js';
|
|
17
17
|
import { ChromeDevtoolsProtocol } from '../chromeDevtoolsProtocol.js';
|
|
18
|
+
import { Coverage } from '../coverage.js';
|
|
18
19
|
import { NetworkManager } from '../networkManager.js';
|
|
19
20
|
import { Android, isAndroidConfigured } from '../../android/index.js';
|
|
20
21
|
import { getRenderBlocking } from './traceUtilities.js';
|
|
@@ -181,6 +182,11 @@ export class Chromium {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
if (this.chrome.coverage) {
|
|
186
|
+
this.coverage = new Coverage(this.cdpClient);
|
|
187
|
+
await this.coverage.start();
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
if (
|
|
185
191
|
this.collectTracingEvents &&
|
|
186
192
|
!this.isTracing &&
|
|
@@ -325,6 +331,14 @@ export class Chromium {
|
|
|
325
331
|
);
|
|
326
332
|
}
|
|
327
333
|
|
|
334
|
+
if (this.coverage) {
|
|
335
|
+
const coverage = await this.coverage.collect();
|
|
336
|
+
this.coverage = undefined;
|
|
337
|
+
if (coverage) {
|
|
338
|
+
result.coverage = coverage;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
328
342
|
if (this.chrome.cdp && this.chrome.cdp.performance) {
|
|
329
343
|
const rawCDPMetrics = await this.cdpClient.getPerformanceMetrics();
|
|
330
344
|
const cleanedMetrics = {};
|
|
@@ -39,6 +39,7 @@ function getNewResult(url, options) {
|
|
|
39
39
|
markedAsFailure: 0,
|
|
40
40
|
failureMessages: [],
|
|
41
41
|
cdp: { performance: [] },
|
|
42
|
+
coverage: { js: [], css: [] },
|
|
42
43
|
android: { batteryTemperature: [], power: [] },
|
|
43
44
|
timestamps: [],
|
|
44
45
|
browserScripts: [],
|
|
@@ -185,6 +186,13 @@ export class Collector {
|
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Only available for Chrome with --chrome.coverage or
|
|
190
|
+
// --enableProfileRun.
|
|
191
|
+
if (data.coverage) {
|
|
192
|
+
results.coverage.js.push(data.coverage.js);
|
|
193
|
+
results.coverage.css.push(data.coverage.css);
|
|
194
|
+
}
|
|
195
|
+
|
|
188
196
|
this._logIterationMetrics(data, url);
|
|
189
197
|
|
|
190
198
|
if (data.visualMetrics) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getLogger } from '@sitespeed.io/log';
|
|
3
|
-
import merge from 'lodash.merge';
|
|
4
3
|
import { timestamp as _timestamp } from '../../../support/engineUtils.js';
|
|
5
4
|
import { pathToFolder } from '../../../support/pathToFolder.js';
|
|
6
5
|
import {
|
|
@@ -9,7 +8,7 @@ import {
|
|
|
9
8
|
} from '../../../support/userTiming.js';
|
|
10
9
|
import { isAndroidConfigured, Android } from '../../../android/index.js';
|
|
11
10
|
import { TCPDump } from '../../../support/tcpdump.js';
|
|
12
|
-
import { getProperty } from '../../../support/util.js';
|
|
11
|
+
import { getProperty, merge } from '../../../support/util.js';
|
|
13
12
|
import { MeasureScreenshots } from './measure/screenshots.js';
|
|
14
13
|
import { MeasureVideo } from './measure/video.js';
|
|
15
14
|
const log = getLogger('browsertime.command.measure');
|
|
@@ -329,7 +328,7 @@ export class Measure {
|
|
|
329
328
|
*
|
|
330
329
|
* @async
|
|
331
330
|
* @param {string} errorMessage - The message about the error. This will end up on the HTML report for sitespeed.io so give it a good message so you know what's gone wrong.
|
|
332
|
-
* @returns {Promise} A promise that resolves when the stop process has completed.
|
|
331
|
+
* @returns {Promise<void>} A promise that resolves when the stop process has completed.
|
|
333
332
|
* @since 21.2.0
|
|
334
333
|
*/
|
|
335
334
|
async stopAsError(errorMessage) {
|
|
@@ -362,7 +361,7 @@ export class Measure {
|
|
|
362
361
|
* @async
|
|
363
362
|
* @param {string} alias - The friendly name for this measurement.
|
|
364
363
|
* @param {string} selector - The CSS selector of the element to click.
|
|
365
|
-
* @returns {Promise} A promise that resolves with the collected metrics data.
|
|
364
|
+
* @returns {Promise<unknown>} A promise that resolves with the collected metrics data.
|
|
366
365
|
* @throws {Error} Throws an error if the element is not found or the measurement fails.
|
|
367
366
|
*/
|
|
368
367
|
async clickAndMeasure(alias, selector) {
|
|
@@ -379,7 +378,7 @@ export class Measure {
|
|
|
379
378
|
* @async
|
|
380
379
|
* @param {string} testedStartUrl - The URL that was initially tested. If not provided, it will be obtained from the browser.
|
|
381
380
|
* @throws {Error} Throws an error if there are issues in stopping the measurement or collecting data.
|
|
382
|
-
* @returns {Promise} A promise that resolves with the collected metrics data.
|
|
381
|
+
* @returns {Promise<unknown>} A promise that resolves with the collected metrics data.
|
|
383
382
|
*/
|
|
384
383
|
async stop(testedStartUrl) {
|
|
385
384
|
log.debug('Stop measuring');
|
|
@@ -227,10 +227,10 @@ export class Commands {
|
|
|
227
227
|
* Navigates to a specified URL and handles additional setup for a page visit.
|
|
228
228
|
* @async
|
|
229
229
|
* @example await commands.navigate('https://www.example.org');
|
|
230
|
-
* @type {Function}
|
|
231
230
|
* @param {string} url - The URL to navigate to.
|
|
232
231
|
* @throws {Error} Throws an error if navigation or setup fails.
|
|
233
232
|
* @returns {Promise<void>} A promise that resolves when the navigation and setup are
|
|
233
|
+
* @type {(url: string) => Promise<void>}
|
|
234
234
|
*/
|
|
235
235
|
this.navigate = measure._navigate.bind(measure);
|
|
236
236
|
|
|
@@ -244,7 +244,7 @@ export class Commands {
|
|
|
244
244
|
* Add a text that will be an error attached to the current page.
|
|
245
245
|
* @example await commands.error('My error message');
|
|
246
246
|
* @param {string} message - The error message.
|
|
247
|
-
* @type {
|
|
247
|
+
* @type {(message: string) => void}
|
|
248
248
|
*/
|
|
249
249
|
this.error = measure._error.bind(measure);
|
|
250
250
|
|
|
@@ -252,7 +252,7 @@ export class Commands {
|
|
|
252
252
|
* Mark this run as an failure. Add a message that explains the failure.
|
|
253
253
|
* @example await commands.markAsFailure('My failure message');
|
|
254
254
|
* @param {string} message - The message attached as a failure
|
|
255
|
-
* @type {
|
|
255
|
+
* @type {(message: string) => void}
|
|
256
256
|
*/
|
|
257
257
|
this.markAsFailure = measure._failure.bind(measure);
|
|
258
258
|
|
|
@@ -357,10 +357,6 @@ export class Commands {
|
|
|
357
357
|
*/
|
|
358
358
|
this.debug = new Debug(browser, options);
|
|
359
359
|
|
|
360
|
-
/**
|
|
361
|
-
* Interact with the page using the mouse.
|
|
362
|
-
* @type {Object}
|
|
363
|
-
*/
|
|
364
360
|
const singleClickInstance = new SingleClick(
|
|
365
361
|
browser,
|
|
366
362
|
pageCompleteCheck,
|
|
@@ -375,6 +371,16 @@ export class Commands {
|
|
|
375
371
|
const mouseMoveInstance = new MouseMove(browser, options);
|
|
376
372
|
const clickAndHoldInstance = new ClickAndHold(browser);
|
|
377
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Interact with the page using the mouse.
|
|
376
|
+
* @type {{
|
|
377
|
+
* moveTo: (selector: string) => Promise<void>,
|
|
378
|
+
* singleClick: (selector: string, mouseOptions?: { waitForNavigation?: boolean }) => Promise<void>,
|
|
379
|
+
* doubleClick: (selector: string) => Promise<void>,
|
|
380
|
+
* contextClick: (selector: string) => Promise<void>,
|
|
381
|
+
* clickAndHold: ClickAndHold
|
|
382
|
+
* }}
|
|
383
|
+
*/
|
|
378
384
|
this.mouse = {
|
|
379
385
|
moveTo: async selector => mouseMoveInstance.run(selector),
|
|
380
386
|
singleClick: async (selector, mouseOptions) =>
|
|
@@ -485,7 +491,6 @@ export class Commands {
|
|
|
485
491
|
* @param {string} text - The text to type into the element.
|
|
486
492
|
* @returns {Promise<void>} A promise that resolves when the text has been added.
|
|
487
493
|
* @throws {Error} Throws an error if the element is not found.
|
|
488
|
-
* @type {Function}
|
|
489
494
|
*/
|
|
490
495
|
this.type = async (selector, text) => {
|
|
491
496
|
return this.addText(selector, text);
|
|
@@ -502,7 +507,7 @@ export class Commands {
|
|
|
502
507
|
* @param {boolean} [options.visible=false] - If true, waits for the element to be visible.
|
|
503
508
|
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
|
|
504
509
|
* @throws {Error} Throws an error if the element is not found.
|
|
505
|
-
* @type {
|
|
510
|
+
* @type {(selector: string, options?: { timeout?: number, visible?: boolean }) => Promise<import('selenium-webdriver').WebElement>}
|
|
506
511
|
*/
|
|
507
512
|
this.find = this.element.find.bind(this.element);
|
|
508
513
|
|
|
@@ -518,7 +523,6 @@ export class Commands {
|
|
|
518
523
|
* @param {Object} [existsOptions] - Options.
|
|
519
524
|
* @param {number} [existsOptions.timeout=0] - Maximum time to wait for the element. Default 0 (no wait).
|
|
520
525
|
* @returns {Promise<boolean>} True if the element exists.
|
|
521
|
-
* @type {Function}
|
|
522
526
|
*/
|
|
523
527
|
this.exists = async (selector, existsOptions = {}) => {
|
|
524
528
|
const { locator } = parseSelector(selector);
|
|
@@ -552,7 +556,6 @@ export class Commands {
|
|
|
552
556
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
553
557
|
* @returns {Promise<string>} The visible text content.
|
|
554
558
|
* @throws {Error} Throws an error if the element is not found.
|
|
555
|
-
* @type {Function}
|
|
556
559
|
*/
|
|
557
560
|
this.getText = async selector => {
|
|
558
561
|
const element = await findElement(selector);
|
|
@@ -567,7 +570,6 @@ export class Commands {
|
|
|
567
570
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
568
571
|
* @returns {Promise<string>} The element's value.
|
|
569
572
|
* @throws {Error} Throws an error if the element is not found.
|
|
570
|
-
* @type {Function}
|
|
571
573
|
*/
|
|
572
574
|
this.getValue = async selector => {
|
|
573
575
|
const element = await findElement(selector);
|
|
@@ -581,7 +583,6 @@ export class Commands {
|
|
|
581
583
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
582
584
|
* @returns {Promise<boolean>} True if the element is visible.
|
|
583
585
|
* @throws {Error} Throws an error if the element is not found.
|
|
584
|
-
* @type {Function}
|
|
585
586
|
*/
|
|
586
587
|
this.isVisible = async selector => {
|
|
587
588
|
const element = await findElement(selector);
|
|
@@ -597,7 +598,6 @@ export class Commands {
|
|
|
597
598
|
* @param {string} attribute - The attribute name.
|
|
598
599
|
* @returns {Promise<string>} The attribute value.
|
|
599
600
|
* @throws {Error} Throws an error if the element is not found.
|
|
600
|
-
* @type {Function}
|
|
601
601
|
*/
|
|
602
602
|
this.getAttribute = async (selector, attribute) => {
|
|
603
603
|
const element = await findElement(selector);
|
|
@@ -611,7 +611,6 @@ export class Commands {
|
|
|
611
611
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
612
612
|
* @returns {Promise<boolean>} True if the element is enabled.
|
|
613
613
|
* @throws {Error} Throws an error if the element is not found.
|
|
614
|
-
* @type {Function}
|
|
615
614
|
*/
|
|
616
615
|
this.isEnabled = async selector => {
|
|
617
616
|
const element = await findElement(selector);
|
|
@@ -625,7 +624,6 @@ export class Commands {
|
|
|
625
624
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
626
625
|
* @returns {Promise<boolean>} True if the element is checked/selected.
|
|
627
626
|
* @throws {Error} Throws an error if the element is not found.
|
|
628
|
-
* @type {Function}
|
|
629
627
|
*/
|
|
630
628
|
this.isChecked = async selector => {
|
|
631
629
|
const element = await findElement(selector);
|
|
@@ -640,7 +638,6 @@ export class Commands {
|
|
|
640
638
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
641
639
|
* @returns {Promise<void>} A promise that resolves when the element is cleared.
|
|
642
640
|
* @throws {Error} Throws an error if the element is not found.
|
|
643
|
-
* @type {Function}
|
|
644
641
|
*/
|
|
645
642
|
this.clear = async selector => {
|
|
646
643
|
const { locator } = parseSelector(selector);
|
|
@@ -665,7 +662,6 @@ export class Commands {
|
|
|
665
662
|
* @param {Object<string, string>} fields - An object mapping selectors to values.
|
|
666
663
|
* @returns {Promise<void>} A promise that resolves when all fields are filled.
|
|
667
664
|
* @throws {Error} Throws an error if any element is not found.
|
|
668
|
-
* @type {Function}
|
|
669
665
|
*/
|
|
670
666
|
this.fill = async fields => {
|
|
671
667
|
for (const [selector, text] of Object.entries(fields)) {
|
|
@@ -682,7 +678,6 @@ export class Commands {
|
|
|
682
678
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
683
679
|
* @returns {Promise<void>}
|
|
684
680
|
* @throws {Error} Throws an error if the element is not found.
|
|
685
|
-
* @type {Function}
|
|
686
681
|
*/
|
|
687
682
|
this.hover = async selector => {
|
|
688
683
|
return this.mouse.moveTo(selector);
|
|
@@ -697,7 +692,6 @@ export class Commands {
|
|
|
697
692
|
* @example await commands.press('Escape');
|
|
698
693
|
* @param {string} key - The key name to press (e.g. 'Enter', 'Tab', 'Escape').
|
|
699
694
|
* @returns {Promise<void>}
|
|
700
|
-
* @type {Function}
|
|
701
695
|
*/
|
|
702
696
|
this.press = async key => {
|
|
703
697
|
const keyValue = webdriver.Key[key.toUpperCase()] || key;
|
|
@@ -711,7 +705,6 @@ export class Commands {
|
|
|
711
705
|
* @async
|
|
712
706
|
* @example const title = await commands.getTitle();
|
|
713
707
|
* @returns {Promise<string>} The page title.
|
|
714
|
-
* @type {Function}
|
|
715
708
|
*/
|
|
716
709
|
this.getTitle = async () => {
|
|
717
710
|
return browser.getDriver().getTitle();
|
|
@@ -722,7 +715,6 @@ export class Commands {
|
|
|
722
715
|
* @async
|
|
723
716
|
* @example const url = await commands.getUrl();
|
|
724
717
|
* @returns {Promise<string>} The current URL.
|
|
725
|
-
* @type {Function}
|
|
726
718
|
*/
|
|
727
719
|
this.getUrl = async () => {
|
|
728
720
|
return browser.getDriver().getCurrentUrl();
|
|
@@ -736,7 +728,6 @@ export class Commands {
|
|
|
736
728
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
737
729
|
* @returns {Promise<void>}
|
|
738
730
|
* @throws {Error} Throws an error if the element is not found.
|
|
739
|
-
* @type {Function}
|
|
740
731
|
*/
|
|
741
732
|
this.check = async selector => {
|
|
742
733
|
const element = await findElement(selector);
|
|
@@ -754,7 +745,6 @@ export class Commands {
|
|
|
754
745
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
755
746
|
* @returns {Promise<void>}
|
|
756
747
|
* @throws {Error} Throws an error if the element is not found.
|
|
757
|
-
* @type {Function}
|
|
758
748
|
*/
|
|
759
749
|
this.uncheck = async selector => {
|
|
760
750
|
const element = await findElement(selector);
|
|
@@ -772,7 +762,6 @@ export class Commands {
|
|
|
772
762
|
* @param {string} selector - The CSS selector or prefixed selector.
|
|
773
763
|
* @returns {Promise<void>}
|
|
774
764
|
* @throws {Error} Throws an error if the element is not found.
|
|
775
|
-
* @type {Function}
|
|
776
765
|
*/
|
|
777
766
|
this.scrollIntoView = async selector => {
|
|
778
767
|
const element = await findElement(selector);
|
|
@@ -795,7 +784,6 @@ export class Commands {
|
|
|
795
784
|
* @param {number} [urlOptions.timeout=10000] - Maximum time to wait in milliseconds.
|
|
796
785
|
* @returns {Promise<void>}
|
|
797
786
|
* @throws {Error} Throws an error if the URL does not match within the timeout.
|
|
798
|
-
* @type {Function}
|
|
799
787
|
*/
|
|
800
788
|
this.waitForUrl = async (pattern, urlOptions = {}) => {
|
|
801
789
|
const timeout = urlOptions.timeout ?? 10_000;
|
|
@@ -30,13 +30,13 @@ export class Context {
|
|
|
30
30
|
) {
|
|
31
31
|
/**
|
|
32
32
|
* This is the yargs object you get from the cli. If you add --my.id you can get that using options.my.id.
|
|
33
|
-
* @type {
|
|
33
|
+
* @type {Record<string, any>}
|
|
34
34
|
*/
|
|
35
35
|
this.options = options;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Here the result from each run is stored.
|
|
39
|
-
* @type {
|
|
39
|
+
* @type {Record<string, any>}
|
|
40
40
|
*/
|
|
41
41
|
this.result = result;
|
|
42
42
|
|
|
@@ -58,7 +58,8 @@ export class Context {
|
|
|
58
58
|
this.storageManager = storageManager;
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Scratch space shared between commands within a single iteration.
|
|
62
|
+
* @type {Record<string, any>}
|
|
62
63
|
*/
|
|
63
64
|
this.taskData = {};
|
|
64
65
|
|
|
@@ -68,13 +69,7 @@ export class Context {
|
|
|
68
69
|
* @type {{webdriver: WebDriverClass, driver: WebDriverInstance}}
|
|
69
70
|
*/
|
|
70
71
|
this.selenium = {
|
|
71
|
-
/**
|
|
72
|
-
* @typedef {typeof import('selenium-webdriver')} WebDriverClass
|
|
73
|
-
*/
|
|
74
72
|
webdriver,
|
|
75
|
-
/**
|
|
76
|
-
* @typedef {import('selenium-webdriver').WebDriver} WebDriverInstance
|
|
77
|
-
*/
|
|
78
73
|
driver: instantiatedDriver
|
|
79
74
|
};
|
|
80
75
|
}
|