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.
Files changed (58) hide show
  1. package/dist/report/bundle.esm.js +38 -24
  2. package/dist/report/flow.js +5 -5
  3. package/dist/report/standalone.js +9 -9
  4. package/flow-report/src/topbar.tsx +1 -1
  5. package/flow-report/src/wrappers/report.tsx +2 -1
  6. package/flow-report/test/topbar-test.tsx +1 -1
  7. package/lighthouse-cli/bin.js +1 -4
  8. package/lighthouse-cli/run.js +1 -1
  9. package/lighthouse-cli/test/smokehouse/core-tests.js +4 -5
  10. package/lighthouse-cli/test/smokehouse/readme.md +4 -0
  11. package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
  12. package/lighthouse-core/audits/autocomplete.js +34 -37
  13. package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
  14. package/lighthouse-core/audits/seo/hreflang.js +2 -17
  15. package/lighthouse-core/config/default-config.js +2 -2
  16. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
  17. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +50 -31
  18. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -5
  19. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +4 -5
  20. package/lighthouse-core/fraggle-rock/user-flow.js +8 -6
  21. package/lighthouse-core/gather/driver/navigation.js +29 -10
  22. package/lighthouse-core/gather/driver/network-monitor.js +8 -6
  23. package/lighthouse-core/gather/driver/prepare.js +13 -7
  24. package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
  25. package/lighthouse-core/gather/gather-runner.js +1 -1
  26. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
  27. package/lighthouse-core/gather/gatherers/inputs.js +113 -0
  28. package/lighthouse-core/index.js +3 -3
  29. package/lighthouse-core/lib/page-functions.js +11 -1
  30. package/lighthouse-core/lib/sentry.js +39 -24
  31. package/lighthouse-core/runner.js +79 -52
  32. package/lighthouse-core/util-commonjs.js +4 -0
  33. package/package.json +2 -3
  34. package/readme.md +1 -1
  35. package/report/assets/templates.html +2 -5
  36. package/report/renderer/components.js +3 -3
  37. package/report/renderer/dom.js +1 -3
  38. package/report/renderer/report-ui-features.js +25 -17
  39. package/report/renderer/topbar-features.js +5 -1
  40. package/report/renderer/util.js +4 -0
  41. package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
  42. package/report/test/renderer/report-renderer-axe-test.js +38 -17
  43. package/report/test-assets/faux-psi.js +2 -1
  44. package/report/types/report-renderer.d.ts +21 -2
  45. package/shared/localization/locales/en-US.json +7 -1
  46. package/shared/localization/locales/en-XL.json +7 -1
  47. package/third-party/axe/LICENSE +362 -0
  48. package/third-party/axe/README.md +6 -0
  49. package/third-party/axe/valid-langs.js +110 -0
  50. package/third-party/snyk/snapshot.json +1 -0
  51. package/tsconfig.json +1 -0
  52. package/types/artifacts.d.ts +14 -14
  53. package/types/global-lh.d.ts +1 -0
  54. package/types/parse-cache-control/index.d.ts +1 -0
  55. package/types/smokehouse.d.ts +3 -0
  56. package/changelog.md +0 -5803
  57. package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
  58. 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(url, stepOptions) {
61
- const options = {url, ...this.options, ...stepOptions};
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 {string} url
83
+ * @param {LH.NavigationRequestor} requestor
85
84
  * @param {StepOptions=} stepOptions
86
85
  */
87
- async navigate(url, stepOptions) {
86
+ async navigate(requestor, stepOptions) {
88
87
  if (this.currentTimespan) throw Error('Timespan already in progress');
89
88
 
90
- const result = await navigation(this._getNextNavigationOptions(url, stepOptions));
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 {string} url
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, url, options) {
86
- const status = {msg: `Navigating to ${url}`, id: 'lh:driver:navigate'};
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
- // No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413
98
- session.setNextProtocolTimeout(Infinity);
99
- const waitforPageNavigateCmd = session.sendCommand('Page.navigate', {url});
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 finalUrl = (await networkMonitor.getFinalNavigationUrl()) || url;
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 waitforPageNavigateCmd;
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: url}),
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 | undefined>} */
137
- async getFinalNavigationUrl() {
136
+ /** @return {Promise<{requestedUrl?: string, finalUrl?: string}>} */
137
+ async getNavigationUrls() {
138
138
  const frameNavigations = this._frameNavigations;
139
- if (!frameNavigations.length) return undefined;
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
- const finalNavigation = mainFrameNavigations[mainFrameNavigations.length - 1];
145
- if (!finalNavigation) log.warn('NetworkMonitor', 'No detected navigations');
144
+ if (!mainFrameNavigations.length) log.warn('NetworkMonitor', 'No detected navigations');
146
145
 
147
- return finalNavigation?.url;
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 {{url: string}} navigation
74
+ * @param {string} url
75
75
  * @return {Promise<{warnings: Array<LH.IcuMessage>}>}
76
76
  */
77
- async function resetStorageForNavigation(session, navigation) {
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, navigation.url);
82
+ const warning = await storage.getImportantStorageWarning(session, url);
83
83
  if (warning) warnings.push(warning);
84
- await storage.clearDataForOrigin(session, navigation.url);
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'> & {url: string}} navigation
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 shouldResetStorage = !settings.disableStorageReset && !navigation.disableStorageReset;
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 {warnings: storageWarnings} = await resetStorageForNavigation(session, navigation);
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
- session.once('Page.domContentEventFired', domContentLoadedListener);
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 && clearTimeout(idleTimeout);
184
- session.off('Page.domContentEventFired', domContentLoadedListener);
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
- url: passContext.url,
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 getMaxScreenshotHeight(context) {
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 maxScreenshotHeight = await this.getMaxScreenshotHeight(context);
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, maxScreenshotHeight);
80
- const height = Math.min(metrics.contentSize.height, maxScreenshotHeight);
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
- // TODO: elements collected earlier in gathering are likely to have been shifted by now.
93
- // The lower in the page, the more likely (footer elements especially).
94
- // https://github.com/GoogleChrome/lighthouse/issues/11118
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;
@@ -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 gatherFn = () => {
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.run(gatherFn, options);
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) clone.setAttribute(attributeName, attributeValue);
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('raven').CaptureOptions} CaptureOptions */
11
- /** @typedef {import('raven').ConstructorOptions} ConstructorOptions */
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, options?: CaptureOptions) => void} */
34
+ /** @type {(message: string, level?: Severity) => void} */
33
35
  captureMessage: noop,
34
- /** @type {(breadcrumb: any) => void} */
36
+ /** @type {(breadcrumb: Breadcrumb) => void} */
35
37
  captureBreadcrumb: noop,
36
38
  /** @type {() => any} */
37
39
  getContext: noop,
38
- /** @type {(error: Error, options?: CaptureOptions) => Promise<void>} */
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: ConstructorOptions}} opts
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('raven');
62
- const sentryConfig = Object.assign({}, opts.environmentData,
63
- {captureUnhandledRejections: true});
64
- Sentry.config(SENTRY_URL, sentryConfig).install();
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.captureBreadcrumb(...args);
69
- sentryDelegate.getContext = () => Sentry.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
- return new Promise(resolve => {
111
- Sentry.captureException(err, opts, () => resolve());
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 raven library, errors will not be reported.'
139
+ 'Could not load Sentry, errors will not be reported.'
125
140
  );
126
141
  }
127
142
  }