@wdio/lighthouse-service 9.0.0-alpha.321
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/LICENSE-MIT +20 -0
- package/README.md +258 -0
- package/build/auditor.d.ts +54 -0
- package/build/auditor.d.ts.map +1 -0
- package/build/auditor.js +147 -0
- package/build/commands.d.ts +86 -0
- package/build/commands.d.ts.map +1 -0
- package/build/commands.js +212 -0
- package/build/constants.d.ts +95 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +150 -0
- package/build/gatherer/devtools.d.ts +19 -0
- package/build/gatherer/devtools.d.ts.map +1 -0
- package/build/gatherer/devtools.js +12 -0
- package/build/gatherer/pwa.d.ts +28 -0
- package/build/gatherer/pwa.d.ts.map +1 -0
- package/build/gatherer/pwa.js +66 -0
- package/build/gatherer/trace.d.ts +61 -0
- package/build/gatherer/trace.d.ts.map +1 -0
- package/build/gatherer/trace.js +233 -0
- package/build/handler/network.d.ts +45 -0
- package/build/handler/network.d.ts.map +1 -0
- package/build/handler/network.js +133 -0
- package/build/index.d.ts +50 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +126 -0
- package/build/lighthouse/cri.d.ts +23 -0
- package/build/lighthouse/cri.d.ts.map +1 -0
- package/build/lighthouse/cri.js +29 -0
- package/build/scripts/collectMetaElements.d.ts +8 -0
- package/build/scripts/collectMetaElements.d.ts.map +1 -0
- package/build/scripts/collectMetaElements.js +37 -0
- package/build/scripts/registerPerformanceObserverInPage.d.ts +12 -0
- package/build/scripts/registerPerformanceObserverInPage.d.ts.map +1 -0
- package/build/scripts/registerPerformanceObserverInPage.js +23 -0
- package/build/types.d.ts +168 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +1 -0
- package/build/utils.d.ts +23 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +64 -0
- package/package.json +66 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import NetworkRecorder from 'lighthouse/lighthouse-core/lib/network-recorder.js';
|
|
3
|
+
import NetworkMonitor from 'lighthouse/lighthouse-core/gather/driver/network-monitor.js';
|
|
4
|
+
import ProtocolSession from 'lighthouse/lighthouse-core/fraggle-rock/gather/session.js';
|
|
5
|
+
import { waitForFullyLoaded } from 'lighthouse/lighthouse-core/gather/driver/wait-for-condition.js';
|
|
6
|
+
import logger from '@wdio/logger';
|
|
7
|
+
import registerPerformanceObserverInPage from '../scripts/registerPerformanceObserverInPage.js';
|
|
8
|
+
import { FRAME_LOAD_START_TIMEOUT, TRACING_TIMEOUT, MAX_TRACE_WAIT_TIME, CLICK_TRANSITION, NETWORK_RECORDER_EVENTS } from '../constants.js';
|
|
9
|
+
import { isSupportedUrl } from '../utils.js';
|
|
10
|
+
const log = logger('@wdio/lighthouse-service:TraceGatherer');
|
|
11
|
+
export default class TraceGatherer extends EventEmitter {
|
|
12
|
+
_session;
|
|
13
|
+
_page;
|
|
14
|
+
_driver;
|
|
15
|
+
_failingFrameLoadIds = [];
|
|
16
|
+
_pageLoadDetected = false;
|
|
17
|
+
_networkListeners = {};
|
|
18
|
+
_frameId;
|
|
19
|
+
_loaderId;
|
|
20
|
+
_pageUrl;
|
|
21
|
+
_networkStatusMonitor;
|
|
22
|
+
_networkMonitor;
|
|
23
|
+
_protocolSession;
|
|
24
|
+
_trace;
|
|
25
|
+
_traceStart;
|
|
26
|
+
_clickTraceTimeout;
|
|
27
|
+
_waitConditionPromises = [];
|
|
28
|
+
constructor(_session, _page, _driver) {
|
|
29
|
+
super();
|
|
30
|
+
this._session = _session;
|
|
31
|
+
this._page = _page;
|
|
32
|
+
this._driver = _driver;
|
|
33
|
+
NETWORK_RECORDER_EVENTS.forEach((method) => {
|
|
34
|
+
this._networkListeners[method] = (params) => this._networkStatusMonitor.dispatch({ method, params });
|
|
35
|
+
});
|
|
36
|
+
this._protocolSession = new ProtocolSession(_session);
|
|
37
|
+
this._networkMonitor = new NetworkMonitor(_session);
|
|
38
|
+
}
|
|
39
|
+
async startTracing(url) {
|
|
40
|
+
/**
|
|
41
|
+
* delete old trace
|
|
42
|
+
*/
|
|
43
|
+
delete this._trace;
|
|
44
|
+
/**
|
|
45
|
+
* register listener for network status monitoring
|
|
46
|
+
*/
|
|
47
|
+
this._networkStatusMonitor = new NetworkRecorder();
|
|
48
|
+
NETWORK_RECORDER_EVENTS.forEach((method) => {
|
|
49
|
+
this._session.on(method, this._networkListeners[method]);
|
|
50
|
+
});
|
|
51
|
+
this._traceStart = Date.now();
|
|
52
|
+
log.info(`Start tracing frame with url ${url}`);
|
|
53
|
+
await this._driver.beginTrace();
|
|
54
|
+
/**
|
|
55
|
+
* if this tracing was started from a click transition
|
|
56
|
+
* then we want to discard page trace if no load detected
|
|
57
|
+
*/
|
|
58
|
+
if (url === CLICK_TRANSITION) {
|
|
59
|
+
log.info('Start checking for page load for click');
|
|
60
|
+
this._clickTraceTimeout = setTimeout(async () => {
|
|
61
|
+
log.info('No page load detected, canceling trace');
|
|
62
|
+
return this.finishTracing();
|
|
63
|
+
}, FRAME_LOAD_START_TIMEOUT);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* register performance observer
|
|
67
|
+
*/
|
|
68
|
+
await this._page.evaluateOnNewDocument(registerPerformanceObserverInPage);
|
|
69
|
+
this._waitConditionPromises.push(waitForFullyLoaded(this._protocolSession, this._networkMonitor, { timedOut: 1 }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* store frame id of frames that are being traced
|
|
73
|
+
*/
|
|
74
|
+
async onFrameNavigated(msgObj) {
|
|
75
|
+
if (!this.isTracing) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* page load failed, cancel tracing
|
|
80
|
+
*/
|
|
81
|
+
if (this._failingFrameLoadIds.includes(msgObj.frame.url)) {
|
|
82
|
+
this._waitConditionPromises = [];
|
|
83
|
+
this._frameId = '"unsuccessful loaded frame"';
|
|
84
|
+
this.finishTracing();
|
|
85
|
+
this.emit('tracingError', new Error(`Page with url "${msgObj.frame.url}" failed to load`));
|
|
86
|
+
if (this._clickTraceTimeout) {
|
|
87
|
+
clearTimeout(this._clickTraceTimeout);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* ignore event if
|
|
92
|
+
*/
|
|
93
|
+
if (
|
|
94
|
+
// we already detected a frameId before
|
|
95
|
+
this._frameId ||
|
|
96
|
+
// the event was thrown for a sub frame (e.g. iframe)
|
|
97
|
+
msgObj.frame.parentId ||
|
|
98
|
+
// we don't support the url of given frame
|
|
99
|
+
!isSupportedUrl(msgObj.frame.url)) {
|
|
100
|
+
log.info(`Ignore navigated frame with url ${msgObj.frame.url}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this._frameId = msgObj.frame.id;
|
|
104
|
+
this._loaderId = msgObj.frame.loaderId;
|
|
105
|
+
this._pageUrl = msgObj.frame.url;
|
|
106
|
+
log.info(`Page load detected: ${this._pageUrl}, set frameId ${this._frameId}, set loaderId ${this._loaderId}`);
|
|
107
|
+
/**
|
|
108
|
+
* clear click tracing timeout if it's still waiting
|
|
109
|
+
*
|
|
110
|
+
* the reason we have to tie this to Page.frameNavigated instead of Page.frameStartedLoading
|
|
111
|
+
* is because the latter can sometimes occur without the former, which will cause a hang
|
|
112
|
+
* e.g. with duolingo's sign-in button
|
|
113
|
+
*/
|
|
114
|
+
if (this._clickTraceTimeout && !this._pageLoadDetected) {
|
|
115
|
+
log.info('Page load detected for click, clearing click trace timeout}');
|
|
116
|
+
this._pageLoadDetected = true;
|
|
117
|
+
clearTimeout(this._clickTraceTimeout);
|
|
118
|
+
}
|
|
119
|
+
this.emit('tracingStarted', msgObj.frame.id);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* once the page load event has fired, we can grab some performance
|
|
123
|
+
* metrics and timing
|
|
124
|
+
*/
|
|
125
|
+
async onLoadEventFired() {
|
|
126
|
+
if (!this.isTracing) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Ensure that page is fully loaded and all metrics can be calculated.
|
|
131
|
+
*/
|
|
132
|
+
const loadPromise = Promise.all(this._waitConditionPromises).then(() => async () => {
|
|
133
|
+
/**
|
|
134
|
+
* ensure that we trace at least for 5s to ensure that we can
|
|
135
|
+
* calculate "interactive"
|
|
136
|
+
*/
|
|
137
|
+
const minTraceTime = TRACING_TIMEOUT - (Date.now() - (this._traceStart || 0));
|
|
138
|
+
if (minTraceTime > 0) {
|
|
139
|
+
log.info(`page load happen to quick, waiting ${minTraceTime}ms more`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, minTraceTime));
|
|
141
|
+
}
|
|
142
|
+
return this.completeTracing();
|
|
143
|
+
});
|
|
144
|
+
const cleanupFn = await Promise.race([
|
|
145
|
+
loadPromise,
|
|
146
|
+
this.waitForMaxTimeout()
|
|
147
|
+
]);
|
|
148
|
+
this._waitConditionPromises = [];
|
|
149
|
+
return cleanupFn();
|
|
150
|
+
}
|
|
151
|
+
onFrameLoadFail(request) {
|
|
152
|
+
const frame = request.frame();
|
|
153
|
+
if (frame) {
|
|
154
|
+
this._failingFrameLoadIds.push(frame.url());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
get isTracing() {
|
|
158
|
+
return typeof this._traceStart === 'number';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* once tracing has finished capture trace logs into memory
|
|
162
|
+
*/
|
|
163
|
+
async completeTracing() {
|
|
164
|
+
const traceDuration = Date.now() - (this._traceStart || 0);
|
|
165
|
+
log.info(`Tracing completed after ${traceDuration}ms, capturing performance data for frame ${this._frameId}`);
|
|
166
|
+
/**
|
|
167
|
+
* download all tracing data
|
|
168
|
+
* in case it fails, continue without capturing any data
|
|
169
|
+
*/
|
|
170
|
+
try {
|
|
171
|
+
const traceEvents = await this._driver.endTrace();
|
|
172
|
+
/**
|
|
173
|
+
* modify pid of renderer frame to be the same as where tracing was started
|
|
174
|
+
* possibly related to https://github.com/GoogleChrome/lighthouse/issues/6968
|
|
175
|
+
*/
|
|
176
|
+
const startedInBrowserEvt = traceEvents.traceEvents.find(e => e.name === 'TracingStartedInBrowser');
|
|
177
|
+
const mainFrame = (startedInBrowserEvt &&
|
|
178
|
+
startedInBrowserEvt.args &&
|
|
179
|
+
startedInBrowserEvt.args.data.frames?.find((frame) => !frame.parent));
|
|
180
|
+
if (mainFrame && mainFrame.processId) {
|
|
181
|
+
const threadNameEvt = traceEvents.traceEvents.find(e => e.ph === 'R' &&
|
|
182
|
+
e.cat === 'blink.user_timing' && e.name === 'navigationStart' && e.args.data.isLoadingMainFrame);
|
|
183
|
+
if (threadNameEvt) {
|
|
184
|
+
log.info(`Replace mainFrame process id ${mainFrame.processId} with actual thread process id ${threadNameEvt.pid}`);
|
|
185
|
+
mainFrame.processId = threadNameEvt.pid;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
log.info(`Couldn't replace mainFrame process id ${mainFrame.processId} with actual thread process id`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this._trace = {
|
|
192
|
+
...traceEvents,
|
|
193
|
+
frameId: this._frameId,
|
|
194
|
+
loaderId: this._loaderId,
|
|
195
|
+
pageUrl: this._pageUrl,
|
|
196
|
+
traceStart: this._traceStart,
|
|
197
|
+
traceEnd: Date.now()
|
|
198
|
+
};
|
|
199
|
+
this.emit('tracingComplete', this._trace);
|
|
200
|
+
this.finishTracing();
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
log.error(`Error capturing tracing logs: ${err.stack}`);
|
|
204
|
+
this.emit('tracingError', err);
|
|
205
|
+
return this.finishTracing();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* clear tracing states and emit tracingFinished
|
|
210
|
+
*/
|
|
211
|
+
finishTracing() {
|
|
212
|
+
log.info(`Tracing for ${this._frameId} completed`);
|
|
213
|
+
this._pageLoadDetected = false;
|
|
214
|
+
/**
|
|
215
|
+
* clean up the listeners
|
|
216
|
+
*/
|
|
217
|
+
NETWORK_RECORDER_EVENTS.forEach((method) => this._session.off(method, this._networkListeners[method]));
|
|
218
|
+
delete this._networkStatusMonitor;
|
|
219
|
+
delete this._traceStart;
|
|
220
|
+
delete this._frameId;
|
|
221
|
+
delete this._loaderId;
|
|
222
|
+
delete this._pageUrl;
|
|
223
|
+
this._failingFrameLoadIds = [];
|
|
224
|
+
this._waitConditionPromises = [];
|
|
225
|
+
this.emit('tracingFinished');
|
|
226
|
+
}
|
|
227
|
+
waitForMaxTimeout(maxWaitForLoadedMs = MAX_TRACE_WAIT_TIME) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, maxWaitForLoadedMs)).then(() => async () => {
|
|
229
|
+
log.error('Neither network nor CPU idle time could be detected within timeout, wrapping up tracing');
|
|
230
|
+
return this.completeTracing();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CDPSession } from 'puppeteer-core/lib/esm/puppeteer/api/CDPSession.js';
|
|
2
|
+
import type { Protocol } from 'devtools-protocol';
|
|
3
|
+
interface RequestLog {
|
|
4
|
+
id?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
requests: Request[];
|
|
7
|
+
}
|
|
8
|
+
interface Request {
|
|
9
|
+
id: string;
|
|
10
|
+
url: string;
|
|
11
|
+
method: string;
|
|
12
|
+
loaderId?: string;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
requestHeaders?: Protocol.Network.Headers;
|
|
15
|
+
responseHeaders?: Protocol.Network.Headers;
|
|
16
|
+
timing?: Protocol.Network.ResourceTiming;
|
|
17
|
+
type?: Protocol.Network.ResourceType;
|
|
18
|
+
redirect?: {
|
|
19
|
+
url: string;
|
|
20
|
+
statusCode: number;
|
|
21
|
+
requestHeaders?: Protocol.Network.Headers;
|
|
22
|
+
responseHeaders?: Protocol.Network.Headers;
|
|
23
|
+
timing?: Protocol.Network.ResourceTiming;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface RequestPayload {
|
|
27
|
+
size: number;
|
|
28
|
+
encoded: number;
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
export default class NetworkHandler {
|
|
32
|
+
requestLog: RequestLog;
|
|
33
|
+
requestTypes: {
|
|
34
|
+
[key in Protocol.Network.ResourceType]?: RequestPayload;
|
|
35
|
+
};
|
|
36
|
+
cachedFirstRequest?: Request;
|
|
37
|
+
constructor(session: CDPSession);
|
|
38
|
+
findRequest(params: Protocol.Network.DataReceivedEvent | Protocol.Network.ResponseReceivedEvent): Request | undefined;
|
|
39
|
+
onDataReceived(params: Protocol.Network.DataReceivedEvent): void;
|
|
40
|
+
onNetworkResponseReceived(params: Protocol.Network.ResponseReceivedEvent): void;
|
|
41
|
+
onNetworkRequestWillBeSent(params: Protocol.Network.RequestWillBeSentEvent): number | undefined;
|
|
42
|
+
onPageFrameNavigated(params: Protocol.Page.FrameNavigatedEvent): void;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handler/network.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oDAAoD,CAAA;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAIjD,UAAU,UAAU;IAChB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,EAAE,CAAA;CACtB;AAED,UAAU,OAAO;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAA;IACzC,eAAe,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAA;IAC1C,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAA;IACxC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAA;IACpC,QAAQ,CAAC,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAA;QACzC,eAAe,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAA;QAC1C,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAA;KAC3C,CAAA;CACJ;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,CAAC,OAAO,OAAO,cAAc;IAC/B,UAAU,EAAE,UAAU,CAAmB;IACzC,YAAY,EAAE;SACT,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,cAAc;KAC1D,CAAK;IACN,kBAAkB,CAAC,EAAE,OAAO,CAAA;gBAEf,OAAO,EAAE,UAAU;IAOhC,WAAW,CAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,iBAAiB,GAAG,QAAQ,CAAC,OAAO,CAAC,qBAAqB;IAYhG,cAAc,CAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,iBAAiB;IAkB1D,yBAAyB,CAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,qBAAqB;IAgBzE,0BAA0B,CAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,sBAAsB;IAiE3E,oBAAoB,CAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB;CAwBlE"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { IGNORED_URLS } from '../constants.js';
|
|
2
|
+
export default class NetworkHandler {
|
|
3
|
+
requestLog = { requests: [] };
|
|
4
|
+
requestTypes = {};
|
|
5
|
+
cachedFirstRequest;
|
|
6
|
+
constructor(session) {
|
|
7
|
+
session.on('Network.dataReceived', this.onDataReceived.bind(this));
|
|
8
|
+
session.on('Network.responseReceived', this.onNetworkResponseReceived.bind(this));
|
|
9
|
+
session.on('Network.requestWillBeSent', this.onNetworkRequestWillBeSent.bind(this));
|
|
10
|
+
session.on('Page.frameNavigated', this.onPageFrameNavigated.bind(this));
|
|
11
|
+
}
|
|
12
|
+
findRequest(params) {
|
|
13
|
+
let request = this.requestLog.requests.find((req) => req.id === params.requestId);
|
|
14
|
+
/**
|
|
15
|
+
* If no match is found, check if the corresponding request is the cached first request
|
|
16
|
+
*/
|
|
17
|
+
if (!request && this.cachedFirstRequest && this.cachedFirstRequest.id === params.requestId) {
|
|
18
|
+
request = this.cachedFirstRequest;
|
|
19
|
+
}
|
|
20
|
+
return request;
|
|
21
|
+
}
|
|
22
|
+
onDataReceived(params) {
|
|
23
|
+
const request = this.findRequest(params);
|
|
24
|
+
/**
|
|
25
|
+
* ensure that
|
|
26
|
+
* - a requestWillBeSent event was triggered before
|
|
27
|
+
* - the request type is accurate and known (sometimes this is not the case when `Network.requestWillBeSent` is triggered)
|
|
28
|
+
*/
|
|
29
|
+
if (!request || !request.type || !this.requestTypes[request.type]) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const type = request.type;
|
|
33
|
+
const requestType = this.requestTypes[type] || {};
|
|
34
|
+
requestType.size += params.dataLength;
|
|
35
|
+
requestType.encoded += params.encodedDataLength;
|
|
36
|
+
}
|
|
37
|
+
onNetworkResponseReceived(params) {
|
|
38
|
+
const request = this.findRequest(params);
|
|
39
|
+
/**
|
|
40
|
+
* ensure that a requestWillBeSent event was triggered before
|
|
41
|
+
*/
|
|
42
|
+
if (!request) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
request.statusCode = params.response.status;
|
|
46
|
+
request.requestHeaders = params.response.requestHeaders;
|
|
47
|
+
request.responseHeaders = params.response.headers;
|
|
48
|
+
request.timing = params.response.timing;
|
|
49
|
+
request.type = params.type;
|
|
50
|
+
}
|
|
51
|
+
onNetworkRequestWillBeSent(params) {
|
|
52
|
+
let isFirstRequestOfFrame = false;
|
|
53
|
+
if (
|
|
54
|
+
/**
|
|
55
|
+
* A new page was opened when request type is a document.
|
|
56
|
+
* The first request is sent before the Page.frameNavigated event is triggered,
|
|
57
|
+
* so this request must be cached to be able to add it to the requestLog later.
|
|
58
|
+
*/
|
|
59
|
+
params.type === 'Document' &&
|
|
60
|
+
/**
|
|
61
|
+
* ensure that only page loads triggered by non scripts (devtools only) are considered
|
|
62
|
+
* new page loads
|
|
63
|
+
*/
|
|
64
|
+
params.initiator.type === 'other' &&
|
|
65
|
+
/**
|
|
66
|
+
* ignore pages not initated by the user
|
|
67
|
+
*/
|
|
68
|
+
IGNORED_URLS.filter((url) => params.request.url.startsWith(url)).length === 0) {
|
|
69
|
+
isFirstRequestOfFrame = true;
|
|
70
|
+
/**
|
|
71
|
+
* reset the request type sizes
|
|
72
|
+
*/
|
|
73
|
+
this.requestTypes = {};
|
|
74
|
+
}
|
|
75
|
+
const log = {
|
|
76
|
+
id: params.requestId,
|
|
77
|
+
url: params.request.url,
|
|
78
|
+
method: params.request.method
|
|
79
|
+
};
|
|
80
|
+
if (params.redirectResponse) {
|
|
81
|
+
log.redirect = {
|
|
82
|
+
url: params.redirectResponse.url,
|
|
83
|
+
statusCode: params.redirectResponse.status,
|
|
84
|
+
requestHeaders: params.redirectResponse.requestHeaders,
|
|
85
|
+
responseHeaders: params.redirectResponse.headers,
|
|
86
|
+
timing: params.redirectResponse.timing
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (params.type) {
|
|
90
|
+
const requestType = this.requestTypes[params.type];
|
|
91
|
+
if (!requestType) {
|
|
92
|
+
this.requestTypes[params.type] = {
|
|
93
|
+
size: 0,
|
|
94
|
+
encoded: 0,
|
|
95
|
+
count: 1
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
else if (requestType) {
|
|
99
|
+
requestType.count++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (isFirstRequestOfFrame) {
|
|
103
|
+
log.loaderId = params.loaderId;
|
|
104
|
+
this.cachedFirstRequest = log;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return this.requestLog.requests.push(log);
|
|
108
|
+
}
|
|
109
|
+
onPageFrameNavigated(params) {
|
|
110
|
+
/**
|
|
111
|
+
* Only create a requestLog for pages that don't have a parent frame.
|
|
112
|
+
* I.e. iframes are ignored
|
|
113
|
+
*/
|
|
114
|
+
if (!params.frame.parentId && IGNORED_URLS.filter((url) => params.frame.url.startsWith(url)).length === 0) {
|
|
115
|
+
this.requestLog = {
|
|
116
|
+
id: params.frame.loaderId,
|
|
117
|
+
url: params.frame.url,
|
|
118
|
+
requests: []
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Add the first request that was cached before the actual requestLog could be created
|
|
122
|
+
*/
|
|
123
|
+
if (this.cachedFirstRequest && this.cachedFirstRequest.loaderId === params.frame.loaderId) {
|
|
124
|
+
/**
|
|
125
|
+
* Delete the loaderId of the first request so that all request data has the same structure
|
|
126
|
+
*/
|
|
127
|
+
delete this.cachedFirstRequest.loaderId;
|
|
128
|
+
this.requestLog.requests.push(this.cachedFirstRequest);
|
|
129
|
+
this.cachedFirstRequest = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Capabilities, Services, FunctionProperties, ThenArg } from '@wdio/types';
|
|
2
|
+
import CommandHandler from './commands.js';
|
|
3
|
+
import type Auditor from './auditor.js';
|
|
4
|
+
import type { DevtoolsConfig, EnablePerformanceAuditsOptions, PWAAudits } from './types.js';
|
|
5
|
+
export default class DevToolsService implements Services.ServiceInstance {
|
|
6
|
+
private _options;
|
|
7
|
+
private _command;
|
|
8
|
+
private _browser?;
|
|
9
|
+
constructor(_options: DevtoolsConfig);
|
|
10
|
+
before(caps: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities, specs: string[], browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): Promise<void>;
|
|
11
|
+
onReload(): Promise<void>;
|
|
12
|
+
beforeCommand(commandName: string, params: any[]): Promise<(void | undefined)[]>;
|
|
13
|
+
afterCommand(commandName: string): Promise<(void | undefined)[]>;
|
|
14
|
+
/**
|
|
15
|
+
* set flag to run performance audits for page transitions
|
|
16
|
+
*/
|
|
17
|
+
_enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor }?: EnablePerformanceAuditsOptions): void;
|
|
18
|
+
/**
|
|
19
|
+
* custom command to disable performance audits
|
|
20
|
+
*/
|
|
21
|
+
_disablePerformanceAudits(): void;
|
|
22
|
+
_setThrottlingProfile(networkThrottling?: "online" | "offline" | "GPRS" | "Regular 2G" | "Good 2G" | "Regular 3G" | "Good 3G" | "Regular 4G" | "DSL" | "Wifi", cpuThrottling?: number, cacheEnabled?: boolean): Promise<void>;
|
|
23
|
+
_checkPWA(auditsToBeRun?: PWAAudits[]): Promise<import("./types.js").AuditResult | import("./types.js").AuditResult[]>;
|
|
24
|
+
_setupHandler(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export * from './types.js';
|
|
27
|
+
type CommandHandlerCommands = FunctionProperties<CommandHandler>;
|
|
28
|
+
type AuditorCommands = Omit<FunctionProperties<Auditor>, '_audit' | '_auditPWA' | 'updateCommands'>;
|
|
29
|
+
/**
|
|
30
|
+
* ToDo(Christian): use key remapping with TS 4.1
|
|
31
|
+
* https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types
|
|
32
|
+
*/
|
|
33
|
+
interface BrowserExtension extends CommandHandlerCommands, AuditorCommands {
|
|
34
|
+
}
|
|
35
|
+
export type BrowserExtensionSync = {
|
|
36
|
+
[K in keyof BrowserExtension]: (...args: Parameters<BrowserExtension[K]>) => ThenArg<ReturnType<BrowserExtension[K]>>;
|
|
37
|
+
};
|
|
38
|
+
declare global {
|
|
39
|
+
namespace WebdriverIO {
|
|
40
|
+
interface ServiceOption extends DevtoolsConfig {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
namespace WebdriverIO {
|
|
44
|
+
interface Browser extends BrowserExtension {
|
|
45
|
+
}
|
|
46
|
+
interface MultiRemoteBrowser extends BrowserExtension {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAGtF,OAAO,cAAc,MAAM,eAAe,CAAA;AAC1C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,EAAE,cAAc,EAAE,8BAA8B,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3F,MAAM,CAAC,OAAO,OAAO,eAAgB,YAAW,QAAQ,CAAC,eAAe;IAIvD,OAAO,CAAC,QAAQ;IAH7B,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAC,CAAsD;gBAElD,QAAQ,EAAE,cAAc;IAEvC,MAAM,CACR,IAAI,EAAE,YAAY,CAAC,+BAA+B,GAAG,YAAY,CAAC,gCAAgC,EAClG,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IAM3D,QAAQ;IAQR,aAAa,CAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE;IAIjD,YAAY,CAAE,WAAW,EAAE,MAAM;IAQvC;;OAEG;IACH,wBAAwB,CAAE,EAAE,iBAAiB,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,GAAE,8BAAuD;IAkBjJ;;OAEG;IACH,yBAAyB;IAUnB,qBAAqB,CACvB,iBAAiB,sHAA2C,EAC5D,aAAa,GAAE,MAA6C,EAC5D,YAAY,GAAE,OAA6C;IAWzD,SAAS,CAAE,aAAa,CAAC,EAAE,SAAS,EAAE;IAOtC,aAAa;CAwDtB;AAED,cAAc,YAAY,CAAA;AAE1B,KAAK,sBAAsB,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAA;AAChE,KAAK,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,gBAAgB,CAAC,CAAA;AAEnG;;;GAGG;AACH,UAAU,gBAAiB,SAAQ,sBAAsB,EAAE,eAAe;CAAG;AAE7E,MAAM,MAAM,oBAAoB,GAAG;KAC9B,CAAC,IAAI,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;CACxH,CAAA;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,WAAW,CAAC;QAClB,UAAU,aAAc,SAAQ,cAAc;SAAG;KACpD;IAED,UAAU,WAAW,CAAC;QAClB,UAAU,OAAQ,SAAQ,gBAAgB;SAAI;QAC9C,UAAU,kBAAmB,SAAQ,gBAAgB;SAAI;KAC5D;CACJ"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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();
|
|
20
|
+
}
|
|
21
|
+
async beforeCommand(commandName, params) {
|
|
22
|
+
return Promise.all(this._command.map(async (c) => await c._beforeCmd(commandName, params)));
|
|
23
|
+
}
|
|
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
|
+
}
|
|
48
|
+
}
|
|
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
|
+
}
|
|
61
|
+
}
|
|
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
|
+
}
|
|
71
|
+
}
|
|
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)));
|
|
77
|
+
}
|
|
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);
|
|
120
|
+
}
|
|
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));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export * from './types.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="src/@types/lighthouse.js" />
|
|
2
|
+
import CriConnection from 'lighthouse/lighthouse-core/gather/connections/cri.js';
|
|
3
|
+
/**
|
|
4
|
+
* this class got patched to enable connecting to a remote path like
|
|
5
|
+
* ws://192.168.0.39:4444/session/349a44a32846c2659c703e71403bd472/se/cdp
|
|
6
|
+
* as it requires to attach to a session before.
|
|
7
|
+
*/
|
|
8
|
+
export default class ChromeProtocolPatched extends CriConnection {
|
|
9
|
+
private _sessionId?;
|
|
10
|
+
/**
|
|
11
|
+
* Add constructor for typing safety
|
|
12
|
+
* @param {number=} port Optional port number. Defaults to 9222;
|
|
13
|
+
* @param {string=} hostname Optional hostname. Defaults to localhost.
|
|
14
|
+
* @constructor
|
|
15
|
+
*/
|
|
16
|
+
constructor(port?: string, hostname?: string);
|
|
17
|
+
setSessionId(sessionId: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* force every command to be send with the given session id
|
|
20
|
+
*/
|
|
21
|
+
sendCommand(method: string, sessionId?: string, ...paramArgs: any[]): any;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=cri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cri.d.ts","sourceRoot":"","sources":["../../src/lighthouse/cri.ts"],"names":[],"mappings":";AAAA,OAAO,aAAa,MAAM,sDAAsD,CAAA;AAKhF;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,aAAa;IAC5D,OAAO,CAAC,UAAU,CAAC,CAAQ;IAE3B;;;;;OAKG;gBACS,IAAI,GAAE,MAAqB,EAAE,QAAQ,GAAE,MAAyB;IAI5E,YAAY,CAAE,SAAS,EAAE,MAAM;IAI/B;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,EAAE;CAGtE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import CriConnection from 'lighthouse/lighthouse-core/gather/connections/cri.js';
|
|
2
|
+
const DEFAULT_HOSTNAME = 'localhost';
|
|
3
|
+
const DEFAULT_PORT = '9222';
|
|
4
|
+
/**
|
|
5
|
+
* this class got patched to enable connecting to a remote path like
|
|
6
|
+
* ws://192.168.0.39:4444/session/349a44a32846c2659c703e71403bd472/se/cdp
|
|
7
|
+
* as it requires to attach to a session before.
|
|
8
|
+
*/
|
|
9
|
+
export default class ChromeProtocolPatched extends CriConnection {
|
|
10
|
+
_sessionId;
|
|
11
|
+
/**
|
|
12
|
+
* Add constructor for typing safety
|
|
13
|
+
* @param {number=} port Optional port number. Defaults to 9222;
|
|
14
|
+
* @param {string=} hostname Optional hostname. Defaults to localhost.
|
|
15
|
+
* @constructor
|
|
16
|
+
*/
|
|
17
|
+
constructor(port = DEFAULT_PORT, hostname = DEFAULT_HOSTNAME) {
|
|
18
|
+
super(port, hostname);
|
|
19
|
+
}
|
|
20
|
+
setSessionId(sessionId) {
|
|
21
|
+
this._sessionId = sessionId;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* force every command to be send with the given session id
|
|
25
|
+
*/
|
|
26
|
+
sendCommand(method, sessionId, ...paramArgs) {
|
|
27
|
+
return super.sendCommand(method, sessionId || this._sessionId, ...paramArgs);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collectMetaElements.d.ts","sourceRoot":"","sources":["../../src/scripts/collectMetaElements.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,mBAAmB;;;;;;IAuC1C"}
|