@wdio/lighthouse-service 9.0.0-alpha.369 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,233 +0,0 @@
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
- }
@@ -1,133 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
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
- }
@@ -1,37 +0,0 @@
1
- export default function collectMetaElements() {
2
- const selector = 'head meta';
3
- const realMatchesFn = window.Element.prototype.matches;
4
- const metas = [];
5
- const _findAllElements = (nodes) => {
6
- // eslint-disable-next-line no-cond-assign
7
- for (let i = 0, el; el = nodes[i]; ++i) {
8
- if (!selector || realMatchesFn.call(el, selector)) {
9
- metas.push(el);
10
- }
11
- // If the element has a shadow root, dig deeper.
12
- if (el.shadowRoot) {
13
- _findAllElements(el.shadowRoot.querySelectorAll('*'));
14
- }
15
- }
16
- };
17
- _findAllElements(document.querySelectorAll('*'));
18
- return metas.map(meta => {
19
- const getAttribute = (name) => {
20
- const attr = meta.attributes.getNamedItem(name);
21
- if (!attr) {
22
- return;
23
- }
24
- return attr.value;
25
- };
26
- return {
27
- // @ts-ignore
28
- name: meta.name.toLowerCase(),
29
- // @ts-ignore
30
- content: meta.content,
31
- property: getAttribute('property'),
32
- // @ts-ignore
33
- httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
34
- charset: getAttribute('charset'),
35
- };
36
- });
37
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Used by _waitForCPUIdle and executed in the context of the page, updates the ____lastLongTask
3
- * property on window to the end time of the last long task.
4
- */
5
- export default function registerPerformanceObserverInPage() {
6
- window.____lastLongTask = window.performance.now();
7
- const observer = new window.PerformanceObserver(entryList => {
8
- const entries = entryList.getEntries();
9
- for (const entry of entries) {
10
- if (entry.entryType === 'longtask') {
11
- const taskEnd = entry.startTime + entry.duration;
12
- window.____lastLongTask = Math.max(window.____lastLongTask, taskEnd);
13
- }
14
- }
15
- });
16
- observer.observe({ entryTypes: ['longtask'] });
17
- // HACK: A PerformanceObserver will be GC'd if there are no more references to it, so attach it to
18
- // window to ensure we still receive longtask notifications. See https://crbug.com/742530.
19
- // For an example test of this behavior see https://gist.github.com/patrickhulce/69d8bed1807e762218994b121d06fea6.
20
- // FIXME COMPAT: This hack isn't neccessary as of Chrome 62.0.3176.0
21
- // https://bugs.chromium.org/p/chromium/issues/detail?id=742530#c7
22
- window.____lhPerformanceObserver = observer;
23
- }
package/build/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
package/build/utils.js DELETED
@@ -1,64 +0,0 @@
1
- import Driver from 'lighthouse/lighthouse-core/gather/driver.js';
2
- import ChromeProtocol from './lighthouse/cri.js';
3
- import { IGNORED_URLS, UNSUPPORTED_ERROR_MESSAGE } from './constants.js';
4
- const CUSTOM_COMMANDS = [
5
- 'getMetrics',
6
- 'startTracing',
7
- 'getDiagnostics',
8
- 'getCoverageReport',
9
- 'enablePerformanceAudits',
10
- 'disablePerformanceAudits',
11
- 'getMainThreadWorkBreakdown',
12
- 'checkPWA'
13
- ];
14
- export function setUnsupportedCommand(browser) {
15
- for (const command of CUSTOM_COMMANDS) {
16
- browser.addCommand(command, /* istanbul ignore next */ () => {
17
- throw new Error(UNSUPPORTED_ERROR_MESSAGE);
18
- });
19
- }
20
- }
21
- /**
22
- * Create a sum of a specific key from a list of objects
23
- * @param list list of key/value objects
24
- * @param key key of value to be summed up
25
- */
26
- export function sumByKey(list, key) {
27
- return list.map((data) => data[key]).reduce((acc, val) => acc + val, 0);
28
- }
29
- /**
30
- * check if url is supported for tracing
31
- * @param {string} url to check for
32
- * @return {Boolean} true if url was opened by user
33
- */
34
- export function isSupportedUrl(url) {
35
- return IGNORED_URLS.filter((ignoredUrl) => url.startsWith(ignoredUrl)).length === 0;
36
- }
37
- /**
38
- * Either request the page list directly from the browser or if Selenium
39
- * or Selenoid is used connect to a target manually
40
- */
41
- export async function getLighthouseDriver(session, target) {
42
- const connection = session.connection();
43
- if (!connection) {
44
- throw new Error('Couldn\'t find a CDP connection');
45
- }
46
- const cUrl = new URL(connection.url());
47
- const cdpConnection = new ChromeProtocol(cUrl.port, cUrl.hostname);
48
- /**
49
- * only create a new DevTools session if our WebSocket url doesn't already indicate
50
- * that we are using one
51
- */
52
- if (!cUrl.pathname.startsWith('/devtools/browser')) {
53
- await cdpConnection._connectToSocket({
54
- webSocketDebuggerUrl: connection.url(),
55
- id: (await target.asPage()).mainFrame()._id
56
- });
57
- const { sessionId } = await cdpConnection.sendCommand('Target.attachToTarget', undefined, { targetId: (await target.asPage()).mainFrame()._id, flatten: true });
58
- cdpConnection.setSessionId(sessionId);
59
- return new Driver(cdpConnection);
60
- }
61
- const list = await cdpConnection._runJsonCommand('list');
62
- await cdpConnection._connectToSocket(list[0]);
63
- return new Driver(cdpConnection);
64
- }
File without changes