@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.
Files changed (42) hide show
  1. package/LICENSE-MIT +20 -0
  2. package/README.md +258 -0
  3. package/build/auditor.d.ts +54 -0
  4. package/build/auditor.d.ts.map +1 -0
  5. package/build/auditor.js +147 -0
  6. package/build/commands.d.ts +86 -0
  7. package/build/commands.d.ts.map +1 -0
  8. package/build/commands.js +212 -0
  9. package/build/constants.d.ts +95 -0
  10. package/build/constants.d.ts.map +1 -0
  11. package/build/constants.js +150 -0
  12. package/build/gatherer/devtools.d.ts +19 -0
  13. package/build/gatherer/devtools.d.ts.map +1 -0
  14. package/build/gatherer/devtools.js +12 -0
  15. package/build/gatherer/pwa.d.ts +28 -0
  16. package/build/gatherer/pwa.d.ts.map +1 -0
  17. package/build/gatherer/pwa.js +66 -0
  18. package/build/gatherer/trace.d.ts +61 -0
  19. package/build/gatherer/trace.d.ts.map +1 -0
  20. package/build/gatherer/trace.js +233 -0
  21. package/build/handler/network.d.ts +45 -0
  22. package/build/handler/network.d.ts.map +1 -0
  23. package/build/handler/network.js +133 -0
  24. package/build/index.d.ts +50 -0
  25. package/build/index.d.ts.map +1 -0
  26. package/build/index.js +126 -0
  27. package/build/lighthouse/cri.d.ts +23 -0
  28. package/build/lighthouse/cri.d.ts.map +1 -0
  29. package/build/lighthouse/cri.js +29 -0
  30. package/build/scripts/collectMetaElements.d.ts +8 -0
  31. package/build/scripts/collectMetaElements.d.ts.map +1 -0
  32. package/build/scripts/collectMetaElements.js +37 -0
  33. package/build/scripts/registerPerformanceObserverInPage.d.ts +12 -0
  34. package/build/scripts/registerPerformanceObserverInPage.d.ts.map +1 -0
  35. package/build/scripts/registerPerformanceObserverInPage.js +23 -0
  36. package/build/types.d.ts +168 -0
  37. package/build/types.d.ts.map +1 -0
  38. package/build/types.js +1 -0
  39. package/build/utils.d.ts +23 -0
  40. package/build/utils.d.ts.map +1 -0
  41. package/build/utils.js +64 -0
  42. 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
+ }
@@ -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,8 @@
1
+ export default function collectMetaElements(): {
2
+ name: any;
3
+ content: any;
4
+ property: string | undefined;
5
+ httpEquiv: any;
6
+ charset: string | undefined;
7
+ }[];
8
+ //# sourceMappingURL=collectMetaElements.d.ts.map
@@ -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"}