lighthouse 9.3.1-dev.20220213 → 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 +22 -18
- package/dist/report/flow.js +4 -4
- package/dist/report/standalone.js +5 -5
- 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/test/smokehouse/core-tests.js +2 -3
- package/lighthouse-core/audits/autocomplete.js +34 -37
- package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
- package/lighthouse-core/config/default-config.js +2 -2
- package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
- package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
- package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
- package/lighthouse-core/gather/gatherers/inputs.js +113 -0
- package/lighthouse-core/lib/sentry.js +39 -24
- package/lighthouse-core/runner.js +1 -1
- package/package.json +2 -3
- package/report/renderer/dom.js +1 -3
- package/report/renderer/report-ui-features.js +16 -14
- package/report/renderer/topbar-features.js +5 -1
- package/report/test-assets/faux-psi.js +2 -1
- package/report/types/report-renderer.d.ts +14 -2
- package/shared/localization/locales/en-US.json +1 -1
- package/shared/localization/locales/en-XL.json +1 -1
- package/types/artifacts.d.ts +14 -14
- package/types/parse-cache-control/index.d.ts +1 -0
- package/changelog.md +0 -5803
- package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
- package/lighthouse-core/scripts/package.json +0 -4
|
@@ -16,7 +16,7 @@ import {saveFile} from '../../report/renderer/api';
|
|
|
16
16
|
|
|
17
17
|
function saveHtml(flowResult: LH.FlowResult, htmlStr: string) {
|
|
18
18
|
const blob = new Blob([htmlStr], {type: 'text/html'});
|
|
19
|
-
const filename = getFlowResultFilenamePrefix(flowResult);
|
|
19
|
+
const filename = getFlowResultFilenamePrefix(flowResult) + '.html';
|
|
20
20
|
saveFile(blob, filename);
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -33,7 +33,8 @@ export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
|
|
|
33
33
|
({hashState}) => {
|
|
34
34
|
const ref = useExternalRenderer<HTMLDivElement>(() => {
|
|
35
35
|
return renderReport(hashState.currentLhr, {
|
|
36
|
-
|
|
36
|
+
disableFireworks: true,
|
|
37
|
+
disableDarkMode: true,
|
|
37
38
|
omitTopbar: true,
|
|
38
39
|
omitGlobalStyles: true,
|
|
39
40
|
onPageAnchorRendered: link => convertAnchor(link, hashState.index),
|
package/lighthouse-cli/bin.js
CHANGED
|
@@ -137,12 +137,9 @@ async function begin() {
|
|
|
137
137
|
url: urlUnderTest,
|
|
138
138
|
flags: cliFlags,
|
|
139
139
|
environmentData: {
|
|
140
|
-
|
|
140
|
+
serverName: 'redacted', // prevent sentry from using hostname
|
|
141
141
|
environment: isDev() ? 'development' : 'production',
|
|
142
142
|
release: pkg.version,
|
|
143
|
-
tags: {
|
|
144
|
-
channel: 'cli',
|
|
145
|
-
},
|
|
146
143
|
},
|
|
147
144
|
});
|
|
148
145
|
}
|
|
@@ -14,7 +14,7 @@ import dbw from './test-definitions/dobetterweb.js';
|
|
|
14
14
|
import errorsExpiredSsl from './test-definitions/errors-expired-ssl.js';
|
|
15
15
|
import errorsIframeExpiredSsl from './test-definitions/errors-iframe-expired-ssl.js';
|
|
16
16
|
import errorsInfiniteLoop from './test-definitions/errors-infinite-loop.js';
|
|
17
|
-
|
|
17
|
+
import formsAutoComplete from './test-definitions/forms-autocomplete.js';
|
|
18
18
|
import issuesMixedContent from './test-definitions/issues-mixed-content.js';
|
|
19
19
|
import lanternFetch from './test-definitions/lantern-fetch.js';
|
|
20
20
|
import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js';
|
|
@@ -72,8 +72,7 @@ const smokeTests = [
|
|
|
72
72
|
errorsExpiredSsl,
|
|
73
73
|
errorsIframeExpiredSsl,
|
|
74
74
|
errorsInfiniteLoop,
|
|
75
|
-
|
|
76
|
-
// formsAutoComplete,
|
|
75
|
+
formsAutoComplete,
|
|
77
76
|
issuesMixedContent,
|
|
78
77
|
lanternOnline,
|
|
79
78
|
lanternSetTimeout,
|
|
@@ -195,12 +195,12 @@ class AutocompleteAudit extends Audit {
|
|
|
195
195
|
title: str_(UIStrings.title),
|
|
196
196
|
failureTitle: str_(UIStrings.failureTitle),
|
|
197
197
|
description: str_(UIStrings.description),
|
|
198
|
-
requiredArtifacts: ['
|
|
198
|
+
requiredArtifacts: ['Inputs'],
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* @param {LH.Artifacts.
|
|
203
|
+
* @param {LH.Artifacts.InputElement} input
|
|
204
204
|
* @return {{hasValidTokens: boolean, isValidOrder?: boolean}}
|
|
205
205
|
*/
|
|
206
206
|
static checkAttributeValidity(input) {
|
|
@@ -224,47 +224,44 @@ class AutocompleteAudit extends Audit {
|
|
|
224
224
|
* @return {LH.Audit.Product}
|
|
225
225
|
*/
|
|
226
226
|
static audit(artifacts) {
|
|
227
|
-
const forms = artifacts.FormElements;
|
|
228
227
|
const failingFormsData = [];
|
|
229
228
|
const warnings = [];
|
|
230
229
|
let foundPrediction = false;
|
|
231
|
-
for (const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
!input.autocomplete.attribute) continue;
|
|
230
|
+
for (const input of artifacts.Inputs.inputs) {
|
|
231
|
+
const validity = this.checkAttributeValidity(input);
|
|
232
|
+
if (validity.hasValidTokens && validity.isValidOrder) continue;
|
|
233
|
+
if (!input.autocomplete.prediction) continue;
|
|
234
|
+
if (noPrediction.includes(input.autocomplete.prediction) &&
|
|
235
|
+
!input.autocomplete.attribute) continue;
|
|
238
236
|
|
|
239
|
-
|
|
237
|
+
foundPrediction = true;
|
|
240
238
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (validity.isValidOrder === false) {
|
|
251
|
-
warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
|
|
252
|
-
snippet: input.node.snippet}));
|
|
253
|
-
suggestion = UIStrings.reviewOrder;
|
|
254
|
-
}
|
|
255
|
-
// If the autofill prediction is not in our autofill suggestion mapping, then we warn
|
|
256
|
-
if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
|
|
257
|
-
validity.isValidOrder) {
|
|
258
|
-
log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
|
|
259
|
-
not found in our mapping`);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
failingFormsData.push({
|
|
263
|
-
node: Audit.makeNodeItem(input.node),
|
|
264
|
-
suggestion: suggestion,
|
|
265
|
-
current: input.autocomplete.attribute,
|
|
266
|
-
});
|
|
239
|
+
// @ts-ignore
|
|
240
|
+
let suggestion = predictionTypesToTokens[input.autocomplete.prediction];
|
|
241
|
+
// This is here to satisfy typescript because the possible null value of autocomplete.attribute is not compatible with Audit details.
|
|
242
|
+
if (!input.autocomplete.attribute) input.autocomplete.attribute = '';
|
|
243
|
+
// Warning is created because while there is an autocomplete attribute, the autocomplete property does not exsist, thus the attribute's value is invalid.
|
|
244
|
+
if (input.autocomplete.attribute) {
|
|
245
|
+
warnings.push(str_(UIStrings.warningInvalid, {token: input.autocomplete.attribute,
|
|
246
|
+
snippet: input.node.snippet}));
|
|
267
247
|
}
|
|
248
|
+
if (validity.isValidOrder === false) {
|
|
249
|
+
warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
|
|
250
|
+
snippet: input.node.snippet}));
|
|
251
|
+
suggestion = UIStrings.reviewOrder;
|
|
252
|
+
}
|
|
253
|
+
// If the autofill prediction is not in our autofill suggestion mapping, then we warn
|
|
254
|
+
if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
|
|
255
|
+
validity.isValidOrder) {
|
|
256
|
+
log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
|
|
257
|
+
not found in our mapping`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
failingFormsData.push({
|
|
261
|
+
node: Audit.makeNodeItem(input.node),
|
|
262
|
+
suggestion: suggestion,
|
|
263
|
+
current: input.autocomplete.attribute,
|
|
264
|
+
});
|
|
268
265
|
}
|
|
269
266
|
|
|
270
267
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
@@ -39,7 +39,7 @@ const UIStrings = {
|
|
|
39
39
|
/** Title of the Accessibility category of audits. This section contains audits focused on making web content accessible to all users. Also used as a label of a score gauge; try to limit to 20 characters. */
|
|
40
40
|
a11yCategoryTitle: 'Accessibility',
|
|
41
41
|
/** Description of the Accessibility category. This is displayed at the top of a list of audits focused on making web content accessible to all users. No character length limits. 'improve the accessibility of your web app' becomes link text to additional documentation. */
|
|
42
|
-
a11yCategoryDescription: 'These checks highlight opportunities to [improve the accessibility of your web app](https://
|
|
42
|
+
a11yCategoryDescription: 'These checks highlight opportunities to [improve the accessibility of your web app](https://web.dev/lighthouse-accessibility/). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.',
|
|
43
43
|
/** Description of the Accessibility manual checks category. This description is displayed above a list of accessibility audits that currently have no automated test and so must be verified manually by the user. No character length limits. 'conducting an accessibility review' becomes link text to additional documentation. */
|
|
44
44
|
a11yCategoryManualDescription: 'These items address areas which an automated testing tool cannot cover. Learn more in our guide on [conducting an accessibility review](https://developers.google.com/web/fundamentals/accessibility/how-to-review).',
|
|
45
45
|
/** Title of the best practices section of the Accessibility category. Within this section are audits with descriptive titles that highlight common accessibility best practices. */
|
|
@@ -144,7 +144,7 @@ const defaultConfig = {
|
|
|
144
144
|
'meta-elements',
|
|
145
145
|
'script-elements',
|
|
146
146
|
'iframe-elements',
|
|
147
|
-
'
|
|
147
|
+
'inputs',
|
|
148
148
|
'main-document-content',
|
|
149
149
|
'global-listeners',
|
|
150
150
|
'dobetterweb/doctype',
|
|
@@ -46,7 +46,7 @@ const artifacts = {
|
|
|
46
46
|
DOMStats: '',
|
|
47
47
|
EmbeddedContent: '',
|
|
48
48
|
FontSize: '',
|
|
49
|
-
|
|
49
|
+
Inputs: '',
|
|
50
50
|
FullPageScreenshot: '',
|
|
51
51
|
GlobalListeners: '',
|
|
52
52
|
IFrameElements: '',
|
|
@@ -96,7 +96,7 @@ const defaultConfig = {
|
|
|
96
96
|
{id: artifacts.DOMStats, gatherer: 'dobetterweb/domstats'},
|
|
97
97
|
{id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
|
|
98
98
|
{id: artifacts.FontSize, gatherer: 'seo/font-size'},
|
|
99
|
-
{id: artifacts.
|
|
99
|
+
{id: artifacts.Inputs, gatherer: 'inputs'},
|
|
100
100
|
{id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
|
|
101
101
|
{id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
|
|
102
102
|
{id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
|
|
@@ -148,7 +148,7 @@ const defaultConfig = {
|
|
|
148
148
|
artifacts.DOMStats,
|
|
149
149
|
artifacts.EmbeddedContent,
|
|
150
150
|
artifacts.FontSize,
|
|
151
|
-
artifacts.
|
|
151
|
+
artifacts.Inputs,
|
|
152
152
|
artifacts.GlobalListeners,
|
|
153
153
|
artifacts.IFrameElements,
|
|
154
154
|
artifacts.ImageElements,
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lighthouse",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
4
4
|
"description": "Automated auditing, performance metrics, and best practices for the web.",
|
|
5
5
|
"main": "./lighthouse-core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -121,7 +121,6 @@
|
|
|
121
121
|
"@types/lodash.set": "^4.3.6",
|
|
122
122
|
"@types/node": "*",
|
|
123
123
|
"@types/pako": "^1.0.1",
|
|
124
|
-
"@types/raven": "^2.5.1",
|
|
125
124
|
"@types/resize-observer-browser": "^0.1.1",
|
|
126
125
|
"@types/semver": "^5.5.0",
|
|
127
126
|
"@types/tabulator-tables": "^4.9.1",
|
|
@@ -183,6 +182,7 @@
|
|
|
183
182
|
"webtreemap-cdt": "^3.2.1"
|
|
184
183
|
},
|
|
185
184
|
"dependencies": {
|
|
185
|
+
"@sentry/node": "^6.17.4",
|
|
186
186
|
"axe-core": "4.3.5",
|
|
187
187
|
"chrome-launcher": "^0.15.0",
|
|
188
188
|
"configstore": "^5.0.1",
|
|
@@ -204,7 +204,6 @@
|
|
|
204
204
|
"open": "^8.4.0",
|
|
205
205
|
"parse-cache-control": "1.0.1",
|
|
206
206
|
"ps-list": "^8.0.0",
|
|
207
|
-
"raven": "^2.2.1",
|
|
208
207
|
"robots-parser": "^3.0.0",
|
|
209
208
|
"semver": "^5.3.0",
|
|
210
209
|
"speedline-core": "^1.4.3",
|
package/report/renderer/dom.js
CHANGED
|
@@ -293,10 +293,8 @@ export class DOM {
|
|
|
293
293
|
* @param {string} filename
|
|
294
294
|
*/
|
|
295
295
|
saveFile(blob, filename) {
|
|
296
|
-
const ext = blob.type.match('json') ? '.json' : '.html';
|
|
297
|
-
|
|
298
296
|
const a = this.createElement('a');
|
|
299
|
-
a.download =
|
|
297
|
+
a.download = filename;
|
|
300
298
|
this.safelySetBlobHref(a, blob);
|
|
301
299
|
this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
|
|
302
300
|
a.click();
|