@wdio/lighthouse-service 9.0.0-alpha.369 → 9.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.
package/build/index.js CHANGED
@@ -1,126 +1,1103 @@
1
- import CommandHandler from './commands.js';
2
- import { setUnsupportedCommand, getLighthouseDriver } from './utils.js';
3
- import { DEFAULT_THROTTLE_STATE, NETWORK_STATES } from './constants.js';
4
- export default class DevToolsService {
5
- _options;
6
- _command = [];
7
- _browser;
8
- constructor(_options) {
9
- this._options = _options;
10
- }
11
- async before(caps, specs, browser) {
12
- this._browser = browser;
13
- return await this._setupHandler();
14
- }
15
- async onReload() {
16
- if (!this._browser) {
17
- return;
18
- }
19
- return this._setupHandler();
1
+ // src/commands.ts
2
+ import logger3 from "@wdio/logger";
3
+
4
+ // src/constants.ts
5
+ import InstallableManifest from "lighthouse/lighthouse-core/audits/installable-manifest.js";
6
+ import ServiceWorker from "lighthouse/lighthouse-core/audits/service-worker.js";
7
+ import SplashScreen from "lighthouse/lighthouse-core/audits/splash-screen.js";
8
+ import ThemedOmnibox from "lighthouse/lighthouse-core/audits/themed-omnibox.js";
9
+ import ContentWidth from "lighthouse/lighthouse-core/audits/content-width.js";
10
+ import Viewport from "lighthouse/lighthouse-core/audits/viewport.js";
11
+ import AppleTouchIcon from "lighthouse/lighthouse-core/audits/apple-touch-icon.js";
12
+ import MaskableIcon from "lighthouse/lighthouse-core/audits/maskable-icon.js";
13
+ import { throttling } from "lighthouse/lighthouse-core/config/constants.js";
14
+ var DEFAULT_TRACING_CATEGORIES = [
15
+ // Exclude default categories. We'll be selective to minimize trace size
16
+ "-*",
17
+ // Used instead of 'toplevel' in Chrome 71+
18
+ "disabled-by-default-lighthouse",
19
+ // Used for Cumulative Layout Shift metric
20
+ "loading",
21
+ // All compile/execute events are captured by parent events in devtools.timeline..
22
+ // But the v8 category provides some nice context for only <0.5% of the trace size
23
+ "v8",
24
+ // Same situation here. This category is there for RunMicrotasks only, but with other teams
25
+ // accidentally excluding microtasks, we don't want to assume a parent event will always exist
26
+ "v8.execute",
27
+ // For extracting UserTiming marks/measures
28
+ "blink.user_timing",
29
+ // Not mandatory but not used much
30
+ "blink.console",
31
+ // Most of the events we need are from these two categories
32
+ "devtools.timeline",
33
+ "disabled-by-default-devtools.timeline",
34
+ // Up to 450 (https://goo.gl/rBfhn4) JPGs added to the trace
35
+ "disabled-by-default-devtools.screenshot",
36
+ // This doesn't add its own events, but adds a `stackTrace` property to devtools.timeline events
37
+ "disabled-by-default-devtools.timeline.stack",
38
+ // Additional categories used by devtools. Not used by Lighthouse, but included to facilitate
39
+ // loading traces from Lighthouse into the Performance panel.
40
+ "disabled-by-default-devtools.timeline.frame",
41
+ "latencyInfo"
42
+ // CPU sampling profiler data only enabled for debugging purposes
43
+ // 'disabled-by-default-v8.cpu_profiler',
44
+ // 'disabled-by-default-v8.cpu_profiler.hires',
45
+ ];
46
+ var IGNORED_URLS = [
47
+ "data:,",
48
+ // empty pages
49
+ "about:",
50
+ // new tabs
51
+ "chrome-extension://"
52
+ // all chrome extensions
53
+ ];
54
+ var FRAME_LOAD_START_TIMEOUT = 2e3;
55
+ var TRACING_TIMEOUT = 15e3;
56
+ var MAX_TRACE_WAIT_TIME = 45e3;
57
+ var DEFAULT_NETWORK_THROTTLING_STATE = "online";
58
+ var DEFAULT_FORM_FACTOR = "desktop";
59
+ var UNSUPPORTED_ERROR_MESSAGE = "Can't connect to Chrome DevTools! The @wdio/lighthouse-service currently only supports Chrome and Chromium!\n\nGiven that cloud vendors don't expose access to the Chrome DevTools Protocol this service also usually only works when running tests locally or through a Selenium Grid (https://www.selenium.dev/documentation/grid/) v4 or higher.";
60
+ var NETWORK_STATES = {
61
+ offline: {
62
+ offline: true,
63
+ latency: 0,
64
+ downloadThroughput: 0,
65
+ uploadThroughput: 0
66
+ },
67
+ GPRS: {
68
+ offline: false,
69
+ downloadThroughput: 50 * 1024 / 8,
70
+ uploadThroughput: 20 * 1024 / 8,
71
+ latency: 500
72
+ },
73
+ "Regular 2G": {
74
+ offline: false,
75
+ downloadThroughput: 250 * 1024 / 8,
76
+ uploadThroughput: 50 * 1024 / 8,
77
+ latency: 300
78
+ },
79
+ "Good 2G": {
80
+ offline: false,
81
+ downloadThroughput: 450 * 1024 / 8,
82
+ uploadThroughput: 150 * 1024 / 8,
83
+ latency: 150
84
+ },
85
+ "Regular 3G": {
86
+ offline: false,
87
+ latency: throttling.mobileRegular3G.requestLatencyMs,
88
+ // DevTools expects throughput in bytes per second rather than kbps
89
+ downloadThroughput: Math.floor(throttling.mobileRegular3G.downloadThroughputKbps * 1024 / 8),
90
+ uploadThroughput: Math.floor(throttling.mobileRegular3G.uploadThroughputKbps * 1024 / 8)
91
+ },
92
+ "Good 3G": {
93
+ offline: false,
94
+ latency: throttling.mobileSlow4G.requestLatencyMs,
95
+ // DevTools expects throughput in bytes per second rather than kbps
96
+ downloadThroughput: Math.floor(throttling.mobileSlow4G.downloadThroughputKbps * 1024 / 8),
97
+ uploadThroughput: Math.floor(throttling.mobileSlow4G.uploadThroughputKbps * 1024 / 8)
98
+ },
99
+ "Regular 4G": {
100
+ offline: false,
101
+ downloadThroughput: 4 * 1024 * 1024 / 8,
102
+ uploadThroughput: 3 * 1024 * 1024 / 8,
103
+ latency: 20
104
+ },
105
+ "DSL": {
106
+ offline: false,
107
+ downloadThroughput: 2 * 1024 * 1024 / 8,
108
+ uploadThroughput: 1 * 1024 * 1024 / 8,
109
+ latency: 5
110
+ },
111
+ "Wifi": {
112
+ offline: false,
113
+ downloadThroughput: 30 * 1024 * 1024 / 8,
114
+ uploadThroughput: 15 * 1024 * 1024 / 8,
115
+ latency: 2
116
+ },
117
+ online: {
118
+ offline: false,
119
+ latency: 0,
120
+ downloadThroughput: -1,
121
+ uploadThroughput: -1
122
+ }
123
+ };
124
+ var CLICK_TRANSITION = "click transition";
125
+ var DEFAULT_THROTTLE_STATE = {
126
+ networkThrottling: DEFAULT_NETWORK_THROTTLING_STATE,
127
+ cpuThrottling: 0,
128
+ cacheEnabled: false,
129
+ formFactor: DEFAULT_FORM_FACTOR
130
+ };
131
+ var NETWORK_RECORDER_EVENTS = [
132
+ "Network.requestWillBeSent",
133
+ "Network.requestServedFromCache",
134
+ "Network.responseReceived",
135
+ "Network.dataReceived",
136
+ "Network.loadingFinished",
137
+ "Network.loadingFailed",
138
+ "Network.resourceChangedPriority"
139
+ ];
140
+ var PWA_AUDITS = {
141
+ isInstallable: InstallableManifest,
142
+ serviceWorker: ServiceWorker,
143
+ splashScreen: SplashScreen,
144
+ themedOmnibox: ThemedOmnibox,
145
+ contentWith: ContentWidth,
146
+ viewport: Viewport,
147
+ appleTouchIcon: AppleTouchIcon,
148
+ maskableIcon: MaskableIcon
149
+ };
150
+
151
+ // src/handler/network.ts
152
+ var NetworkHandler = class {
153
+ requestLog = { requests: [] };
154
+ requestTypes = {};
155
+ cachedFirstRequest;
156
+ constructor(session) {
157
+ session.on("Network.dataReceived", this.onDataReceived.bind(this));
158
+ session.on("Network.responseReceived", this.onNetworkResponseReceived.bind(this));
159
+ session.on("Network.requestWillBeSent", this.onNetworkRequestWillBeSent.bind(this));
160
+ session.on("Page.frameNavigated", this.onPageFrameNavigated.bind(this));
161
+ }
162
+ findRequest(params) {
163
+ let request = this.requestLog.requests.find((req) => req.id === params.requestId);
164
+ if (!request && this.cachedFirstRequest && this.cachedFirstRequest.id === params.requestId) {
165
+ request = this.cachedFirstRequest;
20
166
  }
21
- async beforeCommand(commandName, params) {
22
- return Promise.all(this._command.map(async (c) => await c._beforeCmd(commandName, params)));
167
+ return request;
168
+ }
169
+ onDataReceived(params) {
170
+ const request = this.findRequest(params);
171
+ if (!request || !request.type || !this.requestTypes[request.type]) {
172
+ return;
23
173
  }
24
- async afterCommand(commandName) {
25
- if (commandName === 'switchToWindow') {
26
- await this._setupHandler();
27
- }
28
- return Promise.all(this._command.map(async (c) => await c._afterCmd(commandName)));
29
- }
30
- /**
31
- * set flag to run performance audits for page transitions
32
- */
33
- _enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor } = DEFAULT_THROTTLE_STATE) {
34
- if (!NETWORK_STATES[networkThrottling]) {
35
- throw new Error(`Network throttling profile "${networkThrottling}" is unknown, choose between ${Object.keys(NETWORK_STATES).join(', ')}`);
36
- }
37
- if (typeof cpuThrottling !== 'number') {
38
- throw new Error(`CPU throttling rate needs to be typeof number but was "${typeof cpuThrottling}"`);
39
- }
40
- if (this._command.length === 1) {
41
- this._command[0].enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
42
- }
43
- else {
44
- for (const c of this._command) {
45
- c.enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
46
- }
47
- }
174
+ const type = request.type;
175
+ const requestType = this.requestTypes[type] || {};
176
+ requestType.size += params.dataLength;
177
+ requestType.encoded += params.encodedDataLength;
178
+ }
179
+ onNetworkResponseReceived(params) {
180
+ const request = this.findRequest(params);
181
+ if (!request) {
182
+ return;
48
183
  }
49
- /**
50
- * custom command to disable performance audits
51
- */
52
- _disablePerformanceAudits() {
53
- if (this._command.length === 1) {
54
- this._command[0].disablePerformanceAudits();
55
- }
56
- else {
57
- for (const c of this._command) {
58
- c.disablePerformanceAudits();
59
- }
60
- }
184
+ request.statusCode = params.response.status;
185
+ request.requestHeaders = params.response.requestHeaders;
186
+ request.responseHeaders = params.response.headers;
187
+ request.timing = params.response.timing;
188
+ request.type = params.type;
189
+ }
190
+ onNetworkRequestWillBeSent(params) {
191
+ let isFirstRequestOfFrame = false;
192
+ if (
193
+ /**
194
+ * A new page was opened when request type is a document.
195
+ * The first request is sent before the Page.frameNavigated event is triggered,
196
+ * so this request must be cached to be able to add it to the requestLog later.
197
+ */
198
+ params.type === "Document" && /**
199
+ * ensure that only page loads triggered by non scripts (devtools only) are considered
200
+ * new page loads
201
+ */
202
+ params.initiator.type === "other" && /**
203
+ * ignore pages not initated by the user
204
+ */
205
+ IGNORED_URLS.filter((url) => params.request.url.startsWith(url)).length === 0
206
+ ) {
207
+ isFirstRequestOfFrame = true;
208
+ this.requestTypes = {};
61
209
  }
62
- async _setThrottlingProfile(networkThrottling = DEFAULT_THROTTLE_STATE.networkThrottling, cpuThrottling = DEFAULT_THROTTLE_STATE.cpuThrottling, cacheEnabled = DEFAULT_THROTTLE_STATE.cacheEnabled) {
63
- if (this._command.length === 1) {
64
- this._command[0].setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
65
- }
66
- else {
67
- for (const c of this._command) {
68
- c.setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
69
- }
70
- }
210
+ const log4 = {
211
+ id: params.requestId,
212
+ url: params.request.url,
213
+ method: params.request.method
214
+ };
215
+ if (params.redirectResponse) {
216
+ log4.redirect = {
217
+ url: params.redirectResponse.url,
218
+ statusCode: params.redirectResponse.status,
219
+ requestHeaders: params.redirectResponse.requestHeaders,
220
+ responseHeaders: params.redirectResponse.headers,
221
+ timing: params.redirectResponse.timing
222
+ };
71
223
  }
72
- async _checkPWA(auditsToBeRun) {
73
- if (this._command.length === 1) {
74
- return await this._command[0].checkPWA(auditsToBeRun);
75
- }
76
- return Promise.all(this._command.map(async (c) => await c.checkPWA(auditsToBeRun)));
224
+ if (params.type) {
225
+ const requestType = this.requestTypes[params.type];
226
+ if (!requestType) {
227
+ this.requestTypes[params.type] = {
228
+ size: 0,
229
+ encoded: 0,
230
+ count: 1
231
+ };
232
+ } else if (requestType) {
233
+ requestType.count++;
234
+ }
77
235
  }
78
- async _setupHandler() {
79
- if (!this._browser) {
80
- return;
81
- }
82
- /**
83
- * In case of switchToWindow, needs to not add more commands to the array
84
- */
85
- this._command.length = 0;
86
- /**
87
- * To avoid if-else, gather all browser instances into an array
88
- */
89
- const browsers = Object.keys(this._browser).includes('sessionId') ?
90
- [this._browser] :
91
- this._browser.instances.map(i => this._browser.getInstance(i));
92
- for (const browser of browsers) {
93
- const puppeteer = await browser.getPuppeteer().catch(() => undefined);
94
- if (!puppeteer) {
95
- return setUnsupportedCommand(browser);
96
- }
97
- const url = await browser.getUrl();
98
- const target = url !== 'data:,' ?
99
- await puppeteer.waitForTarget(
100
- /* istanbul ignore next */
101
- (t) => t.url().includes(url)) :
102
- await puppeteer.waitForTarget(
103
- /* istanbul ignore next */
104
- // @ts-expect-error
105
- (t) => t.type() === 'page' || Boolean(t._getTargetInfo().browserContextId));
106
- /* istanbul ignore next */
107
- if (!target) {
108
- throw new Error('No page target found');
109
- }
110
- const page = await target.page() || null;
111
- /* istanbul ignore next */
112
- if (!page) {
113
- throw new Error('No page found');
114
- }
115
- const session = await target.createCDPSession();
116
- const driver = await getLighthouseDriver(session, target);
117
- const cmd = new CommandHandler(session, page, driver, this._options, browser);
118
- await cmd._initCommand();
119
- this._command.push(cmd);
236
+ if (isFirstRequestOfFrame) {
237
+ log4.loaderId = params.loaderId;
238
+ this.cachedFirstRequest = log4;
239
+ return;
240
+ }
241
+ return this.requestLog.requests.push(log4);
242
+ }
243
+ onPageFrameNavigated(params) {
244
+ if (!params.frame.parentId && IGNORED_URLS.filter((url) => params.frame.url.startsWith(url)).length === 0) {
245
+ this.requestLog = {
246
+ id: params.frame.loaderId,
247
+ url: params.frame.url,
248
+ requests: []
249
+ };
250
+ if (this.cachedFirstRequest && this.cachedFirstRequest.loaderId === params.frame.loaderId) {
251
+ delete this.cachedFirstRequest.loaderId;
252
+ this.requestLog.requests.push(this.cachedFirstRequest);
253
+ this.cachedFirstRequest = void 0;
254
+ }
255
+ }
256
+ }
257
+ };
258
+
259
+ // src/utils.ts
260
+ import Driver from "lighthouse/lighthouse-core/gather/driver.js";
261
+
262
+ // src/lighthouse/cri.ts
263
+ import CriConnection from "lighthouse/lighthouse-core/gather/connections/cri.js";
264
+ var DEFAULT_HOSTNAME = "localhost";
265
+ var DEFAULT_PORT = "9222";
266
+ var ChromeProtocolPatched = class extends CriConnection {
267
+ _sessionId;
268
+ /**
269
+ * Add constructor for typing safety
270
+ * @param {number=} port Optional port number. Defaults to 9222;
271
+ * @param {string=} hostname Optional hostname. Defaults to localhost.
272
+ * @constructor
273
+ */
274
+ constructor(port = DEFAULT_PORT, hostname = DEFAULT_HOSTNAME) {
275
+ super(port, hostname);
276
+ }
277
+ setSessionId(sessionId) {
278
+ this._sessionId = sessionId;
279
+ }
280
+ /**
281
+ * force every command to be send with the given session id
282
+ */
283
+ sendCommand(method, sessionId, ...paramArgs) {
284
+ return super.sendCommand(method, sessionId || this._sessionId, ...paramArgs);
285
+ }
286
+ };
287
+
288
+ // src/utils.ts
289
+ var CUSTOM_COMMANDS = [
290
+ "getMetrics",
291
+ "startTracing",
292
+ "getDiagnostics",
293
+ "getCoverageReport",
294
+ "enablePerformanceAudits",
295
+ "disablePerformanceAudits",
296
+ "getMainThreadWorkBreakdown",
297
+ "checkPWA"
298
+ ];
299
+ function setUnsupportedCommand(browser) {
300
+ for (const command of CUSTOM_COMMANDS) {
301
+ browser.addCommand(
302
+ command,
303
+ /* istanbul ignore next */
304
+ () => {
305
+ throw new Error(UNSUPPORTED_ERROR_MESSAGE);
306
+ }
307
+ );
308
+ }
309
+ }
310
+ function sumByKey(list, key) {
311
+ return list.map((data) => data[key]).reduce((acc, val) => acc + val, 0);
312
+ }
313
+ function isSupportedUrl(url) {
314
+ return IGNORED_URLS.filter((ignoredUrl) => url.startsWith(ignoredUrl)).length === 0;
315
+ }
316
+ async function getLighthouseDriver(session, target) {
317
+ const connection = session.connection();
318
+ if (!connection) {
319
+ throw new Error("Couldn't find a CDP connection");
320
+ }
321
+ const cUrl = new URL(connection.url());
322
+ const cdpConnection = new ChromeProtocolPatched(cUrl.port, cUrl.hostname);
323
+ if (!cUrl.pathname.startsWith("/devtools/browser")) {
324
+ await cdpConnection._connectToSocket({
325
+ webSocketDebuggerUrl: connection.url(),
326
+ id: (await target.asPage()).mainFrame()._id
327
+ });
328
+ const { sessionId } = await cdpConnection.sendCommand(
329
+ "Target.attachToTarget",
330
+ void 0,
331
+ { targetId: (await target.asPage()).mainFrame()._id, flatten: true }
332
+ );
333
+ cdpConnection.setSessionId(sessionId);
334
+ return new Driver(cdpConnection);
335
+ }
336
+ const list = await cdpConnection._runJsonCommand("list");
337
+ await cdpConnection._connectToSocket(list[0]);
338
+ return new Driver(cdpConnection);
339
+ }
340
+
341
+ // src/gatherer/devtools.ts
342
+ var DevtoolsGatherer = class {
343
+ _logs = [];
344
+ onMessage(msgObj) {
345
+ this._logs.push(msgObj);
346
+ }
347
+ /**
348
+ * retrieve logs and clean cache
349
+ */
350
+ getLogs() {
351
+ return this._logs.splice(0, this._logs.length);
352
+ }
353
+ };
354
+
355
+ // src/auditor.ts
356
+ import Diagnostics from "lighthouse/lighthouse-core/audits/diagnostics.js";
357
+ import MainThreadWorkBreakdown from "lighthouse/lighthouse-core/audits/mainthread-work-breakdown.js";
358
+ import Metrics from "lighthouse/lighthouse-core/audits/metrics.js";
359
+ import ServerResponseTime from "lighthouse/lighthouse-core/audits/server-response-time.js";
360
+ import CumulativeLayoutShift from "lighthouse/lighthouse-core/audits/metrics/cumulative-layout-shift.js";
361
+ import FirstContentfulPaint from "lighthouse/lighthouse-core/audits/metrics/first-contentful-paint.js";
362
+ import LargestContentfulPaint from "lighthouse/lighthouse-core/audits/metrics/largest-contentful-paint.js";
363
+ import SpeedIndex from "lighthouse/lighthouse-core/audits/metrics/speed-index.js";
364
+ import InteractiveMetric from "lighthouse/lighthouse-core/audits/metrics/interactive.js";
365
+ import TotalBlockingTime from "lighthouse/lighthouse-core/audits/metrics/total-blocking-time.js";
366
+ import ReportScoring from "lighthouse/lighthouse-core/scoring.js";
367
+ import defaultConfig from "lighthouse/lighthouse-core/config/default-config.js";
368
+ import logger from "@wdio/logger";
369
+ var log = logger("@wdio/lighthouse-service:Auditor");
370
+ var Auditor = class {
371
+ constructor(_traceLogs, _devtoolsLogs, _formFactor) {
372
+ this._traceLogs = _traceLogs;
373
+ this._devtoolsLogs = _devtoolsLogs;
374
+ this._formFactor = _formFactor;
375
+ if (_traceLogs) {
376
+ this._url = _traceLogs.pageUrl;
377
+ }
378
+ }
379
+ _url;
380
+ _audit(AUDIT, params = {}) {
381
+ const auditContext = {
382
+ options: {
383
+ ...AUDIT.defaultOptions
384
+ },
385
+ settings: {
386
+ throttlingMethod: "devtools",
387
+ formFactor: this._formFactor || DEFAULT_FORM_FACTOR
388
+ },
389
+ LighthouseRunWarnings: false,
390
+ computedCache: /* @__PURE__ */ new Map()
391
+ };
392
+ try {
393
+ return AUDIT.audit({
394
+ traces: { defaultPass: this._traceLogs },
395
+ devtoolsLogs: { defaultPass: this._devtoolsLogs },
396
+ TestedAsMobileDevice: true,
397
+ GatherContext: { gatherMode: "navigation" },
398
+ ...params
399
+ }, auditContext);
400
+ } catch (error) {
401
+ log.error(error);
402
+ return {
403
+ score: 0,
404
+ error
405
+ };
406
+ }
407
+ }
408
+ /**
409
+ * an Auditor instance is created for every trace so provide an updateCommands
410
+ * function to receive the latest performance metrics with the browser instance
411
+ */
412
+ updateCommands(browser, customFn) {
413
+ const commands = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
414
+ (fnName) => fnName !== "constructor" && fnName !== "updateCommands" && !fnName.startsWith("_")
415
+ );
416
+ commands.forEach((fnName) => browser.addCommand(fnName, customFn || this[fnName].bind(this)));
417
+ }
418
+ /**
419
+ * Returns a list with a breakdown of all main thread task and their total duration
420
+ */
421
+ async getMainThreadWorkBreakdown() {
422
+ const result = await this._audit(MainThreadWorkBreakdown);
423
+ return result.details.items.map(
424
+ ({ group, duration }) => ({ group, duration })
425
+ );
426
+ }
427
+ /**
428
+ * Get some useful diagnostics about the page load
429
+ */
430
+ async getDiagnostics() {
431
+ const result = await this._audit(Diagnostics);
432
+ if (!Object.prototype.hasOwnProperty.call(result, "details")) {
433
+ return null;
434
+ }
435
+ return result.details.items[0];
436
+ }
437
+ /**
438
+ * Get most common used performance metrics
439
+ */
440
+ async getMetrics() {
441
+ const serverResponseTime = await this._audit(ServerResponseTime, { URL: this._url });
442
+ const cumulativeLayoutShift = await this._audit(CumulativeLayoutShift);
443
+ const result = await this._audit(Metrics);
444
+ const metrics = result.details.items[0] || {};
445
+ return {
446
+ timeToFirstByte: Math.round(serverResponseTime.numericValue),
447
+ serverResponseTime: Math.round(serverResponseTime.numericValue),
448
+ domContentLoaded: metrics.observedDomContentLoaded,
449
+ firstVisualChange: metrics.observedFirstVisualChange,
450
+ firstPaint: metrics.observedFirstPaint,
451
+ firstContentfulPaint: metrics.firstContentfulPaint,
452
+ firstMeaningfulPaint: metrics.firstMeaningfulPaint,
453
+ largestContentfulPaint: metrics.largestContentfulPaint,
454
+ lastVisualChange: metrics.observedLastVisualChange,
455
+ interactive: metrics.interactive,
456
+ load: metrics.observedLoad,
457
+ speedIndex: metrics.speedIndex,
458
+ totalBlockingTime: metrics.totalBlockingTime,
459
+ maxPotentialFID: metrics.maxPotentialFID,
460
+ cumulativeLayoutShift: cumulativeLayoutShift.numericValue
461
+ };
462
+ }
463
+ /**
464
+ * Returns the Lighthouse Performance Score which is a weighted mean of the following metrics: firstMeaningfulPaint, interactive, speedIndex
465
+ */
466
+ async getPerformanceScore() {
467
+ const auditResults = {
468
+ "speed-index": await this._audit(SpeedIndex),
469
+ "first-contentful-paint": await this._audit(FirstContentfulPaint),
470
+ "largest-contentful-paint": await this._audit(LargestContentfulPaint),
471
+ "cumulative-layout-shift": await this._audit(CumulativeLayoutShift),
472
+ "total-blocking-time": await this._audit(TotalBlockingTime),
473
+ interactive: await this._audit(InteractiveMetric)
474
+ };
475
+ if (!auditResults.interactive || !auditResults["cumulative-layout-shift"] || !auditResults["first-contentful-paint"] || !auditResults["largest-contentful-paint"] || !auditResults["speed-index"] || !auditResults["total-blocking-time"]) {
476
+ log.info("One or multiple required metrics couldn't be found, setting performance score to: null");
477
+ return null;
478
+ }
479
+ const scores = defaultConfig.categories.performance.auditRefs.filter((auditRef) => auditRef.weight).map((auditRef) => ({
480
+ score: auditResults[auditRef.id].score,
481
+ weight: auditRef.weight
482
+ }));
483
+ return ReportScoring.arithmeticMean(scores);
484
+ }
485
+ async _auditPWA(params, auditsToBeRun = Object.keys(PWA_AUDITS)) {
486
+ const audits = await Promise.all(
487
+ Object.entries(PWA_AUDITS).filter(([name]) => auditsToBeRun.includes(name)).map(
488
+ async ([name, Audit]) => [name, await this._audit(Audit, params)]
489
+ )
490
+ );
491
+ return {
492
+ passed: !audits.find(([, result]) => result.score < 1),
493
+ details: audits.reduce((details, [name, result]) => {
494
+ details[name] = result;
495
+ return details;
496
+ }, {})
497
+ };
498
+ }
499
+ };
500
+
501
+ // src/gatherer/pwa.ts
502
+ import FRGatherer from "lighthouse/lighthouse-core/fraggle-rock/gather/session.js";
503
+ import pageFunctions from "lighthouse/lighthouse-core/lib/page-functions.js";
504
+ import NetworkRecorder from "lighthouse/lighthouse-core/lib/network-recorder.js";
505
+ import InstallabilityErrors from "lighthouse/lighthouse-core/gather/gatherers/installability-errors.js";
506
+ import WebAppManifest from "lighthouse/lighthouse-core/gather/gatherers/web-app-manifest.js";
507
+ import LinkElements from "lighthouse/lighthouse-core/gather/gatherers/link-elements.js";
508
+ import ViewportDimensions from "lighthouse/lighthouse-core/gather/gatherers/viewport-dimensions.js";
509
+ import serviceWorkers from "lighthouse/lighthouse-core/gather/driver/service-workers.js";
510
+
511
+ // src/scripts/collectMetaElements.ts
512
+ function collectMetaElements() {
513
+ const selector = "head meta";
514
+ const realMatchesFn = window.Element.prototype.matches;
515
+ const metas = [];
516
+ const _findAllElements = (nodes) => {
517
+ for (let i = 0, el; el = nodes[i]; ++i) {
518
+ if (!selector || realMatchesFn.call(el, selector)) {
519
+ metas.push(el);
520
+ }
521
+ if (el.shadowRoot) {
522
+ _findAllElements(el.shadowRoot.querySelectorAll("*"));
523
+ }
524
+ }
525
+ };
526
+ _findAllElements(document.querySelectorAll("*"));
527
+ return metas.map((meta) => {
528
+ const getAttribute = (name) => {
529
+ const attr = meta.attributes.getNamedItem(name);
530
+ if (!attr) {
531
+ return;
532
+ }
533
+ return attr.value;
534
+ };
535
+ return {
536
+ // @ts-ignore
537
+ name: meta.name.toLowerCase(),
538
+ // @ts-ignore
539
+ content: meta.content,
540
+ property: getAttribute("property"),
541
+ // @ts-ignore
542
+ httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : void 0,
543
+ charset: getAttribute("charset")
544
+ };
545
+ });
546
+ }
547
+
548
+ // src/gatherer/pwa.ts
549
+ var PWAGatherer = class {
550
+ constructor(_session, _page, _driver) {
551
+ this._session = _session;
552
+ this._page = _page;
553
+ this._driver = _driver;
554
+ this._frGatherer = new FRGatherer(this._session);
555
+ this._networkRecorder = new NetworkRecorder();
556
+ NETWORK_RECORDER_EVENTS.forEach((method) => {
557
+ this._session.on(method, (params) => this._networkRecorder.dispatch({ method, params }));
558
+ });
559
+ this._page.on("load", () => {
560
+ this._networkRecords = this._networkRecorder.getRawRecords();
561
+ delete this._networkRecorder;
562
+ this._networkRecorder = new NetworkRecorder();
563
+ });
564
+ }
565
+ _frGatherer;
566
+ _networkRecorder;
567
+ _networkRecords = [];
568
+ async gatherData() {
569
+ const pageUrl = await this._page?.url();
570
+ const passContext = {
571
+ url: pageUrl,
572
+ driver: this._driver
573
+ };
574
+ const loadData = {
575
+ networkRecords: this._networkRecords
576
+ };
577
+ const linkElements = new LinkElements();
578
+ const viewportDimensions = new ViewportDimensions();
579
+ const { registrations } = await serviceWorkers.getServiceWorkerRegistrations(this._frGatherer);
580
+ const { versions } = await serviceWorkers.getServiceWorkerVersions(this._frGatherer);
581
+ return {
582
+ URL: { requestedUrl: pageUrl, finalUrl: pageUrl },
583
+ WebAppManifest: await WebAppManifest.getWebAppManifest(this._frGatherer, pageUrl),
584
+ InstallabilityErrors: await InstallabilityErrors.getInstallabilityErrors(this._frGatherer),
585
+ MetaElements: await this._driver.evaluate(collectMetaElements, {
586
+ args: [],
587
+ useIsolation: true,
588
+ deps: [pageFunctions.getElementsInDocument]
589
+ }),
590
+ ViewportDimensions: await viewportDimensions.afterPass(passContext),
591
+ ServiceWorker: { versions, registrations },
592
+ LinkElements: await linkElements.afterPass(passContext, loadData)
593
+ };
594
+ }
595
+ };
596
+
597
+ // src/gatherer/trace.ts
598
+ import { EventEmitter } from "node:events";
599
+ import NetworkRecorder2 from "lighthouse/lighthouse-core/lib/network-recorder.js";
600
+ import NetworkMonitor from "lighthouse/lighthouse-core/gather/driver/network-monitor.js";
601
+ import ProtocolSession from "lighthouse/lighthouse-core/fraggle-rock/gather/session.js";
602
+ import { waitForFullyLoaded } from "lighthouse/lighthouse-core/gather/driver/wait-for-condition.js";
603
+ import logger2 from "@wdio/logger";
604
+
605
+ // src/scripts/registerPerformanceObserverInPage.ts
606
+ function registerPerformanceObserverInPage() {
607
+ window.____lastLongTask = window.performance.now();
608
+ const observer = new window.PerformanceObserver((entryList) => {
609
+ const entries = entryList.getEntries();
610
+ for (const entry of entries) {
611
+ if (entry.entryType === "longtask") {
612
+ const taskEnd = entry.startTime + entry.duration;
613
+ window.____lastLongTask = Math.max(window.____lastLongTask, taskEnd);
614
+ }
615
+ }
616
+ });
617
+ observer.observe({ entryTypes: ["longtask"] });
618
+ window.____lhPerformanceObserver = observer;
619
+ }
620
+
621
+ // src/gatherer/trace.ts
622
+ var log2 = logger2("@wdio/lighthouse-service:TraceGatherer");
623
+ var TraceGatherer = class extends EventEmitter {
624
+ constructor(_session, _page, _driver) {
625
+ super();
626
+ this._session = _session;
627
+ this._page = _page;
628
+ this._driver = _driver;
629
+ NETWORK_RECORDER_EVENTS.forEach((method) => {
630
+ this._networkListeners[method] = (params) => this._networkStatusMonitor.dispatch({ method, params });
631
+ });
632
+ this._protocolSession = new ProtocolSession(_session);
633
+ this._networkMonitor = new NetworkMonitor(_session);
634
+ }
635
+ _failingFrameLoadIds = [];
636
+ _pageLoadDetected = false;
637
+ _networkListeners = {};
638
+ _frameId;
639
+ _loaderId;
640
+ _pageUrl;
641
+ _networkStatusMonitor;
642
+ _networkMonitor;
643
+ _protocolSession;
644
+ _trace;
645
+ _traceStart;
646
+ _clickTraceTimeout;
647
+ _waitConditionPromises = [];
648
+ async startTracing(url) {
649
+ delete this._trace;
650
+ this._networkStatusMonitor = new NetworkRecorder2();
651
+ NETWORK_RECORDER_EVENTS.forEach((method) => {
652
+ this._session.on(method, this._networkListeners[method]);
653
+ });
654
+ this._traceStart = Date.now();
655
+ log2.info(`Start tracing frame with url ${url}`);
656
+ await this._driver.beginTrace();
657
+ if (url === CLICK_TRANSITION) {
658
+ log2.info("Start checking for page load for click");
659
+ this._clickTraceTimeout = setTimeout(async () => {
660
+ log2.info("No page load detected, canceling trace");
661
+ return this.finishTracing();
662
+ }, FRAME_LOAD_START_TIMEOUT);
663
+ }
664
+ await this._page.evaluateOnNewDocument(registerPerformanceObserverInPage);
665
+ this._waitConditionPromises.push(
666
+ waitForFullyLoaded(this._protocolSession, this._networkMonitor, { timedOut: 1 })
667
+ );
668
+ }
669
+ /**
670
+ * store frame id of frames that are being traced
671
+ */
672
+ async onFrameNavigated(msgObj) {
673
+ if (!this.isTracing) {
674
+ return;
675
+ }
676
+ if (this._failingFrameLoadIds.includes(msgObj.frame.url)) {
677
+ this._waitConditionPromises = [];
678
+ this._frameId = '"unsuccessful loaded frame"';
679
+ this.finishTracing();
680
+ this.emit("tracingError", new Error(`Page with url "${msgObj.frame.url}" failed to load`));
681
+ if (this._clickTraceTimeout) {
682
+ clearTimeout(this._clickTraceTimeout);
683
+ }
684
+ }
685
+ if (
686
+ // we already detected a frameId before
687
+ this._frameId || // the event was thrown for a sub frame (e.g. iframe)
688
+ msgObj.frame.parentId || // we don't support the url of given frame
689
+ !isSupportedUrl(msgObj.frame.url)
690
+ ) {
691
+ log2.info(`Ignore navigated frame with url ${msgObj.frame.url}`);
692
+ return;
693
+ }
694
+ this._frameId = msgObj.frame.id;
695
+ this._loaderId = msgObj.frame.loaderId;
696
+ this._pageUrl = msgObj.frame.url;
697
+ log2.info(`Page load detected: ${this._pageUrl}, set frameId ${this._frameId}, set loaderId ${this._loaderId}`);
698
+ if (this._clickTraceTimeout && !this._pageLoadDetected) {
699
+ log2.info("Page load detected for click, clearing click trace timeout}");
700
+ this._pageLoadDetected = true;
701
+ clearTimeout(this._clickTraceTimeout);
702
+ }
703
+ this.emit("tracingStarted", msgObj.frame.id);
704
+ }
705
+ /**
706
+ * once the page load event has fired, we can grab some performance
707
+ * metrics and timing
708
+ */
709
+ async onLoadEventFired() {
710
+ if (!this.isTracing) {
711
+ return;
712
+ }
713
+ const loadPromise = Promise.all(this._waitConditionPromises).then(() => async () => {
714
+ const minTraceTime = TRACING_TIMEOUT - (Date.now() - (this._traceStart || 0));
715
+ if (minTraceTime > 0) {
716
+ log2.info(`page load happen to quick, waiting ${minTraceTime}ms more`);
717
+ await new Promise((resolve) => setTimeout(resolve, minTraceTime));
718
+ }
719
+ return this.completeTracing();
720
+ });
721
+ const cleanupFn = await Promise.race([
722
+ loadPromise,
723
+ this.waitForMaxTimeout()
724
+ ]);
725
+ this._waitConditionPromises = [];
726
+ return cleanupFn();
727
+ }
728
+ onFrameLoadFail(request) {
729
+ const frame = request.frame();
730
+ if (frame) {
731
+ this._failingFrameLoadIds.push(frame.url());
732
+ }
733
+ }
734
+ get isTracing() {
735
+ return typeof this._traceStart === "number";
736
+ }
737
+ /**
738
+ * once tracing has finished capture trace logs into memory
739
+ */
740
+ async completeTracing() {
741
+ const traceDuration = Date.now() - (this._traceStart || 0);
742
+ log2.info(`Tracing completed after ${traceDuration}ms, capturing performance data for frame ${this._frameId}`);
743
+ try {
744
+ const traceEvents = await this._driver.endTrace();
745
+ const startedInBrowserEvt = traceEvents.traceEvents.find((e) => e.name === "TracingStartedInBrowser");
746
+ const mainFrame = startedInBrowserEvt && startedInBrowserEvt.args && startedInBrowserEvt.args.data.frames?.find((frame) => !frame.parent);
747
+ if (mainFrame && mainFrame.processId) {
748
+ const threadNameEvt = traceEvents.traceEvents.find((e) => e.ph === "R" && e.cat === "blink.user_timing" && e.name === "navigationStart" && e.args.data.isLoadingMainFrame);
749
+ if (threadNameEvt) {
750
+ log2.info(`Replace mainFrame process id ${mainFrame.processId} with actual thread process id ${threadNameEvt.pid}`);
751
+ mainFrame.processId = threadNameEvt.pid;
752
+ } else {
753
+ log2.info(`Couldn't replace mainFrame process id ${mainFrame.processId} with actual thread process id`);
120
754
  }
121
- this._browser.addCommand('enablePerformanceAudits', this._enablePerformanceAudits.bind(this));
122
- this._browser.addCommand('disablePerformanceAudits', this._disablePerformanceAudits.bind(this));
123
- this._browser.addCommand('checkPWA', this._checkPWA.bind(this));
755
+ }
756
+ this._trace = {
757
+ ...traceEvents,
758
+ frameId: this._frameId,
759
+ loaderId: this._loaderId,
760
+ pageUrl: this._pageUrl,
761
+ traceStart: this._traceStart,
762
+ traceEnd: Date.now()
763
+ };
764
+ this.emit("tracingComplete", this._trace);
765
+ this.finishTracing();
766
+ } catch (err) {
767
+ log2.error(`Error capturing tracing logs: ${err.stack}`);
768
+ this.emit("tracingError", err);
769
+ return this.finishTracing();
124
770
  }
771
+ }
772
+ /**
773
+ * clear tracing states and emit tracingFinished
774
+ */
775
+ finishTracing() {
776
+ log2.info(`Tracing for ${this._frameId} completed`);
777
+ this._pageLoadDetected = false;
778
+ NETWORK_RECORDER_EVENTS.forEach(
779
+ (method) => this._session.off(method, this._networkListeners[method])
780
+ );
781
+ delete this._networkStatusMonitor;
782
+ delete this._traceStart;
783
+ delete this._frameId;
784
+ delete this._loaderId;
785
+ delete this._pageUrl;
786
+ this._failingFrameLoadIds = [];
787
+ this._waitConditionPromises = [];
788
+ this.emit("tracingFinished");
789
+ }
790
+ waitForMaxTimeout(maxWaitForLoadedMs = MAX_TRACE_WAIT_TIME) {
791
+ return new Promise(
792
+ (resolve) => setTimeout(resolve, maxWaitForLoadedMs)
793
+ ).then(() => async () => {
794
+ log2.error("Neither network nor CPU idle time could be detected within timeout, wrapping up tracing");
795
+ return this.completeTracing();
796
+ });
797
+ }
798
+ };
799
+
800
+ // src/commands.ts
801
+ var log3 = logger3("@wdio/lighthouse-service:CommandHandler");
802
+ var TRACE_COMMANDS = ["click", "navigateTo", "url"];
803
+ function isCDPSessionOnMessageObject(data) {
804
+ return data !== null && typeof data === "object" && Object.prototype.hasOwnProperty.call(data, "params") && Object.prototype.hasOwnProperty.call(data, "method");
125
805
  }
126
- export * from './types.js';
806
+ var CommandHandler = class {
807
+ constructor(_session, _page, _driver, _options, _browser) {
808
+ this._session = _session;
809
+ this._page = _page;
810
+ this._driver = _driver;
811
+ this._options = _options;
812
+ this._browser = _browser;
813
+ this._networkHandler = new NetworkHandler(_session);
814
+ this._traceGatherer = new TraceGatherer(_session, _page, _driver);
815
+ this._pwaGatherer = new PWAGatherer(_session, _page, _driver);
816
+ _session.on("Page.loadEventFired", this._traceGatherer.onLoadEventFired.bind(this._traceGatherer));
817
+ _session.on("Page.frameNavigated", this._traceGatherer.onFrameNavigated.bind(this._traceGatherer));
818
+ _page.on("requestfailed", this._traceGatherer.onFrameLoadFail.bind(this._traceGatherer));
819
+ this._pwaGatherer = new PWAGatherer(_session, _page, _driver);
820
+ const commands = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
821
+ (fnName) => fnName !== "constructor" && !fnName.startsWith("_")
822
+ );
823
+ commands.forEach((fnName) => _browser.addCommand(
824
+ fnName,
825
+ this[fnName].bind(this)
826
+ ));
827
+ this._devtoolsGatherer = new DevtoolsGatherer();
828
+ _session.on("*", this._propagateWSEvents.bind(this));
829
+ }
830
+ _isTracing = false;
831
+ _networkHandler;
832
+ _traceEvents;
833
+ _shouldRunPerformanceAudits = false;
834
+ _cacheEnabled;
835
+ _cpuThrottling;
836
+ _networkThrottling;
837
+ _formFactor;
838
+ _traceGatherer;
839
+ _devtoolsGatherer;
840
+ _pwaGatherer;
841
+ /**
842
+ * Start tracing the browser. You can optionally pass in custom tracing categories and the
843
+ * sampling frequency.
844
+ */
845
+ startTracing({
846
+ categories = DEFAULT_TRACING_CATEGORIES,
847
+ path,
848
+ screenshots = true
849
+ } = {}) {
850
+ if (this._isTracing) {
851
+ throw new Error("browser is already being traced");
852
+ }
853
+ this._isTracing = true;
854
+ this._traceEvents = void 0;
855
+ return this._page.tracing.start({ categories, path, screenshots });
856
+ }
857
+ /**
858
+ * Stop tracing the browser.
859
+ */
860
+ async endTracing() {
861
+ if (!this._isTracing) {
862
+ throw new Error("No tracing was initiated, call `browser.startTracing()` first");
863
+ }
864
+ try {
865
+ const traceBuffer = await this._page.tracing.stop();
866
+ if (!traceBuffer) {
867
+ throw new Error("No tracebuffer captured");
868
+ }
869
+ this._traceEvents = JSON.parse(traceBuffer.toString());
870
+ this._isTracing = false;
871
+ } catch (err) {
872
+ throw new Error(`Couldn't parse trace events: ${err.message}`);
873
+ }
874
+ return this._traceEvents;
875
+ }
876
+ /**
877
+ * Returns the tracelogs that was captured within the tracing period.
878
+ * You can use this command to store the trace logs on the file system to analyse the trace
879
+ * via Chrome DevTools interface.
880
+ */
881
+ getTraceLogs() {
882
+ return this._traceEvents;
883
+ }
884
+ /**
885
+ * Returns page weight information of the last page load.
886
+ */
887
+ getPageWeight() {
888
+ const requestTypes = Object.values(this._networkHandler.requestTypes).filter(Boolean);
889
+ const pageWeight = sumByKey(requestTypes, "size");
890
+ const transferred = sumByKey(requestTypes, "encoded");
891
+ const requestCount = sumByKey(requestTypes, "count");
892
+ return { pageWeight, transferred, requestCount, details: this._networkHandler.requestTypes };
893
+ }
894
+ /**
895
+ * set flag to run performance audits for page transitions
896
+ */
897
+ enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor } = DEFAULT_THROTTLE_STATE) {
898
+ if (!NETWORK_STATES[networkThrottling]) {
899
+ throw new Error(`Network throttling profile "${networkThrottling}" is unknown, choose between ${Object.keys(NETWORK_STATES).join(", ")}`);
900
+ }
901
+ if (typeof cpuThrottling !== "number") {
902
+ throw new Error(`CPU throttling rate needs to be typeof number but was "${typeof cpuThrottling}"`);
903
+ }
904
+ this._networkThrottling = networkThrottling;
905
+ this._cpuThrottling = cpuThrottling;
906
+ this._cacheEnabled = Boolean(cacheEnabled);
907
+ this._formFactor = formFactor;
908
+ this._shouldRunPerformanceAudits = true;
909
+ }
910
+ /**
911
+ * custom command to disable performance audits
912
+ */
913
+ disablePerformanceAudits() {
914
+ this._shouldRunPerformanceAudits = false;
915
+ }
916
+ /**
917
+ * helper method to set throttling profile
918
+ */
919
+ async setThrottlingProfile(networkThrottling = DEFAULT_THROTTLE_STATE.networkThrottling, cpuThrottling = DEFAULT_THROTTLE_STATE.cpuThrottling, cacheEnabled = DEFAULT_THROTTLE_STATE.cacheEnabled) {
920
+ if (!this._page || !this._session) {
921
+ throw new Error("No page or session has been captured yet");
922
+ }
923
+ await this._page.setCacheEnabled(Boolean(cacheEnabled));
924
+ await this._session.send("Emulation.setCPUThrottlingRate", { rate: cpuThrottling });
925
+ await this._session.send("Network.emulateNetworkConditions", NETWORK_STATES[networkThrottling]);
926
+ }
927
+ async checkPWA(auditsToBeRun) {
928
+ const auditor = new Auditor();
929
+ const artifacts = await this._pwaGatherer.gatherData();
930
+ return auditor._auditPWA(artifacts, auditsToBeRun);
931
+ }
932
+ _propagateWSEvents(data) {
933
+ if (!isCDPSessionOnMessageObject(data)) {
934
+ return;
935
+ }
936
+ this._devtoolsGatherer?.onMessage(data);
937
+ const method = data.method || "event";
938
+ try {
939
+ log3.debug(`cdp event: ${method} with params ${JSON.stringify(data.params)}`);
940
+ } catch {
941
+ }
942
+ if (this._browser) {
943
+ this._browser.emit(method, data.params);
944
+ }
945
+ }
946
+ async _initCommand() {
947
+ await Promise.all(["Page", "Network", "Runtime"].map(
948
+ (domain) => Promise.all([
949
+ this._session?.send(`${domain}.enable`)
950
+ ])
951
+ ));
952
+ }
953
+ _beforeCmd(commandName, params) {
954
+ const isCommandNavigation = ["url", "navigateTo"].some((cmdName) => cmdName === commandName);
955
+ if (!this._shouldRunPerformanceAudits || !this._traceGatherer || this._traceGatherer.isTracing || !TRACE_COMMANDS.includes(commandName)) {
956
+ return;
957
+ }
958
+ this.setThrottlingProfile(this._networkThrottling, this._cpuThrottling, this._cacheEnabled);
959
+ const url = isCommandNavigation ? params[0] : CLICK_TRANSITION;
960
+ return this._traceGatherer.startTracing(url);
961
+ }
962
+ _afterCmd(commandName) {
963
+ if (!this._traceGatherer || !this._traceGatherer.isTracing || !TRACE_COMMANDS.includes(commandName)) {
964
+ return;
965
+ }
966
+ this._traceGatherer.once("tracingComplete", (traceEvents) => {
967
+ const auditor = new Auditor(traceEvents, this._devtoolsGatherer?.getLogs(), this._formFactor);
968
+ auditor.updateCommands(this._browser);
969
+ });
970
+ this._traceGatherer.once("tracingError", (err) => {
971
+ const auditor = new Auditor();
972
+ auditor.updateCommands(
973
+ this._browser,
974
+ /* istanbul ignore next */
975
+ () => {
976
+ throw new Error(`Couldn't capture performance due to: ${err.message}`);
977
+ }
978
+ );
979
+ });
980
+ return new Promise((resolve) => {
981
+ log3.info(`Wait until tracing for command ${commandName} finishes`);
982
+ this._traceGatherer?.once("tracingFinished", async () => {
983
+ log3.info("Disable throttling");
984
+ await this.setThrottlingProfile("online", 0, true);
985
+ log3.info("continuing with next WebDriver command");
986
+ resolve();
987
+ });
988
+ });
989
+ }
990
+ };
991
+
992
+ // src/index.ts
993
+ var DevToolsService = class {
994
+ constructor(_options) {
995
+ this._options = _options;
996
+ }
997
+ _command = [];
998
+ _browser;
999
+ async before(caps, specs, browser) {
1000
+ this._browser = browser;
1001
+ return await this._setupHandler();
1002
+ }
1003
+ async onReload() {
1004
+ if (!this._browser) {
1005
+ return;
1006
+ }
1007
+ return this._setupHandler();
1008
+ }
1009
+ async beforeCommand(commandName, params) {
1010
+ return Promise.all(this._command.map(async (c) => await c._beforeCmd(commandName, params)));
1011
+ }
1012
+ async afterCommand(commandName) {
1013
+ if (commandName === "switchToWindow") {
1014
+ await this._setupHandler();
1015
+ }
1016
+ return Promise.all(this._command.map(async (c) => await c._afterCmd(commandName)));
1017
+ }
1018
+ /**
1019
+ * set flag to run performance audits for page transitions
1020
+ */
1021
+ _enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor } = DEFAULT_THROTTLE_STATE) {
1022
+ if (!NETWORK_STATES[networkThrottling]) {
1023
+ throw new Error(`Network throttling profile "${networkThrottling}" is unknown, choose between ${Object.keys(NETWORK_STATES).join(", ")}`);
1024
+ }
1025
+ if (typeof cpuThrottling !== "number") {
1026
+ throw new Error(`CPU throttling rate needs to be typeof number but was "${typeof cpuThrottling}"`);
1027
+ }
1028
+ if (this._command.length === 1) {
1029
+ this._command[0].enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
1030
+ } else {
1031
+ for (const c of this._command) {
1032
+ c.enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
1033
+ }
1034
+ }
1035
+ }
1036
+ /**
1037
+ * custom command to disable performance audits
1038
+ */
1039
+ _disablePerformanceAudits() {
1040
+ if (this._command.length === 1) {
1041
+ this._command[0].disablePerformanceAudits();
1042
+ } else {
1043
+ for (const c of this._command) {
1044
+ c.disablePerformanceAudits();
1045
+ }
1046
+ }
1047
+ }
1048
+ async _setThrottlingProfile(networkThrottling = DEFAULT_THROTTLE_STATE.networkThrottling, cpuThrottling = DEFAULT_THROTTLE_STATE.cpuThrottling, cacheEnabled = DEFAULT_THROTTLE_STATE.cacheEnabled) {
1049
+ if (this._command.length === 1) {
1050
+ this._command[0].setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
1051
+ } else {
1052
+ for (const c of this._command) {
1053
+ c.setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
1054
+ }
1055
+ }
1056
+ }
1057
+ async _checkPWA(auditsToBeRun) {
1058
+ if (this._command.length === 1) {
1059
+ return await this._command[0].checkPWA(auditsToBeRun);
1060
+ }
1061
+ return Promise.all(this._command.map(async (c) => await c.checkPWA(auditsToBeRun)));
1062
+ }
1063
+ async _setupHandler() {
1064
+ if (!this._browser) {
1065
+ return;
1066
+ }
1067
+ this._command.length = 0;
1068
+ const browsers = Object.keys(this._browser).includes("sessionId") ? [this._browser] : this._browser.instances.map((i) => this._browser.getInstance(i));
1069
+ for (const browser of browsers) {
1070
+ const puppeteer = await browser.getPuppeteer().catch(() => void 0);
1071
+ if (!puppeteer) {
1072
+ return setUnsupportedCommand(browser);
1073
+ }
1074
+ const url = await browser.getUrl();
1075
+ const target = url !== "data:," ? await puppeteer.waitForTarget(
1076
+ /* istanbul ignore next */
1077
+ (t) => t.url().includes(url)
1078
+ ) : await puppeteer.waitForTarget(
1079
+ /* istanbul ignore next */
1080
+ // @ts-expect-error
1081
+ (t) => t.type() === "page" || Boolean(t._getTargetInfo().browserContextId)
1082
+ );
1083
+ if (!target) {
1084
+ throw new Error("No page target found");
1085
+ }
1086
+ const page = await target.page() || null;
1087
+ if (!page) {
1088
+ throw new Error("No page found");
1089
+ }
1090
+ const session = await target.createCDPSession();
1091
+ const driver = await getLighthouseDriver(session, target);
1092
+ const cmd = new CommandHandler(session, page, driver, this._options, browser);
1093
+ await cmd._initCommand();
1094
+ this._command.push(cmd);
1095
+ }
1096
+ this._browser.addCommand("enablePerformanceAudits", this._enablePerformanceAudits.bind(this));
1097
+ this._browser.addCommand("disablePerformanceAudits", this._disablePerformanceAudits.bind(this));
1098
+ this._browser.addCommand("checkPWA", this._checkPWA.bind(this));
1099
+ }
1100
+ };
1101
+ export {
1102
+ DevToolsService as default
1103
+ };