lighthouse 9.3.1 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/bundle.esm.js +38 -24
- package/dist/report/flow.js +5 -5
- package/dist/report/standalone.js +9 -9
- package/flow-report/src/topbar.tsx +1 -1
- package/flow-report/src/wrappers/report.tsx +2 -1
- package/flow-report/test/topbar-test.tsx +1 -1
- package/lighthouse-cli/bin.js +1 -4
- package/lighthouse-cli/run.js +1 -1
- package/lighthouse-cli/test/smokehouse/core-tests.js +4 -5
- package/lighthouse-cli/test/smokehouse/readme.md +4 -0
- package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
- package/lighthouse-core/audits/autocomplete.js +34 -37
- package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
- package/lighthouse-core/audits/seo/hreflang.js +2 -17
- package/lighthouse-core/config/default-config.js +2 -2
- package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
- package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +50 -31
- package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -5
- package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +4 -5
- package/lighthouse-core/fraggle-rock/user-flow.js +8 -6
- package/lighthouse-core/gather/driver/navigation.js +29 -10
- package/lighthouse-core/gather/driver/network-monitor.js +8 -6
- package/lighthouse-core/gather/driver/prepare.js +13 -7
- package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
- package/lighthouse-core/gather/gather-runner.js +1 -1
- package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
- package/lighthouse-core/gather/gatherers/inputs.js +113 -0
- package/lighthouse-core/index.js +3 -3
- package/lighthouse-core/lib/page-functions.js +11 -1
- package/lighthouse-core/lib/sentry.js +39 -24
- package/lighthouse-core/runner.js +79 -52
- package/lighthouse-core/util-commonjs.js +4 -0
- package/package.json +2 -3
- package/readme.md +1 -1
- package/report/assets/templates.html +2 -5
- package/report/renderer/components.js +3 -3
- package/report/renderer/dom.js +1 -3
- package/report/renderer/report-ui-features.js +25 -17
- package/report/renderer/topbar-features.js +5 -1
- package/report/renderer/util.js +4 -0
- package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
- package/report/test/renderer/report-renderer-axe-test.js +38 -17
- package/report/test-assets/faux-psi.js +2 -1
- package/report/types/report-renderer.d.ts +21 -2
- package/shared/localization/locales/en-US.json +7 -1
- package/shared/localization/locales/en-XL.json +7 -1
- package/third-party/axe/LICENSE +362 -0
- package/third-party/axe/README.md +6 -0
- package/third-party/axe/valid-langs.js +110 -0
- package/third-party/snyk/snapshot.json +1 -0
- package/tsconfig.json +1 -0
- package/types/artifacts.d.ts +14 -14
- package/types/global-lh.d.ts +1 -0
- package/types/parse-cache-control/index.d.ts +1 -0
- package/types/smokehouse.d.ts +3 -0
- package/changelog.md +0 -5803
- package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
- package/lighthouse-core/scripts/package.json +0 -4
|
@@ -54,11 +54,10 @@ class UserFlow {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* @param {string} url
|
|
58
57
|
* @param {StepOptions=} stepOptions
|
|
59
58
|
*/
|
|
60
|
-
_getNextNavigationOptions(
|
|
61
|
-
const options = {
|
|
59
|
+
_getNextNavigationOptions(stepOptions) {
|
|
60
|
+
const options = {...this.options, ...stepOptions};
|
|
62
61
|
const configContext = {...options.configContext};
|
|
63
62
|
const settingsOverrides = {...configContext.settingsOverrides};
|
|
64
63
|
|
|
@@ -81,13 +80,16 @@ class UserFlow {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/**
|
|
84
|
-
* @param {
|
|
83
|
+
* @param {LH.NavigationRequestor} requestor
|
|
85
84
|
* @param {StepOptions=} stepOptions
|
|
86
85
|
*/
|
|
87
|
-
async navigate(
|
|
86
|
+
async navigate(requestor, stepOptions) {
|
|
88
87
|
if (this.currentTimespan) throw Error('Timespan already in progress');
|
|
89
88
|
|
|
90
|
-
const result = await navigation(
|
|
89
|
+
const result = await navigation(
|
|
90
|
+
requestor,
|
|
91
|
+
this._getNextNavigationOptions(stepOptions)
|
|
92
|
+
);
|
|
91
93
|
if (!result) throw Error('Navigation returned undefined');
|
|
92
94
|
|
|
93
95
|
const providedName = stepOptions?.stepName;
|
|
@@ -78,12 +78,14 @@ function resolveWaitForFullyLoadedOptions(options) {
|
|
|
78
78
|
* navigations.
|
|
79
79
|
*
|
|
80
80
|
* @param {LH.Gatherer.FRTransitionalDriver} driver
|
|
81
|
-
* @param {
|
|
81
|
+
* @param {LH.NavigationRequestor} requestor
|
|
82
82
|
* @param {NavigationOptions} options
|
|
83
|
-
* @return {Promise<{finalUrl: string, warnings: Array<LH.IcuMessage>}>}
|
|
83
|
+
* @return {Promise<{requestedUrl: string, finalUrl: string, warnings: Array<LH.IcuMessage>}>}
|
|
84
84
|
*/
|
|
85
|
-
async function gotoURL(driver,
|
|
86
|
-
const status =
|
|
85
|
+
async function gotoURL(driver, requestor, options) {
|
|
86
|
+
const status = typeof requestor === 'string' ?
|
|
87
|
+
{msg: `Navigating to ${requestor}`, id: 'lh:driver:navigate'} :
|
|
88
|
+
{msg: 'Navigating using a user defined function', id: 'lh:driver:navigate'};
|
|
87
89
|
log.time(status);
|
|
88
90
|
|
|
89
91
|
const session = driver.defaultSession;
|
|
@@ -94,9 +96,14 @@ async function gotoURL(driver, url, options) {
|
|
|
94
96
|
await session.sendCommand('Page.enable');
|
|
95
97
|
await session.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
let waitForNavigationTriggered;
|
|
100
|
+
if (typeof requestor === 'string') {
|
|
101
|
+
// No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413
|
|
102
|
+
session.setNextProtocolTimeout(Infinity);
|
|
103
|
+
waitForNavigationTriggered = session.sendCommand('Page.navigate', {url: requestor});
|
|
104
|
+
} else {
|
|
105
|
+
waitForNavigationTriggered = requestor();
|
|
106
|
+
}
|
|
100
107
|
|
|
101
108
|
const waitForNavigated = options.waitUntil.includes('navigated');
|
|
102
109
|
const waitForLoad = options.waitUntil.includes('load');
|
|
@@ -119,10 +126,21 @@ async function gotoURL(driver, url, options) {
|
|
|
119
126
|
|
|
120
127
|
const waitConditions = await Promise.all(waitConditionPromises);
|
|
121
128
|
const timedOut = waitConditions.some(condition => condition.timedOut);
|
|
122
|
-
const
|
|
129
|
+
const navigationUrls = await networkMonitor.getNavigationUrls();
|
|
130
|
+
|
|
131
|
+
let requestedUrl = navigationUrls.requestedUrl;
|
|
132
|
+
if (typeof requestor === 'string') {
|
|
133
|
+
if (requestor !== requestedUrl) {
|
|
134
|
+
log.error('Navigation', 'Provided URL did not match initial navigation URL');
|
|
135
|
+
}
|
|
136
|
+
requestedUrl = requestor;
|
|
137
|
+
}
|
|
138
|
+
if (!requestedUrl) throw Error('No navigations detected when running user defined requestor.');
|
|
139
|
+
|
|
140
|
+
const finalUrl = navigationUrls.finalUrl || requestedUrl;
|
|
123
141
|
|
|
124
142
|
// Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
|
|
125
|
-
await
|
|
143
|
+
await waitForNavigationTriggered;
|
|
126
144
|
await networkMonitor.disable();
|
|
127
145
|
|
|
128
146
|
if (options.debugNavigation) {
|
|
@@ -131,8 +149,9 @@ async function gotoURL(driver, url, options) {
|
|
|
131
149
|
|
|
132
150
|
log.timeEnd(status);
|
|
133
151
|
return {
|
|
152
|
+
requestedUrl,
|
|
134
153
|
finalUrl,
|
|
135
|
-
warnings: getNavigationWarnings({timedOut, finalUrl, requestedUrl
|
|
154
|
+
warnings: getNavigationWarnings({timedOut, finalUrl, requestedUrl}),
|
|
136
155
|
};
|
|
137
156
|
}
|
|
138
157
|
|
|
@@ -133,18 +133,20 @@ class NetworkMonitor {
|
|
|
133
133
|
this._sessions = new Map();
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** @return {Promise<string
|
|
137
|
-
async
|
|
136
|
+
/** @return {Promise<{requestedUrl?: string, finalUrl?: string}>} */
|
|
137
|
+
async getNavigationUrls() {
|
|
138
138
|
const frameNavigations = this._frameNavigations;
|
|
139
|
-
if (!frameNavigations.length) return
|
|
139
|
+
if (!frameNavigations.length) return {};
|
|
140
140
|
|
|
141
141
|
const resourceTreeResponse = await this._session.sendCommand('Page.getResourceTree');
|
|
142
142
|
const mainFrameId = resourceTreeResponse.frameTree.frame.id;
|
|
143
143
|
const mainFrameNavigations = frameNavigations.filter(frame => frame.id === mainFrameId);
|
|
144
|
-
|
|
145
|
-
if (!finalNavigation) log.warn('NetworkMonitor', 'No detected navigations');
|
|
144
|
+
if (!mainFrameNavigations.length) log.warn('NetworkMonitor', 'No detected navigations');
|
|
146
145
|
|
|
147
|
-
return
|
|
146
|
+
return {
|
|
147
|
+
requestedUrl: mainFrameNavigations[0]?.url,
|
|
148
|
+
finalUrl: mainFrameNavigations[mainFrameNavigations.length - 1]?.url,
|
|
149
|
+
};
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
@@ -71,17 +71,17 @@ async function dismissJavaScriptDialogs(session) {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* @param {LH.Gatherer.FRProtocolSession} session
|
|
74
|
-
* @param {
|
|
74
|
+
* @param {string} url
|
|
75
75
|
* @return {Promise<{warnings: Array<LH.IcuMessage>}>}
|
|
76
76
|
*/
|
|
77
|
-
async function
|
|
77
|
+
async function resetStorageForUrl(session, url) {
|
|
78
78
|
/** @type {Array<LH.IcuMessage>} */
|
|
79
79
|
const warnings = [];
|
|
80
80
|
|
|
81
81
|
// Reset the storage and warn if there appears to be other important data.
|
|
82
|
-
const warning = await storage.getImportantStorageWarning(session,
|
|
82
|
+
const warning = await storage.getImportantStorageWarning(session, url);
|
|
83
83
|
if (warning) warnings.push(warning);
|
|
84
|
-
await storage.clearDataForOrigin(session,
|
|
84
|
+
await storage.clearDataForOrigin(session, url);
|
|
85
85
|
await storage.clearBrowserCaches(session);
|
|
86
86
|
|
|
87
87
|
return {warnings};
|
|
@@ -190,7 +190,7 @@ async function prepareTargetForNavigationMode(driver, settings) {
|
|
|
190
190
|
*
|
|
191
191
|
* @param {LH.Gatherer.FRProtocolSession} session
|
|
192
192
|
* @param {LH.Config.Settings} settings
|
|
193
|
-
* @param {Pick<LH.Config.NavigationDefn, 'disableThrottling'|'disableStorageReset'|'blockedUrlPatterns'> & {
|
|
193
|
+
* @param {Pick<LH.Config.NavigationDefn, 'disableThrottling'|'disableStorageReset'|'blockedUrlPatterns'> & {requestor: LH.NavigationRequestor}} navigation
|
|
194
194
|
* @return {Promise<{warnings: Array<LH.IcuMessage>}>}
|
|
195
195
|
*/
|
|
196
196
|
async function prepareTargetForIndividualNavigation(session, settings, navigation) {
|
|
@@ -200,9 +200,15 @@ async function prepareTargetForIndividualNavigation(session, settings, navigatio
|
|
|
200
200
|
/** @type {Array<LH.IcuMessage>} */
|
|
201
201
|
const warnings = [];
|
|
202
202
|
|
|
203
|
-
const
|
|
203
|
+
const {requestor} = navigation;
|
|
204
|
+
const shouldResetStorage =
|
|
205
|
+
!settings.disableStorageReset &&
|
|
206
|
+
!navigation.disableStorageReset &&
|
|
207
|
+
// Without prior knowledge of the destination, we cannot know which URL to clear storage for.
|
|
208
|
+
typeof requestor === 'string';
|
|
204
209
|
if (shouldResetStorage) {
|
|
205
|
-
const
|
|
210
|
+
const requestedUrl = requestor;
|
|
211
|
+
const {warnings: storageWarnings} = await resetStorageForUrl(session, requestedUrl);
|
|
206
212
|
warnings.push(...storageWarnings);
|
|
207
213
|
}
|
|
208
214
|
|
|
@@ -114,7 +114,7 @@ function waitForFcp(session, pauseAfterFcpMs, maxWaitForFcpMs) {
|
|
|
114
114
|
* `networkQuietThresholdMs` ms and a method to cancel internal network listeners/timeout.
|
|
115
115
|
* @param {LH.Gatherer.FRProtocolSession} session
|
|
116
116
|
* @param {NetworkMonitor} networkMonitor
|
|
117
|
-
* @param {{networkQuietThresholdMs: number, busyEvent: NetworkMonitorEvent, idleEvent: NetworkMonitorEvent, isIdle(recorder: NetworkMonitor): boolean}} networkQuietOptions
|
|
117
|
+
* @param {{networkQuietThresholdMs: number, busyEvent: NetworkMonitorEvent, idleEvent: NetworkMonitorEvent, isIdle(recorder: NetworkMonitor): boolean, pretendDCLAlreadyFired?: boolean}} networkQuietOptions
|
|
118
118
|
* @return {CancellableWait}
|
|
119
119
|
*/
|
|
120
120
|
function waitForNetworkIdle(session, networkMonitor, networkQuietOptions) {
|
|
@@ -175,13 +175,20 @@ function waitForNetworkIdle(session, networkMonitor, networkQuietOptions) {
|
|
|
175
175
|
networkMonitor.on('requestloaded', logStatus);
|
|
176
176
|
networkMonitor.on(busyEvent, logStatus);
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
if (!networkQuietOptions.pretendDCLAlreadyFired) {
|
|
179
|
+
session.once('Page.domContentEventFired', domContentLoadedListener);
|
|
180
|
+
} else {
|
|
181
|
+
domContentLoadedListener();
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
let canceled = false;
|
|
180
185
|
cancel = () => {
|
|
181
186
|
if (canceled) return;
|
|
182
187
|
canceled = true;
|
|
183
|
-
idleTimeout
|
|
184
|
-
|
|
188
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
189
|
+
if (!networkQuietOptions.pretendDCLAlreadyFired) {
|
|
190
|
+
session.off('Page.domContentEventFired', domContentLoadedListener);
|
|
191
|
+
}
|
|
185
192
|
networkMonitor.removeListener(busyEvent, onBusy);
|
|
186
193
|
networkMonitor.removeListener(idleEvent, onIdle);
|
|
187
194
|
networkMonitor.removeListener('requeststarted', logStatus);
|
|
@@ -572,7 +572,7 @@ class GatherRunner {
|
|
|
572
572
|
driver.defaultSession,
|
|
573
573
|
passContext.settings,
|
|
574
574
|
{
|
|
575
|
-
|
|
575
|
+
requestor: passContext.url,
|
|
576
576
|
disableStorageReset: !passConfig.useThrottling,
|
|
577
577
|
disableThrottling: !passConfig.useThrottling,
|
|
578
578
|
blockedUrlPatterns: passConfig.blockedUrlPatterns,
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
/* globals window document getBoundingClientRect */
|
|
8
|
+
/* globals window document getBoundingClientRect requestAnimationFrame */
|
|
9
9
|
|
|
10
10
|
const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
|
|
11
11
|
const emulation = require('../../lib/emulation.js');
|
|
12
12
|
const pageFunctions = require('../../lib/page-functions.js');
|
|
13
|
+
const NetworkMonitor = require('../driver/network-monitor.js');
|
|
14
|
+
const {waitForNetworkIdle} = require('../driver/wait-for-condition.js');
|
|
13
15
|
|
|
14
16
|
// JPEG quality setting
|
|
15
17
|
// Exploration and examples of reports using different quality settings: https://docs.google.com/document/d/1ZSffucIca9XDW2eEwfoevrk-OTl7WQFeMf0CgeJAA8M/edit#
|
|
@@ -25,7 +27,6 @@ function kebabCaseToCamelCase(str) {
|
|
|
25
27
|
|
|
26
28
|
/* c8 ignore start */
|
|
27
29
|
|
|
28
|
-
// eslint-disable-next-line no-inner-declarations
|
|
29
30
|
function getObservedDeviceMetrics() {
|
|
30
31
|
// Convert the Web API's kebab case (landscape-primary) to camel case (landscapePrimary).
|
|
31
32
|
const screenOrientationType = kebabCaseToCamelCase(window.screen.orientation.type);
|
|
@@ -40,6 +41,12 @@ function getObservedDeviceMetrics() {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function waitForDoubleRaf() {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
/* c8 ignore stop */
|
|
44
51
|
|
|
45
52
|
class FullPageScreenshot extends FRGatherer {
|
|
@@ -53,7 +60,7 @@ class FullPageScreenshot extends FRGatherer {
|
|
|
53
60
|
* @return {Promise<number>}
|
|
54
61
|
* @see https://bugs.chromium.org/p/chromium/issues/detail?id=770769
|
|
55
62
|
*/
|
|
56
|
-
async
|
|
63
|
+
async getMaxTextureSize(context) {
|
|
57
64
|
return await context.driver.executionContext.evaluate(pageFunctions.getMaxTextureSize, {
|
|
58
65
|
args: [],
|
|
59
66
|
useIsolation: true,
|
|
@@ -67,7 +74,7 @@ class FullPageScreenshot extends FRGatherer {
|
|
|
67
74
|
*/
|
|
68
75
|
async _takeScreenshot(context) {
|
|
69
76
|
const session = context.driver.defaultSession;
|
|
70
|
-
const
|
|
77
|
+
const maxTextureSize = await this.getMaxTextureSize(context);
|
|
71
78
|
const metrics = await session.sendCommand('Page.getLayoutMetrics');
|
|
72
79
|
|
|
73
80
|
// Width should match emulated width, without considering content overhang.
|
|
@@ -76,8 +83,19 @@ class FullPageScreenshot extends FRGatherer {
|
|
|
76
83
|
// Note: If the page is zoomed, many assumptions fail.
|
|
77
84
|
//
|
|
78
85
|
// Height should be as tall as the content. So we use contentSize.height
|
|
79
|
-
const width = Math.min(metrics.layoutViewport.clientWidth,
|
|
80
|
-
const height = Math.min(metrics.contentSize.height,
|
|
86
|
+
const width = Math.min(metrics.layoutViewport.clientWidth, maxTextureSize);
|
|
87
|
+
const height = Math.min(metrics.contentSize.height, maxTextureSize);
|
|
88
|
+
|
|
89
|
+
// Setup network monitor before we change the viewport.
|
|
90
|
+
const networkMonitor = new NetworkMonitor(session);
|
|
91
|
+
const waitForNetworkIdleResult = waitForNetworkIdle(session, networkMonitor, {
|
|
92
|
+
pretendDCLAlreadyFired: true,
|
|
93
|
+
networkQuietThresholdMs: 1000,
|
|
94
|
+
busyEvent: 'network-critical-busy',
|
|
95
|
+
idleEvent: 'network-critical-idle',
|
|
96
|
+
isIdle: recorder => recorder.isCriticalIdle(),
|
|
97
|
+
});
|
|
98
|
+
await networkMonitor.enable();
|
|
81
99
|
|
|
82
100
|
await session.sendCommand('Emulation.setDeviceMetricsOverride', {
|
|
83
101
|
// If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
|
|
@@ -89,9 +107,17 @@ class FullPageScreenshot extends FRGatherer {
|
|
|
89
107
|
screenOrientation: {angle: 0, type: 'portraitPrimary'},
|
|
90
108
|
});
|
|
91
109
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
110
|
+
// Now that the viewport is taller, give the page some time to fetch new resources that
|
|
111
|
+
// are now in view.
|
|
112
|
+
await Promise.race([
|
|
113
|
+
new Promise(resolve => setTimeout(resolve, 1000 * 5)),
|
|
114
|
+
waitForNetworkIdleResult.promise,
|
|
115
|
+
]);
|
|
116
|
+
waitForNetworkIdleResult.cancel();
|
|
117
|
+
await networkMonitor.disable();
|
|
118
|
+
|
|
119
|
+
// Now that new resources are (probably) fetched, wait long enough for a layout.
|
|
120
|
+
await context.driver.executionContext.evaluate(waitForDoubleRaf, {args: []});
|
|
95
121
|
|
|
96
122
|
const result = await session.sendCommand('Page.captureScreenshot', {
|
|
97
123
|
format: 'jpeg',
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/* global getNodeDetails */
|
|
9
|
+
|
|
10
|
+
const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
|
|
11
|
+
const pageFunctions = require('../../lib/page-functions.js');
|
|
12
|
+
|
|
13
|
+
/* eslint-env browser, node */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @return {LH.Artifacts['Inputs']}
|
|
17
|
+
*/
|
|
18
|
+
/* c8 ignore start */
|
|
19
|
+
function collectElements() {
|
|
20
|
+
/** @type {LH.Artifacts.InputElement[]} */
|
|
21
|
+
const inputArtifacts = [];
|
|
22
|
+
/** @type {Map<HTMLFormElement, LH.Artifacts.FormElement>} */
|
|
23
|
+
const formElToArtifact = new Map();
|
|
24
|
+
/** @type {Map<HTMLLabelElement, LH.Artifacts.LabelElement>} */
|
|
25
|
+
const labelElToArtifact = new Map();
|
|
26
|
+
|
|
27
|
+
/** @type {HTMLFormElement[]} */
|
|
28
|
+
// @ts-expect-error - put into scope via stringification
|
|
29
|
+
const formEls = getElementsInDocument('form'); // eslint-disable-line no-undef
|
|
30
|
+
for (const formEl of formEls) {
|
|
31
|
+
formElToArtifact.set(formEl, {
|
|
32
|
+
id: formEl.id,
|
|
33
|
+
name: formEl.name,
|
|
34
|
+
autocomplete: formEl.autocomplete,
|
|
35
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
36
|
+
node: getNodeDetails(formEl),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {HTMLLabelElement[]} */
|
|
41
|
+
// @ts-expect-error - put into scope via stringification
|
|
42
|
+
const labelEls = getElementsInDocument('label'); // eslint-disable-line no-undef
|
|
43
|
+
for (const labelEl of labelEls) {
|
|
44
|
+
labelElToArtifact.set(labelEl, {
|
|
45
|
+
for: labelEl.htmlFor,
|
|
46
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
47
|
+
node: getNodeDetails(labelEl),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {HTMLInputElement[]} */
|
|
52
|
+
// @ts-expect-error - put into scope via stringification
|
|
53
|
+
const inputEls = getElementsInDocument('textarea, input, select'); // eslint-disable-line no-undef
|
|
54
|
+
for (const inputEl of inputEls) {
|
|
55
|
+
// If the input element is in a form (either because an ancestor element is <form> or the
|
|
56
|
+
// form= attribute is associated with a <form> element's id), this will be set.
|
|
57
|
+
const parentFormEl = inputEl.form;
|
|
58
|
+
const parentFormIndex = parentFormEl ?
|
|
59
|
+
[...formElToArtifact.keys()].indexOf(parentFormEl) :
|
|
60
|
+
undefined;
|
|
61
|
+
const labelIndices = [...inputEl.labels || []].map((labelEl) => {
|
|
62
|
+
return [...labelElToArtifact.keys()].indexOf(labelEl);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
inputArtifacts.push({
|
|
66
|
+
parentFormIndex,
|
|
67
|
+
labelIndices,
|
|
68
|
+
id: inputEl.id,
|
|
69
|
+
name: inputEl.name,
|
|
70
|
+
type: inputEl.type,
|
|
71
|
+
placeholder: inputEl instanceof HTMLSelectElement ? undefined : inputEl.placeholder,
|
|
72
|
+
autocomplete: {
|
|
73
|
+
property: inputEl.autocomplete,
|
|
74
|
+
attribute: inputEl.getAttribute('autocomplete'),
|
|
75
|
+
// Requires `--enable-features=AutofillShowTypePredictions`.
|
|
76
|
+
prediction: inputEl.getAttribute('autofill-prediction'),
|
|
77
|
+
},
|
|
78
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
79
|
+
node: getNodeDetails(inputEl),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
inputs: inputArtifacts,
|
|
85
|
+
forms: [...formElToArtifact.values()],
|
|
86
|
+
labels: [...labelElToArtifact.values()],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/* c8 ignore stop */
|
|
90
|
+
|
|
91
|
+
class Inputs extends FRGatherer {
|
|
92
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
93
|
+
meta = {
|
|
94
|
+
supportedModes: ['snapshot', 'navigation'],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {LH.Gatherer.FRTransitionalContext} passContext
|
|
99
|
+
* @return {Promise<LH.Artifacts['Inputs']>}
|
|
100
|
+
*/
|
|
101
|
+
async getArtifact(passContext) {
|
|
102
|
+
return passContext.driver.executionContext.evaluate(collectElements, {
|
|
103
|
+
args: [],
|
|
104
|
+
useIsolation: true,
|
|
105
|
+
deps: [
|
|
106
|
+
pageFunctions.getElementsInDocumentString,
|
|
107
|
+
pageFunctions.getNodeDetailsString,
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = Inputs;
|
package/lighthouse-core/index.js
CHANGED
|
@@ -47,11 +47,11 @@ async function lighthouse(url, flags = {}, configJSON, userConnection) {
|
|
|
47
47
|
const connection = userConnection || new ChromeProtocol(flags.port, flags.hostname);
|
|
48
48
|
|
|
49
49
|
// kick off a lighthouse run
|
|
50
|
-
const
|
|
50
|
+
const artifacts = await Runner.gather(() => {
|
|
51
51
|
const requestedUrl = URL.normalizeUrl(url);
|
|
52
52
|
return Runner._gatherArtifactsFromBrowser(requestedUrl, options, connection);
|
|
53
|
-
};
|
|
54
|
-
return Runner.
|
|
53
|
+
}, options);
|
|
54
|
+
return Runner.audit(artifacts, options);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -138,7 +138,17 @@ function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit =
|
|
|
138
138
|
dirty = true;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if (dirty)
|
|
141
|
+
if (dirty) {
|
|
142
|
+
// Style attributes can be blocked by the CSP if they are set via `setAttribute`.
|
|
143
|
+
// If we are trying to set the style attribute, use `el.style.cssText` instead.
|
|
144
|
+
// https://github.com/GoogleChrome/lighthouse/issues/13630
|
|
145
|
+
if (attributeName === 'style') {
|
|
146
|
+
const elementWithStyle = /** @type {HTMLElement} */ (clone);
|
|
147
|
+
elementWithStyle.style.cssText = attributeValue;
|
|
148
|
+
} else {
|
|
149
|
+
clone.setAttribute(attributeName, attributeValue);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
142
152
|
charCount += attributeName.length + attributeValue.length;
|
|
143
153
|
}
|
|
144
154
|
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
const log = require('lighthouse-logger');
|
|
9
9
|
|
|
10
|
-
/** @typedef {import('
|
|
11
|
-
/** @typedef {import('
|
|
10
|
+
/** @typedef {import('@sentry/node').Breadcrumb} Breadcrumb */
|
|
11
|
+
/** @typedef {import('@sentry/node').NodeClient} NodeClient */
|
|
12
|
+
/** @typedef {import('@sentry/node').NodeOptions} NodeOptions */
|
|
13
|
+
/** @typedef {import('@sentry/node').Severity} Severity */
|
|
12
14
|
|
|
13
15
|
const SENTRY_URL = 'https://a6bb0da87ee048cc9ae2a345fc09ab2e:63a7029f46f74265981b7e005e0f69f8@sentry.io/174697';
|
|
14
16
|
|
|
@@ -21,7 +23,7 @@ const SAMPLED_ERRORS = [
|
|
|
21
23
|
// e.g.: {pattern: /No.*node with given id/, rate: 0.01},
|
|
22
24
|
];
|
|
23
25
|
|
|
24
|
-
const noop = () => {};
|
|
26
|
+
const noop = () => { };
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* A delegate for sentry so that environments without error reporting enabled will use
|
|
@@ -29,14 +31,14 @@ const noop = () => {};
|
|
|
29
31
|
*/
|
|
30
32
|
const sentryDelegate = {
|
|
31
33
|
init,
|
|
32
|
-
/** @type {(message: string,
|
|
34
|
+
/** @type {(message: string, level?: Severity) => void} */
|
|
33
35
|
captureMessage: noop,
|
|
34
|
-
/** @type {(breadcrumb:
|
|
36
|
+
/** @type {(breadcrumb: Breadcrumb) => void} */
|
|
35
37
|
captureBreadcrumb: noop,
|
|
36
38
|
/** @type {() => any} */
|
|
37
39
|
getContext: noop,
|
|
38
|
-
/** @type {(error: Error, options?:
|
|
39
|
-
captureException: async () => {},
|
|
40
|
+
/** @type {(error: Error, options: {level?: string, tags?: {[key: string]: any}, extra?: {[key: string]: any}}) => Promise<void>} */
|
|
41
|
+
captureException: async () => { },
|
|
40
42
|
_shouldSample() {
|
|
41
43
|
return SAMPLE_RATE >= Math.random();
|
|
42
44
|
},
|
|
@@ -44,7 +46,7 @@ const sentryDelegate = {
|
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* When called, replaces noops with actual Sentry implementation.
|
|
47
|
-
* @param {{url: string, flags: LH.CliFlags, environmentData:
|
|
49
|
+
* @param {{url: string, flags: LH.CliFlags, environmentData: NodeOptions}} opts
|
|
48
50
|
*/
|
|
49
51
|
function init(opts) {
|
|
50
52
|
// If error reporting is disabled, leave the functions as a noop
|
|
@@ -58,15 +60,25 @@ function init(opts) {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
try {
|
|
61
|
-
const Sentry = require('
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const Sentry = require('@sentry/node');
|
|
64
|
+
Sentry.init({
|
|
65
|
+
...opts.environmentData,
|
|
66
|
+
dsn: SENTRY_URL,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const extras = {
|
|
70
|
+
...opts.flags.throttling,
|
|
71
|
+
channel: opts.flags.channel || 'cli',
|
|
72
|
+
url: opts.url,
|
|
73
|
+
formFactor: opts.flags.formFactor,
|
|
74
|
+
throttlingMethod: opts.flags.throttlingMethod,
|
|
75
|
+
};
|
|
76
|
+
Sentry.setExtras(extras);
|
|
65
77
|
|
|
66
78
|
// Have each delegate function call the corresponding sentry function by default
|
|
67
79
|
sentryDelegate.captureMessage = (...args) => Sentry.captureMessage(...args);
|
|
68
|
-
sentryDelegate.captureBreadcrumb = (...args) => Sentry.
|
|
69
|
-
sentryDelegate.getContext = () =>
|
|
80
|
+
sentryDelegate.captureBreadcrumb = (...args) => Sentry.addBreadcrumb(...args);
|
|
81
|
+
sentryDelegate.getContext = () => extras;
|
|
70
82
|
|
|
71
83
|
// Keep a record of exceptions per audit/gatherer so we can just report once
|
|
72
84
|
const sentryExceptionCache = new Map();
|
|
@@ -107,21 +119,24 @@ function init(opts) {
|
|
|
107
119
|
opts.tags.protocolMethod = err.protocolMethod;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
Sentry.withScope(scope => {
|
|
123
|
+
if (opts.level) {
|
|
124
|
+
// @ts-expect-error - allow any string.
|
|
125
|
+
scope.setLevel(opts.level);
|
|
126
|
+
}
|
|
127
|
+
if (opts.tags) {
|
|
128
|
+
scope.setTags(opts.tags);
|
|
129
|
+
}
|
|
130
|
+
if (opts.extra) {
|
|
131
|
+
scope.setExtras(opts.extra);
|
|
132
|
+
}
|
|
133
|
+
Sentry.captureException(err);
|
|
112
134
|
});
|
|
113
135
|
};
|
|
114
|
-
|
|
115
|
-
const context = Object.assign({
|
|
116
|
-
url: opts.url,
|
|
117
|
-
formFactor: opts.flags.formFactor,
|
|
118
|
-
throttlingMethod: opts.flags.throttlingMethod,
|
|
119
|
-
}, opts.flags.throttling);
|
|
120
|
-
Sentry.mergeContext({extra: Object.assign({}, opts.environmentData.extra, context)});
|
|
121
136
|
} catch (e) {
|
|
122
137
|
log.warn(
|
|
123
138
|
'sentry',
|
|
124
|
-
'Could not load
|
|
139
|
+
'Could not load Sentry, errors will not be reported.'
|
|
125
140
|
);
|
|
126
141
|
}
|
|
127
142
|
}
|