@wdio/lighthouse-service 9.32.0 → 10.0.0-alpha.149
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/AGENTS.md +10 -0
- package/README.md +8 -5
- package/build/auditor.d.ts +12 -37
- package/build/auditor.d.ts.map +1 -1
- package/build/commands.d.ts +21 -12
- package/build/commands.d.ts.map +1 -1
- package/build/constants.d.ts +13 -15
- package/build/constants.d.ts.map +1 -1
- package/build/handler/network.d.ts +1 -1
- package/build/handler/network.d.ts.map +1 -1
- package/build/index.d.ts +4 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +428 -574
- package/build/pwa.d.ts +47 -0
- package/build/pwa.d.ts.map +1 -0
- package/build/types.d.ts +96 -79
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +31 -6
- package/build/utils.d.ts.map +1 -1
- package/package.json +12 -21
- package/build/gatherer/devtools.d.ts +0 -19
- package/build/gatherer/devtools.d.ts.map +0 -1
- package/build/gatherer/pwa.d.ts +0 -28
- package/build/gatherer/pwa.d.ts.map +0 -1
- package/build/gatherer/trace.d.ts +0 -60
- package/build/gatherer/trace.d.ts.map +0 -1
- package/build/lighthouse/cri.d.ts +0 -22
- package/build/lighthouse/cri.d.ts.map +0 -1
- package/build/scripts/collectMetaElements.d.ts +0 -8
- package/build/scripts/collectMetaElements.d.ts.map +0 -1
- package/build/scripts/registerPerformanceObserverInPage.d.ts +0 -12
- package/build/scripts/registerPerformanceObserverInPage.d.ts.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
// src/commands.ts
|
|
2
2
|
import logger3 from "@wdio/logger";
|
|
3
|
+
import { desktopConfig, startFlow } from "lighthouse";
|
|
3
4
|
|
|
4
5
|
// 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
6
|
var DEFAULT_TRACING_CATEGORIES = [
|
|
15
7
|
// Exclude default categories. We'll be selective to minimize trace size
|
|
16
8
|
"-*",
|
|
@@ -35,13 +27,10 @@ var DEFAULT_TRACING_CATEGORIES = [
|
|
|
35
27
|
"disabled-by-default-devtools.screenshot",
|
|
36
28
|
// This doesn't add its own events, but adds a `stackTrace` property to devtools.timeline events
|
|
37
29
|
"disabled-by-default-devtools.timeline.stack",
|
|
38
|
-
// Additional categories used by
|
|
30
|
+
// Additional categories used by DevTools. Not used by Lighthouse, but included to facilitate
|
|
39
31
|
// loading traces from Lighthouse into the Performance panel.
|
|
40
32
|
"disabled-by-default-devtools.timeline.frame",
|
|
41
33
|
"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
34
|
];
|
|
46
35
|
var IGNORED_URLS = [
|
|
47
36
|
"data:,",
|
|
@@ -52,8 +41,6 @@ var IGNORED_URLS = [
|
|
|
52
41
|
// all chrome extensions
|
|
53
42
|
];
|
|
54
43
|
var FRAME_LOAD_START_TIMEOUT = 2e3;
|
|
55
|
-
var TRACING_TIMEOUT = 15e3;
|
|
56
|
-
var MAX_TRACE_WAIT_TIME = 45e3;
|
|
57
44
|
var DEFAULT_NETWORK_THROTTLING_STATE = "online";
|
|
58
45
|
var DEFAULT_FORM_FACTOR = "desktop";
|
|
59
46
|
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.";
|
|
@@ -84,17 +71,15 @@ var NETWORK_STATES = {
|
|
|
84
71
|
},
|
|
85
72
|
"Regular 3G": {
|
|
86
73
|
offline: false,
|
|
87
|
-
latency:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
uploadThroughput: Math.floor(throttling.mobileRegular3G.uploadThroughputKbps * 1024 / 8)
|
|
74
|
+
latency: 300 * 3.75,
|
|
75
|
+
downloadThroughput: Math.floor(700 * 1024 / 8),
|
|
76
|
+
uploadThroughput: Math.floor(700 * 1024 / 8)
|
|
91
77
|
},
|
|
92
78
|
"Good 3G": {
|
|
93
79
|
offline: false,
|
|
94
|
-
latency:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
uploadThroughput: Math.floor(throttling.mobileSlow4G.uploadThroughputKbps * 1024 / 8)
|
|
80
|
+
latency: 150 * 3.75,
|
|
81
|
+
downloadThroughput: Math.floor(1.6 * 1024 * 1024 / 8),
|
|
82
|
+
uploadThroughput: Math.floor(750 * 1024 / 8)
|
|
98
83
|
},
|
|
99
84
|
"Regular 4G": {
|
|
100
85
|
offline: false,
|
|
@@ -128,25 +113,17 @@ var DEFAULT_THROTTLE_STATE = {
|
|
|
128
113
|
cacheEnabled: false,
|
|
129
114
|
formFactor: DEFAULT_FORM_FACTOR
|
|
130
115
|
};
|
|
131
|
-
var
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
116
|
+
var PWA_AUDIT_NAMES = [
|
|
117
|
+
"isInstallable",
|
|
118
|
+
"serviceWorker",
|
|
119
|
+
"splashScreen",
|
|
120
|
+
"themedOmnibox",
|
|
121
|
+
"contentWith",
|
|
122
|
+
"viewport",
|
|
123
|
+
"appleTouchIcon",
|
|
124
|
+
"maskableIcon"
|
|
139
125
|
];
|
|
140
|
-
var
|
|
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
|
-
};
|
|
126
|
+
var TRACE_COMMANDS = ["click", "navigateTo", "url"];
|
|
150
127
|
|
|
151
128
|
// src/handler/network.ts
|
|
152
129
|
var NetworkHandler = class {
|
|
@@ -256,35 +233,6 @@ var NetworkHandler = class {
|
|
|
256
233
|
}
|
|
257
234
|
};
|
|
258
235
|
|
|
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
236
|
// src/utils.ts
|
|
289
237
|
var CUSTOM_COMMANDS = [
|
|
290
238
|
"getMetrics",
|
|
@@ -294,7 +242,11 @@ var CUSTOM_COMMANDS = [
|
|
|
294
242
|
"enablePerformanceAudits",
|
|
295
243
|
"disablePerformanceAudits",
|
|
296
244
|
"getMainThreadWorkBreakdown",
|
|
297
|
-
"checkPWA"
|
|
245
|
+
"checkPWA",
|
|
246
|
+
"getPerformanceScore",
|
|
247
|
+
"getTraceLogs",
|
|
248
|
+
"getPageWeight",
|
|
249
|
+
"endTracing"
|
|
298
250
|
];
|
|
299
251
|
function setUnsupportedCommand(browser) {
|
|
300
252
|
for (const command of CUSTOM_COMMANDS) {
|
|
@@ -314,104 +266,100 @@ function sumByKey(list, key) {
|
|
|
314
266
|
function isSupportedUrl(url) {
|
|
315
267
|
return IGNORED_URLS.filter((ignoredUrl) => url.startsWith(ignoredUrl)).length === 0;
|
|
316
268
|
}
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
269
|
+
function parseTraceBuffer(buffer) {
|
|
270
|
+
const raw = typeof buffer === "string" ? buffer : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).toString("utf8");
|
|
271
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
272
|
+
if (Array.isArray(parsed)) {
|
|
273
|
+
return { traceEvents: parsed };
|
|
321
274
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!cUrl.pathname.startsWith("/devtools/browser")) {
|
|
325
|
-
await cdpConnection._connectToSocket({
|
|
326
|
-
webSocketDebuggerUrl: connection.url(),
|
|
327
|
-
id: (await target.asPage()).mainFrame()._id
|
|
328
|
-
});
|
|
329
|
-
const { sessionId } = await cdpConnection.sendCommand(
|
|
330
|
-
"Target.attachToTarget",
|
|
331
|
-
void 0,
|
|
332
|
-
{ targetId: (await target.asPage()).mainFrame()._id, flatten: true }
|
|
333
|
-
);
|
|
334
|
-
cdpConnection.setSessionId(sessionId);
|
|
335
|
-
return new Driver(cdpConnection);
|
|
275
|
+
if (parsed && typeof parsed === "object") {
|
|
276
|
+
return parsed;
|
|
336
277
|
}
|
|
337
|
-
|
|
338
|
-
await cdpConnection._connectToSocket(list[0]);
|
|
339
|
-
return new Driver(cdpConnection);
|
|
278
|
+
throw new Error("Trace buffer did not contain JSON trace data");
|
|
340
279
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
280
|
+
function isInternalBrowserPage(url) {
|
|
281
|
+
return url.includes("BiDi-CDP Mapper") || url.startsWith("devtools://") || url.startsWith("chrome-extension://");
|
|
282
|
+
}
|
|
283
|
+
function pageMatchesUrl(pageUrl, currentUrl) {
|
|
284
|
+
return pageUrl === currentUrl || pageUrl.includes(currentUrl) || currentUrl.includes(pageUrl);
|
|
285
|
+
}
|
|
286
|
+
function resolveAuditablePage(pages, currentUrl) {
|
|
287
|
+
const candidates = pages.filter((page) => !isInternalBrowserPage(page.url()));
|
|
288
|
+
if (currentUrl && currentUrl !== "data:,") {
|
|
289
|
+
const match = candidates.find((page) => pageMatchesUrl(page.url(), currentUrl));
|
|
290
|
+
if (match) {
|
|
291
|
+
return match;
|
|
292
|
+
}
|
|
347
293
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
294
|
+
return candidates.find((page) => isSupportedUrl(page.url())) ?? candidates.at(-1);
|
|
295
|
+
}
|
|
296
|
+
async function getAuditablePuppeteerPage(puppeteer, currentUrl) {
|
|
297
|
+
const pages = await puppeteer.pages();
|
|
298
|
+
const fromPages = resolveAuditablePage(pages, currentUrl);
|
|
299
|
+
if (fromPages) {
|
|
300
|
+
return fromPages;
|
|
353
301
|
}
|
|
354
|
-
|
|
302
|
+
if (!puppeteer.waitForTarget) {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
const target = currentUrl && currentUrl !== "data:," ? await puppeteer.waitForTarget(async (t) => pageMatchesUrl(t.url(), currentUrl) && !isInternalBrowserPage(t.url()) && Boolean(await t.page())) : await puppeteer.waitForTarget(async (t) => (t.type?.() === "page" || Boolean(await t.page())) && !isInternalBrowserPage(t.url()));
|
|
306
|
+
return await target?.page() ?? void 0;
|
|
307
|
+
}
|
|
308
|
+
var CORE_METRIC_KEYS = [
|
|
309
|
+
"firstContentfulPaint",
|
|
310
|
+
"largestContentfulPaint",
|
|
311
|
+
"speedIndex"
|
|
312
|
+
];
|
|
313
|
+
function metricValue(lhr, auditId, metricKey) {
|
|
314
|
+
const audits = lhr.audits ?? {};
|
|
315
|
+
const metrics = audits.metrics?.details?.items?.[0] ?? {};
|
|
316
|
+
const value = metrics[metricKey] ?? audits[auditId]?.numericValue;
|
|
317
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
318
|
+
}
|
|
319
|
+
function hasCorePerformanceMetrics(lhr) {
|
|
320
|
+
if (!lhr) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return CORE_METRIC_KEYS.every((key) => {
|
|
324
|
+
const auditId = key === "firstContentfulPaint" ? "first-contentful-paint" : key === "largestContentfulPaint" ? "largest-contentful-paint" : "speed-index";
|
|
325
|
+
return metricValue(lhr, auditId, key) !== void 0;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function selectLighthouseResult(result) {
|
|
329
|
+
const steps = result?.steps ?? [];
|
|
330
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
331
|
+
if (hasCorePerformanceMetrics(steps[i]?.lhr)) {
|
|
332
|
+
return steps[i].lhr;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return steps.at(-1)?.lhr;
|
|
336
|
+
}
|
|
337
|
+
function describeLighthouseResult(lhr) {
|
|
338
|
+
const url = lhr?.finalDisplayedUrl || lhr?.finalUrl || "unknown";
|
|
339
|
+
const runtimeError = lhr?.runtimeError ? `${lhr.runtimeError.code ?? "unknown"}: ${lhr.runtimeError.message ?? ""}`.trim() : "none";
|
|
340
|
+
return `url=${url} runtimeError=${runtimeError}`;
|
|
341
|
+
}
|
|
355
342
|
|
|
356
343
|
// src/auditor.ts
|
|
357
|
-
import Diagnostics from "lighthouse/lighthouse-core/audits/diagnostics.js";
|
|
358
|
-
import MainThreadWorkBreakdown from "lighthouse/lighthouse-core/audits/mainthread-work-breakdown.js";
|
|
359
|
-
import Metrics from "lighthouse/lighthouse-core/audits/metrics.js";
|
|
360
|
-
import ServerResponseTime from "lighthouse/lighthouse-core/audits/server-response-time.js";
|
|
361
|
-
import CumulativeLayoutShift from "lighthouse/lighthouse-core/audits/metrics/cumulative-layout-shift.js";
|
|
362
|
-
import FirstContentfulPaint from "lighthouse/lighthouse-core/audits/metrics/first-contentful-paint.js";
|
|
363
|
-
import LargestContentfulPaint from "lighthouse/lighthouse-core/audits/metrics/largest-contentful-paint.js";
|
|
364
|
-
import SpeedIndex from "lighthouse/lighthouse-core/audits/metrics/speed-index.js";
|
|
365
|
-
import InteractiveMetric from "lighthouse/lighthouse-core/audits/metrics/interactive.js";
|
|
366
|
-
import TotalBlockingTime from "lighthouse/lighthouse-core/audits/metrics/total-blocking-time.js";
|
|
367
|
-
import ReportScoring from "lighthouse/lighthouse-core/scoring.js";
|
|
368
|
-
import defaultConfig from "lighthouse/lighthouse-core/config/default-config.js";
|
|
369
344
|
import logger from "@wdio/logger";
|
|
370
345
|
var log = logger("@wdio/lighthouse-service:Auditor");
|
|
346
|
+
function asNumber(value) {
|
|
347
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
348
|
+
}
|
|
349
|
+
function roundMetric(value) {
|
|
350
|
+
const numeric = asNumber(value);
|
|
351
|
+
return numeric === void 0 ? void 0 : Math.round(numeric);
|
|
352
|
+
}
|
|
371
353
|
var Auditor = class {
|
|
372
|
-
constructor(
|
|
373
|
-
this.
|
|
374
|
-
this._devtoolsLogs = _devtoolsLogs;
|
|
354
|
+
constructor(_lhr, _formFactor) {
|
|
355
|
+
this._lhr = _lhr;
|
|
375
356
|
this._formFactor = _formFactor;
|
|
376
|
-
if (_traceLogs) {
|
|
377
|
-
this._url = _traceLogs.pageUrl;
|
|
378
|
-
}
|
|
379
357
|
}
|
|
380
|
-
|
|
381
|
-
_devtoolsLogs;
|
|
358
|
+
_lhr;
|
|
382
359
|
_formFactor;
|
|
383
|
-
_url;
|
|
384
|
-
_audit(AUDIT, params = {}) {
|
|
385
|
-
const auditContext = {
|
|
386
|
-
options: {
|
|
387
|
-
...AUDIT.defaultOptions
|
|
388
|
-
},
|
|
389
|
-
settings: {
|
|
390
|
-
throttlingMethod: "devtools",
|
|
391
|
-
formFactor: this._formFactor || DEFAULT_FORM_FACTOR
|
|
392
|
-
},
|
|
393
|
-
LighthouseRunWarnings: false,
|
|
394
|
-
computedCache: /* @__PURE__ */ new Map()
|
|
395
|
-
};
|
|
396
|
-
try {
|
|
397
|
-
return AUDIT.audit({
|
|
398
|
-
traces: { defaultPass: this._traceLogs },
|
|
399
|
-
devtoolsLogs: { defaultPass: this._devtoolsLogs },
|
|
400
|
-
TestedAsMobileDevice: true,
|
|
401
|
-
GatherContext: { gatherMode: "navigation" },
|
|
402
|
-
...params
|
|
403
|
-
}, auditContext);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
log.error(error);
|
|
406
|
-
return {
|
|
407
|
-
score: 0,
|
|
408
|
-
error
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
360
|
/**
|
|
413
|
-
* an Auditor instance is created for every
|
|
414
|
-
* function to receive the latest performance metrics
|
|
361
|
+
* an Auditor instance is created for every Lighthouse run so provide an
|
|
362
|
+
* updateCommands function to receive the latest performance metrics
|
|
415
363
|
*/
|
|
416
364
|
updateCommands(browser, customFn) {
|
|
417
365
|
const commands = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
|
|
@@ -420,428 +368,230 @@ var Auditor = class {
|
|
|
420
368
|
commands.forEach((fnName) => browser.addCommand(fnName, customFn || this[fnName].bind(this)));
|
|
421
369
|
}
|
|
422
370
|
/**
|
|
423
|
-
* Returns a list with a breakdown of all main thread
|
|
371
|
+
* Returns a list with a breakdown of all main thread tasks and their total duration
|
|
424
372
|
*/
|
|
425
373
|
async getMainThreadWorkBreakdown() {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
374
|
+
const items = this._lhr?.audits?.["mainthread-work-breakdown"]?.details?.items;
|
|
375
|
+
if (!Array.isArray(items)) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
return items.map((item) => ({
|
|
379
|
+
group: String(item.group ?? ""),
|
|
380
|
+
duration: asNumber(item.duration) ?? 0
|
|
381
|
+
}));
|
|
430
382
|
}
|
|
431
383
|
/**
|
|
432
384
|
* Get some useful diagnostics about the page load
|
|
433
385
|
*/
|
|
434
386
|
async getDiagnostics() {
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
387
|
+
const details = this._lhr?.audits?.diagnostics?.details;
|
|
388
|
+
if (!details || !Array.isArray(details.items) || !details.items[0]) {
|
|
437
389
|
return null;
|
|
438
390
|
}
|
|
439
|
-
return
|
|
391
|
+
return details.items[0];
|
|
440
392
|
}
|
|
441
393
|
/**
|
|
442
|
-
* Get most
|
|
394
|
+
* Get most commonly used performance metrics
|
|
443
395
|
*/
|
|
444
396
|
async getMetrics() {
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const result = await this._audit(Metrics);
|
|
448
|
-
const metrics = result.details.items[0] || {};
|
|
397
|
+
const audits = this._lhr?.audits ?? {};
|
|
398
|
+
const metrics = audits.metrics?.details?.items?.[0] ?? {};
|
|
449
399
|
return {
|
|
450
|
-
timeToFirstByte:
|
|
451
|
-
serverResponseTime:
|
|
452
|
-
domContentLoaded: metrics.observedDomContentLoaded,
|
|
453
|
-
firstVisualChange: metrics.observedFirstVisualChange,
|
|
454
|
-
firstPaint: metrics.observedFirstPaint,
|
|
455
|
-
firstContentfulPaint: metrics.firstContentfulPaint,
|
|
456
|
-
firstMeaningfulPaint: metrics.firstMeaningfulPaint,
|
|
457
|
-
largestContentfulPaint: metrics.largestContentfulPaint,
|
|
458
|
-
lastVisualChange: metrics.observedLastVisualChange,
|
|
459
|
-
interactive: metrics.interactive,
|
|
460
|
-
load: metrics.observedLoad,
|
|
461
|
-
speedIndex: metrics.speedIndex,
|
|
462
|
-
totalBlockingTime: metrics.totalBlockingTime,
|
|
463
|
-
maxPotentialFID: metrics.maxPotentialFID,
|
|
464
|
-
cumulativeLayoutShift:
|
|
400
|
+
timeToFirstByte: roundMetric(audits["server-response-time"]?.numericValue),
|
|
401
|
+
serverResponseTime: roundMetric(audits["server-response-time"]?.numericValue),
|
|
402
|
+
domContentLoaded: asNumber(metrics.observedDomContentLoaded),
|
|
403
|
+
firstVisualChange: asNumber(metrics.observedFirstVisualChange),
|
|
404
|
+
firstPaint: asNumber(metrics.observedFirstPaint),
|
|
405
|
+
firstContentfulPaint: asNumber(metrics.firstContentfulPaint ?? audits["first-contentful-paint"]?.numericValue),
|
|
406
|
+
firstMeaningfulPaint: asNumber(metrics.firstMeaningfulPaint),
|
|
407
|
+
largestContentfulPaint: asNumber(metrics.largestContentfulPaint ?? audits["largest-contentful-paint"]?.numericValue),
|
|
408
|
+
lastVisualChange: asNumber(metrics.observedLastVisualChange),
|
|
409
|
+
interactive: asNumber(metrics.interactive ?? audits.interactive?.numericValue),
|
|
410
|
+
load: asNumber(metrics.observedLoad),
|
|
411
|
+
speedIndex: asNumber(metrics.speedIndex ?? audits["speed-index"]?.numericValue),
|
|
412
|
+
totalBlockingTime: asNumber(metrics.totalBlockingTime ?? audits["total-blocking-time"]?.numericValue),
|
|
413
|
+
maxPotentialFID: asNumber(metrics.maxPotentialFID ?? audits["max-potential-fid"]?.numericValue),
|
|
414
|
+
cumulativeLayoutShift: asNumber(
|
|
415
|
+
audits["cumulative-layout-shift"]?.numericValue ?? metrics.cumulativeLayoutShift
|
|
416
|
+
),
|
|
417
|
+
interactionToNextPaint: asNumber(audits["interaction-to-next-paint"]?.numericValue)
|
|
465
418
|
};
|
|
466
419
|
}
|
|
467
420
|
/**
|
|
468
|
-
* Returns the Lighthouse Performance Score
|
|
421
|
+
* Returns the Lighthouse Performance Score, a weighted mean of FCP, SI, LCP, TBT and CLS.
|
|
469
422
|
*/
|
|
470
423
|
async getPerformanceScore() {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
"
|
|
474
|
-
"largest-contentful-paint": await this._audit(LargestContentfulPaint),
|
|
475
|
-
"cumulative-layout-shift": await this._audit(CumulativeLayoutShift),
|
|
476
|
-
"total-blocking-time": await this._audit(TotalBlockingTime),
|
|
477
|
-
interactive: await this._audit(InteractiveMetric)
|
|
478
|
-
};
|
|
479
|
-
if (!auditResults.interactive || !auditResults["cumulative-layout-shift"] || !auditResults["first-contentful-paint"] || !auditResults["largest-contentful-paint"] || !auditResults["speed-index"] || !auditResults["total-blocking-time"]) {
|
|
480
|
-
log.info("One or multiple required metrics couldn't be found, setting performance score to: null");
|
|
424
|
+
const score = this._lhr?.categories?.performance?.score;
|
|
425
|
+
if (typeof score !== "number" || Number.isNaN(score)) {
|
|
426
|
+
log.info("Performance score couldn't be found, setting performance score to: null");
|
|
481
427
|
return null;
|
|
482
428
|
}
|
|
483
|
-
|
|
484
|
-
score: auditResults[auditRef.id].score,
|
|
485
|
-
weight: auditRef.weight
|
|
486
|
-
}));
|
|
487
|
-
return ReportScoring.arithmeticMean(scores);
|
|
488
|
-
}
|
|
489
|
-
async _auditPWA(params, auditsToBeRun = Object.keys(PWA_AUDITS)) {
|
|
490
|
-
const audits = await Promise.all(
|
|
491
|
-
Object.entries(PWA_AUDITS).filter(([name]) => auditsToBeRun.includes(name)).map(
|
|
492
|
-
async ([name, Audit]) => [name, await this._audit(Audit, params)]
|
|
493
|
-
)
|
|
494
|
-
);
|
|
495
|
-
return {
|
|
496
|
-
passed: !audits.find(([, result]) => result.score < 1),
|
|
497
|
-
details: audits.reduce((details, [name, result]) => {
|
|
498
|
-
details[name] = result;
|
|
499
|
-
return details;
|
|
500
|
-
}, {})
|
|
501
|
-
};
|
|
429
|
+
return score;
|
|
502
430
|
}
|
|
503
431
|
};
|
|
504
432
|
|
|
505
|
-
// src/
|
|
506
|
-
import
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (!selector || realMatchesFn.call(el, selector)) {
|
|
523
|
-
metas.push(el);
|
|
524
|
-
}
|
|
525
|
-
if (el.shadowRoot) {
|
|
526
|
-
_findAllElements(el.shadowRoot.querySelectorAll("*"));
|
|
527
|
-
}
|
|
433
|
+
// src/pwa.ts
|
|
434
|
+
import logger2 from "@wdio/logger";
|
|
435
|
+
var log2 = logger2("@wdio/lighthouse-service:PWA");
|
|
436
|
+
function passed(displayValue, explanation) {
|
|
437
|
+
return { score: 1, displayValue, explanation };
|
|
438
|
+
}
|
|
439
|
+
function failed(displayValue, explanation) {
|
|
440
|
+
return { score: 0, displayValue, explanation };
|
|
441
|
+
}
|
|
442
|
+
function parseIconSize(sizes) {
|
|
443
|
+
if (!sizes || sizes === "any") {
|
|
444
|
+
return sizes === "any" ? Number.POSITIVE_INFINITY : 0;
|
|
445
|
+
}
|
|
446
|
+
return sizes.split(/\s+/).reduce((max, entry) => {
|
|
447
|
+
const match = entry.match(/^(\d+)x(\d+)$/i);
|
|
448
|
+
if (!match) {
|
|
449
|
+
return max;
|
|
528
450
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
451
|
+
return Math.max(max, Math.min(Number(match[1]), Number(match[2])));
|
|
452
|
+
}, 0);
|
|
453
|
+
}
|
|
454
|
+
function hasIconOfSize(icons = [], minSize) {
|
|
455
|
+
return icons.some((icon) => parseIconSize(icon.sizes) >= minSize);
|
|
456
|
+
}
|
|
457
|
+
async function collectPageState(page) {
|
|
458
|
+
return page.evaluate(async () => {
|
|
459
|
+
const manifestLink = document.querySelector('link[rel~="manifest"]');
|
|
460
|
+
const viewport = document.querySelector('meta[name="viewport"]');
|
|
461
|
+
const appleTouch = document.querySelector('link[rel~="apple-touch-icon"]');
|
|
462
|
+
const themeColor = document.querySelector('meta[name="theme-color"]');
|
|
463
|
+
let manifest = null;
|
|
464
|
+
let manifestParseError;
|
|
465
|
+
const manifestHref = manifestLink?.href;
|
|
466
|
+
if (manifestHref) {
|
|
467
|
+
try {
|
|
468
|
+
const response = await fetch(manifestHref, { credentials: "same-origin" });
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
manifestParseError = `Failed to fetch manifest: HTTP ${response.status}`;
|
|
471
|
+
} else {
|
|
472
|
+
manifest = await response.json();
|
|
473
|
+
}
|
|
474
|
+
} catch (error) {
|
|
475
|
+
manifestParseError = error instanceof Error ? error.message : String(error);
|
|
536
476
|
}
|
|
537
|
-
|
|
538
|
-
|
|
477
|
+
}
|
|
478
|
+
let hasServiceWorker = false;
|
|
479
|
+
try {
|
|
480
|
+
const registration = await navigator.serviceWorker?.getRegistration();
|
|
481
|
+
hasServiceWorker = Boolean(registration);
|
|
482
|
+
} catch {
|
|
483
|
+
hasServiceWorker = false;
|
|
484
|
+
}
|
|
539
485
|
return {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
486
|
+
url: location.href,
|
|
487
|
+
manifestHref,
|
|
488
|
+
manifest,
|
|
489
|
+
manifestParseError,
|
|
490
|
+
viewportContent: viewport?.content,
|
|
491
|
+
appleTouchIconHref: appleTouch?.href,
|
|
492
|
+
themeColor: themeColor?.content,
|
|
493
|
+
innerWidth: window.innerWidth,
|
|
494
|
+
documentWidth: document.documentElement.clientWidth,
|
|
495
|
+
hasServiceWorker
|
|
548
496
|
};
|
|
549
497
|
});
|
|
550
498
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
this._frGatherer = new FRGatherer(this._session);
|
|
559
|
-
this._networkRecorder = new NetworkRecorder();
|
|
560
|
-
NETWORK_RECORDER_EVENTS.forEach((method) => {
|
|
561
|
-
this._session.on(method, (params) => this._networkRecorder.dispatch({ method, params }));
|
|
562
|
-
});
|
|
563
|
-
this._page.on("load", () => {
|
|
564
|
-
this._networkRecords = this._networkRecorder.getRawRecords();
|
|
565
|
-
delete this._networkRecorder;
|
|
566
|
-
this._networkRecorder = new NetworkRecorder();
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
_session;
|
|
570
|
-
_page;
|
|
571
|
-
_driver;
|
|
572
|
-
_frGatherer;
|
|
573
|
-
_networkRecorder;
|
|
574
|
-
_networkRecords = [];
|
|
575
|
-
async gatherData() {
|
|
576
|
-
const pageUrl = await this._page?.url();
|
|
577
|
-
const passContext = {
|
|
578
|
-
url: pageUrl,
|
|
579
|
-
driver: this._driver
|
|
580
|
-
};
|
|
581
|
-
const loadData = {
|
|
582
|
-
networkRecords: this._networkRecords
|
|
583
|
-
};
|
|
584
|
-
const linkElements = new LinkElements();
|
|
585
|
-
const viewportDimensions = new ViewportDimensions();
|
|
586
|
-
const { registrations } = await serviceWorkers.getServiceWorkerRegistrations(this._frGatherer);
|
|
587
|
-
const { versions } = await serviceWorkers.getServiceWorkerVersions(this._frGatherer);
|
|
588
|
-
return {
|
|
589
|
-
URL: { requestedUrl: pageUrl, finalUrl: pageUrl },
|
|
590
|
-
WebAppManifest: await WebAppManifest.getWebAppManifest(this._frGatherer, pageUrl),
|
|
591
|
-
InstallabilityErrors: await InstallabilityErrors.getInstallabilityErrors(this._frGatherer),
|
|
592
|
-
MetaElements: await this._driver.evaluate(collectMetaElements, {
|
|
593
|
-
args: [],
|
|
594
|
-
useIsolation: true,
|
|
595
|
-
deps: [pageFunctions.getElementsInDocument]
|
|
596
|
-
}),
|
|
597
|
-
ViewportDimensions: await viewportDimensions.afterPass(passContext),
|
|
598
|
-
ServiceWorker: { versions, registrations },
|
|
599
|
-
LinkElements: await linkElements.afterPass(passContext, loadData)
|
|
600
|
-
};
|
|
499
|
+
async function getInstallabilityErrors(session) {
|
|
500
|
+
try {
|
|
501
|
+
const result = await session.send("Page.getInstallabilityErrors");
|
|
502
|
+
return result.installabilityErrors ?? result.errors ?? [];
|
|
503
|
+
} catch (error) {
|
|
504
|
+
log2.debug(`Page.getInstallabilityErrors is unavailable: ${error.message}`);
|
|
505
|
+
return [];
|
|
601
506
|
}
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
// src/gatherer/trace.ts
|
|
605
|
-
import { EventEmitter } from "node:events";
|
|
606
|
-
import NetworkRecorder2 from "lighthouse/lighthouse-core/lib/network-recorder.js";
|
|
607
|
-
import NetworkMonitor from "lighthouse/lighthouse-core/gather/driver/network-monitor.js";
|
|
608
|
-
import ProtocolSession from "lighthouse/lighthouse-core/fraggle-rock/gather/session.js";
|
|
609
|
-
import { waitForFullyLoaded } from "lighthouse/lighthouse-core/gather/driver/wait-for-condition.js";
|
|
610
|
-
import logger2 from "@wdio/logger";
|
|
611
|
-
|
|
612
|
-
// src/scripts/registerPerformanceObserverInPage.ts
|
|
613
|
-
function registerPerformanceObserverInPage() {
|
|
614
|
-
window.____lastLongTask = window.performance.now();
|
|
615
|
-
const observer = new window.PerformanceObserver((entryList) => {
|
|
616
|
-
const entries = entryList.getEntries();
|
|
617
|
-
for (const entry of entries) {
|
|
618
|
-
if (entry.entryType === "longtask") {
|
|
619
|
-
const taskEnd = entry.startTime + entry.duration;
|
|
620
|
-
window.____lastLongTask = Math.max(window.____lastLongTask, taskEnd);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
observer.observe({ entryTypes: ["longtask"] });
|
|
625
|
-
window.____lhPerformanceObserver = observer;
|
|
626
507
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
508
|
+
function evaluatePWAChecks(state, installabilityErrors) {
|
|
509
|
+
const manifest = state.manifest;
|
|
510
|
+
const icons = manifest?.icons ?? [];
|
|
511
|
+
const display = manifest?.display;
|
|
512
|
+
const hasName = Boolean(manifest?.name || manifest?.short_name);
|
|
513
|
+
const hasStartUrl = Boolean(manifest?.start_url);
|
|
514
|
+
const hasValidDisplay = ["fullscreen", "standalone", "minimal-ui"].includes(String(display));
|
|
515
|
+
const preferRelatedApps = manifest?.prefer_related_applications === true;
|
|
516
|
+
const installableByManifest = Boolean(
|
|
517
|
+
state.manifestHref && manifest && hasName && hasStartUrl && hasValidDisplay && !preferRelatedApps && hasIconOfSize(icons, 192)
|
|
518
|
+
);
|
|
519
|
+
const blockingInstallErrors = installabilityErrors.filter((error) => error.errorId && error.errorId !== "already-installed");
|
|
520
|
+
const isInstallable = blockingInstallErrors.length === 0 && installableByManifest ? passed("installable-manifest", "Web app manifest meets installability requirements") : failed(
|
|
521
|
+
"installable-manifest",
|
|
522
|
+
blockingInstallErrors.length ? `Installability errors: ${blockingInstallErrors.map((error) => error.errorId).join(", ")}` : state.manifestParseError || "Web app manifest does not meet installability requirements"
|
|
523
|
+
);
|
|
524
|
+
const serviceWorker = state.hasServiceWorker ? passed("service-worker", "A service worker controls this page") : failed("service-worker", "No service worker registration was found for this page");
|
|
525
|
+
const splashScreen = manifest && hasName && manifest.background_color && hasIconOfSize(icons, 512) ? passed("splash-screen", "Manifest provides a custom splash screen") : failed("splash-screen", "Manifest is missing name, background_color, or a 512px icon");
|
|
526
|
+
const themedOmnibox = state.themeColor || manifest?.theme_color ? passed("themed-omnibox", "A theme color is provided via manifest or meta tag") : failed("themed-omnibox", "No theme-color meta tag or manifest.theme_color was found");
|
|
527
|
+
const hasViewport = Boolean(state.viewportContent && /width=|initial-scale=/i.test(state.viewportContent));
|
|
528
|
+
const viewport = hasViewport ? passed("viewport", "A viewport meta tag is configured") : failed("viewport", "No usable viewport meta tag was found");
|
|
529
|
+
const contentFits = !hasViewport || Math.abs(state.innerWidth - state.documentWidth) <= 1;
|
|
530
|
+
const contentWith = contentFits ? passed("content-width", "Content size matches the viewport") : failed("content-width", `Content width (${state.documentWidth}) does not match viewport width (${state.innerWidth})`);
|
|
531
|
+
const appleTouchIcon = state.appleTouchIconHref ? passed("apple-touch-icon", "An Apple touch icon is present") : failed("apple-touch-icon", "No apple-touch-icon link was found");
|
|
532
|
+
const maskableIcon = icons.some((icon) => String(icon.purpose || "").split(/\s+/).includes("maskable")) ? passed("maskable-icon", "Manifest includes a maskable icon") : failed("maskable-icon", "Manifest does not include a maskable icon");
|
|
533
|
+
return {
|
|
534
|
+
isInstallable,
|
|
535
|
+
serviceWorker,
|
|
536
|
+
splashScreen,
|
|
537
|
+
themedOmnibox,
|
|
538
|
+
contentWith,
|
|
539
|
+
viewport,
|
|
540
|
+
appleTouchIcon,
|
|
541
|
+
maskableIcon
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
var PWAAuditor = class {
|
|
545
|
+
constructor(_session, _page) {
|
|
633
546
|
this._session = _session;
|
|
634
547
|
this._page = _page;
|
|
635
|
-
this._driver = _driver;
|
|
636
|
-
NETWORK_RECORDER_EVENTS.forEach((method) => {
|
|
637
|
-
this._networkListeners[method] = (params) => this._networkStatusMonitor.dispatch({ method, params });
|
|
638
|
-
});
|
|
639
|
-
this._protocolSession = new ProtocolSession(_session);
|
|
640
|
-
this._networkMonitor = new NetworkMonitor(_session);
|
|
641
548
|
}
|
|
642
549
|
_session;
|
|
643
550
|
_page;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
_frameId;
|
|
649
|
-
_loaderId;
|
|
650
|
-
_pageUrl;
|
|
651
|
-
_networkStatusMonitor;
|
|
652
|
-
_networkMonitor;
|
|
653
|
-
_protocolSession;
|
|
654
|
-
_trace;
|
|
655
|
-
_traceStart;
|
|
656
|
-
_clickTraceTimeout;
|
|
657
|
-
_waitConditionPromises = [];
|
|
658
|
-
async startTracing(url) {
|
|
659
|
-
delete this._trace;
|
|
660
|
-
this._networkStatusMonitor = new NetworkRecorder2();
|
|
661
|
-
NETWORK_RECORDER_EVENTS.forEach((method) => {
|
|
662
|
-
this._session.on(method, this._networkListeners[method]);
|
|
663
|
-
});
|
|
664
|
-
this._traceStart = Date.now();
|
|
665
|
-
log2.info(`Start tracing frame with url ${url}`);
|
|
666
|
-
await this._driver.beginTrace();
|
|
667
|
-
if (url === CLICK_TRANSITION) {
|
|
668
|
-
log2.info("Start checking for page load for click");
|
|
669
|
-
this._clickTraceTimeout = setTimeout(async () => {
|
|
670
|
-
log2.info("No page load detected, canceling trace");
|
|
671
|
-
return this.finishTracing();
|
|
672
|
-
}, FRAME_LOAD_START_TIMEOUT);
|
|
673
|
-
}
|
|
674
|
-
await this._page.evaluateOnNewDocument(registerPerformanceObserverInPage);
|
|
675
|
-
this._waitConditionPromises.push(
|
|
676
|
-
waitForFullyLoaded(this._protocolSession, this._networkMonitor, { timedOut: 1 })
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* store frame id of frames that are being traced
|
|
681
|
-
*/
|
|
682
|
-
async onFrameNavigated(msgObj) {
|
|
683
|
-
if (!this.isTracing) {
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
if (this._failingFrameLoadIds.includes(msgObj.frame.url)) {
|
|
687
|
-
this._waitConditionPromises = [];
|
|
688
|
-
this._frameId = '"unsuccessful loaded frame"';
|
|
689
|
-
this.finishTracing();
|
|
690
|
-
this.emit("tracingError", new Error(`Page with url "${msgObj.frame.url}" failed to load`));
|
|
691
|
-
if (this._clickTraceTimeout) {
|
|
692
|
-
clearTimeout(this._clickTraceTimeout);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (
|
|
696
|
-
// we already detected a frameId before
|
|
697
|
-
this._frameId || // the event was thrown for a sub frame (e.g. iframe)
|
|
698
|
-
msgObj.frame.parentId || // we don't support the url of given frame
|
|
699
|
-
!isSupportedUrl(msgObj.frame.url)
|
|
700
|
-
) {
|
|
701
|
-
log2.info(`Ignore navigated frame with url ${msgObj.frame.url}`);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
this._frameId = msgObj.frame.id;
|
|
705
|
-
this._loaderId = msgObj.frame.loaderId;
|
|
706
|
-
this._pageUrl = msgObj.frame.url;
|
|
707
|
-
log2.info(`Page load detected: ${this._pageUrl}, set frameId ${this._frameId}, set loaderId ${this._loaderId}`);
|
|
708
|
-
if (this._clickTraceTimeout && !this._pageLoadDetected) {
|
|
709
|
-
log2.info("Page load detected for click, clearing click trace timeout}");
|
|
710
|
-
this._pageLoadDetected = true;
|
|
711
|
-
clearTimeout(this._clickTraceTimeout);
|
|
712
|
-
}
|
|
713
|
-
this.emit("tracingStarted", msgObj.frame.id);
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* once the page load event has fired, we can grab some performance
|
|
717
|
-
* metrics and timing
|
|
718
|
-
*/
|
|
719
|
-
async onLoadEventFired() {
|
|
720
|
-
if (!this.isTracing) {
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
const loadPromise = Promise.all(this._waitConditionPromises).then(() => async () => {
|
|
724
|
-
const minTraceTime = TRACING_TIMEOUT - (Date.now() - (this._traceStart || 0));
|
|
725
|
-
if (minTraceTime > 0) {
|
|
726
|
-
log2.info(`page load happen to quick, waiting ${minTraceTime}ms more`);
|
|
727
|
-
await new Promise((resolve) => setTimeout(resolve, minTraceTime));
|
|
728
|
-
}
|
|
729
|
-
return this.completeTracing();
|
|
730
|
-
});
|
|
731
|
-
const cleanupFn = await Promise.race([
|
|
732
|
-
loadPromise,
|
|
733
|
-
this.waitForMaxTimeout()
|
|
551
|
+
async audit(auditsToBeRun = [...PWA_AUDIT_NAMES]) {
|
|
552
|
+
const [state, installabilityErrors] = await Promise.all([
|
|
553
|
+
collectPageState(this._page),
|
|
554
|
+
getInstallabilityErrors(this._session)
|
|
734
555
|
]);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
onFrameLoadFail(request) {
|
|
739
|
-
const frame = request.frame();
|
|
740
|
-
if (frame) {
|
|
741
|
-
this._failingFrameLoadIds.push(frame.url());
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
get isTracing() {
|
|
745
|
-
return typeof this._traceStart === "number";
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* once tracing has finished capture trace logs into memory
|
|
749
|
-
*/
|
|
750
|
-
async completeTracing() {
|
|
751
|
-
const traceDuration = Date.now() - (this._traceStart || 0);
|
|
752
|
-
log2.info(`Tracing completed after ${traceDuration}ms, capturing performance data for frame ${this._frameId}`);
|
|
753
|
-
try {
|
|
754
|
-
const traceEvents = await this._driver.endTrace();
|
|
755
|
-
const startedInBrowserEvt = traceEvents.traceEvents.find((e) => e.name === "TracingStartedInBrowser");
|
|
756
|
-
const mainFrame = startedInBrowserEvt && startedInBrowserEvt.args && startedInBrowserEvt.args.data.frames?.find((frame) => !frame.parent);
|
|
757
|
-
if (mainFrame && mainFrame.processId) {
|
|
758
|
-
const threadNameEvt = traceEvents.traceEvents.find((e) => e.ph === "R" && e.cat === "blink.user_timing" && e.name === "navigationStart" && e.args.data.isLoadingMainFrame);
|
|
759
|
-
if (threadNameEvt) {
|
|
760
|
-
log2.info(`Replace mainFrame process id ${mainFrame.processId} with actual thread process id ${threadNameEvt.pid}`);
|
|
761
|
-
mainFrame.processId = threadNameEvt.pid;
|
|
762
|
-
} else {
|
|
763
|
-
log2.info(`Couldn't replace mainFrame process id ${mainFrame.processId} with actual thread process id`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
this._trace = {
|
|
767
|
-
...traceEvents,
|
|
768
|
-
frameId: this._frameId,
|
|
769
|
-
loaderId: this._loaderId,
|
|
770
|
-
pageUrl: this._pageUrl,
|
|
771
|
-
traceStart: this._traceStart,
|
|
772
|
-
traceEnd: Date.now()
|
|
773
|
-
};
|
|
774
|
-
this.emit("tracingComplete", this._trace);
|
|
775
|
-
this.finishTracing();
|
|
776
|
-
} catch (err) {
|
|
777
|
-
log2.error(`Error capturing tracing logs: ${err.stack}`);
|
|
778
|
-
this.emit("tracingError", err);
|
|
779
|
-
return this.finishTracing();
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* clear tracing states and emit tracingFinished
|
|
784
|
-
*/
|
|
785
|
-
finishTracing() {
|
|
786
|
-
log2.info(`Tracing for ${this._frameId} completed`);
|
|
787
|
-
this._pageLoadDetected = false;
|
|
788
|
-
NETWORK_RECORDER_EVENTS.forEach(
|
|
789
|
-
(method) => this._session.off(method, this._networkListeners[method])
|
|
556
|
+
const allDetails = evaluatePWAChecks(state, installabilityErrors);
|
|
557
|
+
const details = Object.fromEntries(
|
|
558
|
+
Object.entries(allDetails).filter(([name]) => auditsToBeRun.includes(name))
|
|
790
559
|
);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
delete this._pageUrl;
|
|
796
|
-
this._failingFrameLoadIds = [];
|
|
797
|
-
this._waitConditionPromises = [];
|
|
798
|
-
this.emit("tracingFinished");
|
|
799
|
-
}
|
|
800
|
-
waitForMaxTimeout(maxWaitForLoadedMs = MAX_TRACE_WAIT_TIME) {
|
|
801
|
-
return new Promise(
|
|
802
|
-
(resolve) => setTimeout(resolve, maxWaitForLoadedMs)
|
|
803
|
-
).then(() => async () => {
|
|
804
|
-
log2.error("Neither network nor CPU idle time could be detected within timeout, wrapping up tracing");
|
|
805
|
-
return this.completeTracing();
|
|
806
|
-
});
|
|
560
|
+
return {
|
|
561
|
+
passed: !Object.values(details).some((result) => result.score < 1),
|
|
562
|
+
details
|
|
563
|
+
};
|
|
807
564
|
}
|
|
808
565
|
};
|
|
809
566
|
|
|
810
567
|
// src/commands.ts
|
|
811
568
|
var log3 = logger3("@wdio/lighthouse-service:CommandHandler");
|
|
812
|
-
var TRACE_COMMANDS = ["click", "navigateTo", "url"];
|
|
813
569
|
function isCDPSessionOnMessageObject(data) {
|
|
814
570
|
return data !== null && typeof data === "object" && Object.prototype.hasOwnProperty.call(data, "params") && Object.prototype.hasOwnProperty.call(data, "method");
|
|
815
571
|
}
|
|
572
|
+
function isTraceCommand(commandName) {
|
|
573
|
+
return TRACE_COMMANDS.includes(commandName);
|
|
574
|
+
}
|
|
816
575
|
var CommandHandler = class {
|
|
817
|
-
constructor(_session, _page,
|
|
576
|
+
constructor(_session, _page, _options, _browser) {
|
|
818
577
|
this._session = _session;
|
|
819
578
|
this._page = _page;
|
|
820
|
-
this._driver = _driver;
|
|
821
579
|
this._options = _options;
|
|
822
580
|
this._browser = _browser;
|
|
823
581
|
this._networkHandler = new NetworkHandler(_session);
|
|
824
|
-
this.
|
|
825
|
-
|
|
826
|
-
_session.on("Page.loadEventFired", this._traceGatherer.onLoadEventFired.bind(this._traceGatherer));
|
|
827
|
-
_session.on("Page.frameNavigated", this._traceGatherer.onFrameNavigated.bind(this._traceGatherer));
|
|
828
|
-
_page.on("requestfailed", this._traceGatherer.onFrameLoadFail.bind(this._traceGatherer));
|
|
829
|
-
this._pwaGatherer = new PWAGatherer(_session, _page, _driver);
|
|
582
|
+
this._pwaAuditor = new PWAAuditor(_session, _page);
|
|
583
|
+
_session.on("Page.frameNavigated", this._onFrameNavigated);
|
|
830
584
|
const commands = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
|
|
831
585
|
(fnName) => fnName !== "constructor" && !fnName.startsWith("_")
|
|
832
586
|
);
|
|
833
587
|
commands.forEach((fnName) => _browser.addCommand(
|
|
834
588
|
fnName,
|
|
835
|
-
this[fnName].bind(this)
|
|
836
|
-
{}
|
|
837
|
-
// Use the non-deprecated overload with options
|
|
589
|
+
this[fnName].bind(this)
|
|
838
590
|
));
|
|
839
|
-
this._devtoolsGatherer = new DevtoolsGatherer();
|
|
840
591
|
_session.on("*", this._propagateWSEvents.bind(this));
|
|
841
592
|
}
|
|
842
593
|
_session;
|
|
843
594
|
_page;
|
|
844
|
-
_driver;
|
|
845
595
|
_options;
|
|
846
596
|
_browser;
|
|
847
597
|
_isTracing = false;
|
|
@@ -852,9 +602,12 @@ var CommandHandler = class {
|
|
|
852
602
|
_cpuThrottling;
|
|
853
603
|
_networkThrottling;
|
|
854
604
|
_formFactor;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
605
|
+
_pwaAuditor;
|
|
606
|
+
_flow;
|
|
607
|
+
_flowInProgress = false;
|
|
608
|
+
_pageLoadDetected = false;
|
|
609
|
+
_clickTraceTimeout;
|
|
610
|
+
_onFrameNavigated = this._handleFrameNavigated.bind(this);
|
|
858
611
|
/**
|
|
859
612
|
* Start tracing the browser. You can optionally pass in custom tracing categories and the
|
|
860
613
|
* sampling frequency.
|
|
@@ -883,9 +636,11 @@ var CommandHandler = class {
|
|
|
883
636
|
if (!traceBuffer) {
|
|
884
637
|
throw new Error("No tracebuffer captured");
|
|
885
638
|
}
|
|
886
|
-
|
|
639
|
+
const parsed = parseTraceBuffer(traceBuffer);
|
|
640
|
+
this._traceEvents = parsed.traceEvents ?? parsed;
|
|
887
641
|
this._isTracing = false;
|
|
888
642
|
} catch (err) {
|
|
643
|
+
this._isTracing = false;
|
|
889
644
|
throw new Error(`Couldn't parse trace events: ${err.message}`);
|
|
890
645
|
}
|
|
891
646
|
return this._traceEvents;
|
|
@@ -942,15 +697,12 @@ var CommandHandler = class {
|
|
|
942
697
|
await this._session.send("Network.emulateNetworkConditions", NETWORK_STATES[networkThrottling]);
|
|
943
698
|
}
|
|
944
699
|
async checkPWA(auditsToBeRun) {
|
|
945
|
-
|
|
946
|
-
const artifacts = await this._pwaGatherer.gatherData();
|
|
947
|
-
return auditor._auditPWA(artifacts, auditsToBeRun);
|
|
700
|
+
return this._pwaAuditor.audit(auditsToBeRun);
|
|
948
701
|
}
|
|
949
702
|
_propagateWSEvents(data) {
|
|
950
703
|
if (!isCDPSessionOnMessageObject(data)) {
|
|
951
704
|
return;
|
|
952
705
|
}
|
|
953
|
-
this._devtoolsGatherer?.onMessage(data);
|
|
954
706
|
const method = data.method || "event";
|
|
955
707
|
try {
|
|
956
708
|
log3.debug(`cdp event: ${method} with params ${JSON.stringify(data.params)}`);
|
|
@@ -967,24 +719,57 @@ var CommandHandler = class {
|
|
|
967
719
|
])
|
|
968
720
|
));
|
|
969
721
|
}
|
|
970
|
-
_beforeCmd(commandName, params) {
|
|
971
|
-
const isCommandNavigation = ["url", "navigateTo"].
|
|
972
|
-
if (!this._shouldRunPerformanceAudits ||
|
|
722
|
+
async _beforeCmd(commandName, params) {
|
|
723
|
+
const isCommandNavigation = ["url", "navigateTo"].includes(commandName);
|
|
724
|
+
if (!this._shouldRunPerformanceAudits || this._flowInProgress || !isTraceCommand(commandName)) {
|
|
973
725
|
return;
|
|
974
726
|
}
|
|
975
|
-
this.setThrottlingProfile(this._networkThrottling, this._cpuThrottling, this._cacheEnabled);
|
|
727
|
+
await this.setThrottlingProfile(this._networkThrottling, this._cpuThrottling, this._cacheEnabled);
|
|
976
728
|
const url = isCommandNavigation ? params[0] : CLICK_TRANSITION;
|
|
977
|
-
|
|
729
|
+
this._pageLoadDetected = false;
|
|
730
|
+
this._flowInProgress = true;
|
|
731
|
+
if (url === CLICK_TRANSITION) {
|
|
732
|
+
this._clickTraceTimeout = setTimeout(() => {
|
|
733
|
+
if (!this._pageLoadDetected) {
|
|
734
|
+
log3.info("No page load detected, canceling Lighthouse navigation");
|
|
735
|
+
}
|
|
736
|
+
}, FRAME_LOAD_START_TIMEOUT);
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
await this._refreshAuditablePage();
|
|
740
|
+
this._flow = await startFlow(this._page, this._getFlowOptions());
|
|
741
|
+
await this._flow.startNavigation({ name: `WebdriverIO ${commandName}` });
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this._resetFlowState();
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
978
746
|
}
|
|
979
|
-
_afterCmd(commandName) {
|
|
980
|
-
if (!this.
|
|
747
|
+
async _afterCmd(commandName) {
|
|
748
|
+
if (!this._flow || !this._flowInProgress || !isTraceCommand(commandName)) {
|
|
981
749
|
return;
|
|
982
750
|
}
|
|
983
|
-
|
|
984
|
-
|
|
751
|
+
const skipClickWithoutNavigation = commandName === "click" && !this._pageLoadDetected;
|
|
752
|
+
try {
|
|
753
|
+
if (skipClickWithoutNavigation) {
|
|
754
|
+
log3.info("Click did not trigger a page load, skipping Lighthouse navigation");
|
|
755
|
+
await this._cancelNavigation("click did not trigger a page load");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
await this._flow.endNavigation();
|
|
759
|
+
let lhr = selectLighthouseResult(await this._flow.createFlowResult());
|
|
760
|
+
if (!hasCorePerformanceMetrics(lhr)) {
|
|
761
|
+
log3.warn(`Wrapped navigation produced no performance metrics (${describeLighthouseResult(lhr)})`);
|
|
762
|
+
lhr = await this._navigateWithLighthouse() ?? lhr;
|
|
763
|
+
}
|
|
764
|
+
if (!hasCorePerformanceMetrics(lhr)) {
|
|
765
|
+
throw new Error(`Lighthouse did not capture core performance metrics (${describeLighthouseResult(lhr)})`);
|
|
766
|
+
}
|
|
767
|
+
log3.info(`Captured Lighthouse performance metrics (${describeLighthouseResult(lhr)})`);
|
|
768
|
+
const auditor = new Auditor(lhr, this._formFactor);
|
|
985
769
|
auditor.updateCommands(this._browser);
|
|
986
|
-
})
|
|
987
|
-
|
|
770
|
+
} catch (err) {
|
|
771
|
+
log3.error(`Couldn't capture performance due to: ${err.message}`);
|
|
772
|
+
this._disposeFlow();
|
|
988
773
|
const auditor = new Auditor();
|
|
989
774
|
auditor.updateCommands(
|
|
990
775
|
this._browser,
|
|
@@ -993,16 +778,107 @@ var CommandHandler = class {
|
|
|
993
778
|
throw new Error(`Couldn't capture performance due to: ${err.message}`);
|
|
994
779
|
}
|
|
995
780
|
);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
log3.info(
|
|
999
|
-
this.
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
781
|
+
} finally {
|
|
782
|
+
this._resetFlowState();
|
|
783
|
+
log3.info("Disable throttling");
|
|
784
|
+
await this.setThrottlingProfile("online", 0, true);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
_getFlowOptions() {
|
|
788
|
+
const requestedFormFactor = this._formFactor;
|
|
789
|
+
const formFactor = requestedFormFactor === "mobile" ? "mobile" : "desktop";
|
|
790
|
+
return {
|
|
791
|
+
name: "WebdriverIO performance flow",
|
|
792
|
+
config: formFactor === "desktop" ? desktopConfig : void 0,
|
|
793
|
+
flags: {
|
|
794
|
+
formFactor,
|
|
795
|
+
/**
|
|
796
|
+
* Let Lighthouse apply its own desktop/mobile viewport. The Puppeteer
|
|
797
|
+
* connection uses `defaultViewport: null`, and headless Chrome on some
|
|
798
|
+
* CI hosts otherwise paints into a 0×0 window (no FCP / empty LHR).
|
|
799
|
+
*/
|
|
800
|
+
...requestedFormFactor === "none" ? { screenEmulation: { disabled: true } } : {},
|
|
801
|
+
throttlingMethod: "provided",
|
|
802
|
+
disableStorageReset: true,
|
|
803
|
+
skipAboutBlank: true,
|
|
804
|
+
onlyCategories: ["performance"],
|
|
805
|
+
logLevel: "error"
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
_handleFrameNavigated(event) {
|
|
810
|
+
if (!this._flowInProgress || event.frame?.parentId || !event.frame?.url || !isSupportedUrl(event.frame.url)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this._pageLoadDetected = true;
|
|
814
|
+
if (this._clickTraceTimeout) {
|
|
815
|
+
clearTimeout(this._clickTraceTimeout);
|
|
816
|
+
this._clickTraceTimeout = void 0;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async _refreshAuditablePage() {
|
|
820
|
+
try {
|
|
821
|
+
const puppeteer = this._page.browser();
|
|
822
|
+
const currentUrl = await this._browser.getUrl();
|
|
823
|
+
const page = await getAuditablePuppeteerPage(puppeteer, currentUrl);
|
|
824
|
+
if (page) {
|
|
825
|
+
this._page = page;
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
log3.debug(`Could not refresh Puppeteer page: ${error.message}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async _navigateWithLighthouse() {
|
|
832
|
+
const currentUrl = await this._browser.getUrl();
|
|
833
|
+
if (!isSupportedUrl(currentUrl)) {
|
|
834
|
+
log3.warn(`Cannot fall back to Lighthouse navigate for unsupported url: ${currentUrl}`);
|
|
835
|
+
return void 0;
|
|
836
|
+
}
|
|
837
|
+
log3.info(`Falling back to Lighthouse-controlled navigation (${currentUrl})`);
|
|
838
|
+
await this._refreshAuditablePage();
|
|
839
|
+
if (!this._flow?.navigate) {
|
|
840
|
+
this._disposeFlow();
|
|
841
|
+
this._flow = await startFlow(this._page, this._getFlowOptions());
|
|
842
|
+
}
|
|
843
|
+
await this._flow.navigate(currentUrl);
|
|
844
|
+
return selectLighthouseResult(await this._flow.createFlowResult());
|
|
845
|
+
}
|
|
846
|
+
async _cancelNavigation(reason) {
|
|
847
|
+
this._disposeFlow();
|
|
848
|
+
try {
|
|
849
|
+
await Promise.race([
|
|
850
|
+
this._flow?.endNavigation() ?? Promise.resolve(),
|
|
851
|
+
new Promise((_, reject) => {
|
|
852
|
+
setTimeout(() => reject(new Error(reason)), FRAME_LOAD_START_TIMEOUT);
|
|
853
|
+
})
|
|
854
|
+
]);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
log3.debug(`Lighthouse navigation cancelled: ${error.message}`);
|
|
857
|
+
}
|
|
858
|
+
const auditor = new Auditor();
|
|
859
|
+
auditor.updateCommands(
|
|
860
|
+
this._browser,
|
|
861
|
+
/* istanbul ignore next */
|
|
862
|
+
() => {
|
|
863
|
+
throw new Error(`Couldn't capture performance due to: ${reason}`);
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
_disposeFlow() {
|
|
868
|
+
try {
|
|
869
|
+
this._flow?.dispose?.();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
log3.debug(`Failed to dispose Lighthouse flow: ${error.message}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
_resetFlowState() {
|
|
875
|
+
if (this._clickTraceTimeout) {
|
|
876
|
+
clearTimeout(this._clickTraceTimeout);
|
|
877
|
+
this._clickTraceTimeout = void 0;
|
|
878
|
+
}
|
|
879
|
+
this._flow = void 0;
|
|
880
|
+
this._flowInProgress = false;
|
|
881
|
+
this._pageLoadDetected = false;
|
|
1006
882
|
}
|
|
1007
883
|
};
|
|
1008
884
|
|
|
@@ -1043,33 +919,21 @@ var DevToolsService = class {
|
|
|
1043
919
|
if (typeof cpuThrottling !== "number") {
|
|
1044
920
|
throw new Error(`CPU throttling rate needs to be typeof number but was "${typeof cpuThrottling}"`);
|
|
1045
921
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
} else {
|
|
1049
|
-
for (const c of this._command) {
|
|
1050
|
-
c.enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
|
|
1051
|
-
}
|
|
922
|
+
for (const c of this._command) {
|
|
923
|
+
c.enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor });
|
|
1052
924
|
}
|
|
1053
925
|
}
|
|
1054
926
|
/**
|
|
1055
927
|
* custom command to disable performance audits
|
|
1056
928
|
*/
|
|
1057
929
|
_disablePerformanceAudits() {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
} else {
|
|
1061
|
-
for (const c of this._command) {
|
|
1062
|
-
c.disablePerformanceAudits();
|
|
1063
|
-
}
|
|
930
|
+
for (const c of this._command) {
|
|
931
|
+
c.disablePerformanceAudits();
|
|
1064
932
|
}
|
|
1065
933
|
}
|
|
1066
934
|
async _setThrottlingProfile(networkThrottling = DEFAULT_THROTTLE_STATE.networkThrottling, cpuThrottling = DEFAULT_THROTTLE_STATE.cpuThrottling, cacheEnabled = DEFAULT_THROTTLE_STATE.cacheEnabled) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
} else {
|
|
1070
|
-
for (const c of this._command) {
|
|
1071
|
-
c.setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
|
|
1072
|
-
}
|
|
935
|
+
for (const c of this._command) {
|
|
936
|
+
c.setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled);
|
|
1073
937
|
}
|
|
1074
938
|
}
|
|
1075
939
|
async _checkPWA(auditsToBeRun) {
|
|
@@ -1090,22 +954,12 @@ var DevToolsService = class {
|
|
|
1090
954
|
return setUnsupportedCommand(browser2);
|
|
1091
955
|
}
|
|
1092
956
|
const url = await browser2.getUrl();
|
|
1093
|
-
const
|
|
1094
|
-
async (t) => t.url().includes(url) && !t.url().includes("BiDi-CDP Mapper") && Boolean(await t.page())
|
|
1095
|
-
) : await puppeteer.waitForTarget(
|
|
1096
|
-
async (t) => t.type() === "page" || // @ts-expect-error
|
|
1097
|
-
Boolean(t._getTargetInfo().browserContextId) && !!await t.page()
|
|
1098
|
-
);
|
|
1099
|
-
if (!target) {
|
|
1100
|
-
throw new Error("No page target found");
|
|
1101
|
-
}
|
|
1102
|
-
const page = await target.page();
|
|
957
|
+
const page = await getAuditablePuppeteerPage(puppeteer, url);
|
|
1103
958
|
if (!page) {
|
|
1104
959
|
throw new Error("No page found");
|
|
1105
960
|
}
|
|
1106
|
-
const session = await target.createCDPSession();
|
|
1107
|
-
const
|
|
1108
|
-
const cmd = new CommandHandler(session, page, driver, this._options, browser2);
|
|
961
|
+
const session = await page.target().createCDPSession();
|
|
962
|
+
const cmd = new CommandHandler(session, page, this._options, browser2);
|
|
1109
963
|
await cmd._initCommand();
|
|
1110
964
|
this._command.push(cmd);
|
|
1111
965
|
}
|