lighthouse 9.3.1-dev.20220215 → 9.4.0-dev.20220218

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.
@@ -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
- disableAutoDarkModeAndFireworks: true,
36
+ disableFireworks: true,
37
+ disableDarkMode: true,
37
38
  omitTopbar: true,
38
39
  omitGlobalStyles: true,
39
40
  onPageAnchorRendered: link => convertAnchor(link, hashState.index),
@@ -56,7 +56,7 @@ it('save button opens save dialog for HTML file', async () => {
56
56
 
57
57
  expect(mockSaveFile).toHaveBeenCalledWith(
58
58
  expect.any(Blob),
59
- 'User-flow_2021-09-14_22-24-22'
59
+ 'User-flow_2021-09-14_22-24-22.html'
60
60
  );
61
61
  });
62
62
 
@@ -137,12 +137,9 @@ async function begin() {
137
137
  url: urlUnderTest,
138
138
  flags: cliFlags,
139
139
  environmentData: {
140
- name: 'redacted', // prevent sentry from using hostname
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
  }
@@ -181,6 +181,7 @@ class CacheHeaders extends Audit {
181
181
  cacheControl['must-revalidate'] ||
182
182
  cacheControl['no-cache'] ||
183
183
  cacheControl['no-store'] ||
184
+ cacheControl['stale-while-revalidate'] ||
184
185
  cacheControl['private'])) {
185
186
  return true;
186
187
  }
@@ -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://developers.google.com/web/fundamentals/accessibility). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.',
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. */
@@ -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);
@@ -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',
@@ -58,13 +58,13 @@ function collectElements() {
58
58
  const parentFormIndex = parentFormEl ?
59
59
  [...formElToArtifact.keys()].indexOf(parentFormEl) :
60
60
  undefined;
61
- const labelIndicies = [...inputEl.labels || []].map((labelEl) => {
61
+ const labelIndices = [...inputEl.labels || []].map((labelEl) => {
62
62
  return [...labelElToArtifact.keys()].indexOf(labelEl);
63
63
  });
64
64
 
65
65
  inputArtifacts.push({
66
66
  parentFormIndex,
67
- labelIndicies,
67
+ labelIndices,
68
68
  id: inputEl.id,
69
69
  name: inputEl.name,
70
70
  type: inputEl.type,
@@ -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
  }
@@ -149,7 +149,7 @@ class Runner {
149
149
  Sentry.captureBreadcrumb({
150
150
  message: 'Run started',
151
151
  category: 'lifecycle',
152
- data: sentryContext?.extra,
152
+ data: sentryContext,
153
153
  });
154
154
 
155
155
  /** @type {LH.Artifacts} */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.3.1-dev.20220215",
3
+ "version": "9.4.0-dev.20220218",
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",
@@ -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 = `${filename}${ext}`;
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();
@@ -70,12 +70,13 @@ export class ReportUIFeatures {
70
70
  this._setupThirdPartyFilter();
71
71
  this._setupElementScreenshotOverlay(this._dom.rootEl);
72
72
 
73
- let turnOffTheLights = false;
74
73
  // Do not query the system preferences for DevTools - DevTools should only apply dark theme
75
74
  // if dark is selected in the settings panel.
76
- const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
75
+ // TODO: set `disableDarkMode` in devtools and delete this special case.
76
+ const disableDarkMode = this._dom.isDevTools() ||
77
+ this._opts.disableDarkMode || this._opts.disableAutoDarkModeAndFireworks;
77
78
  if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
78
- turnOffTheLights = true;
79
+ toggleDarkTheme(this._dom, true);
79
80
  }
80
81
 
81
82
  // Fireworks!
@@ -85,13 +86,12 @@ export class ReportUIFeatures {
85
86
  const cat = lhr.categories[id];
86
87
  return cat && cat.score === 1;
87
88
  });
88
- if (scoresAll100 && !this._opts.disableAutoDarkModeAndFireworks) {
89
- turnOffTheLights = true;
89
+ const disableFireworks =
90
+ this._opts.disableFireworks || this._opts.disableAutoDarkModeAndFireworks;
91
+ if (scoresAll100 && !disableFireworks) {
90
92
  this._enableFireworks();
91
- }
92
-
93
- if (turnOffTheLights) {
94
- toggleDarkTheme(this._dom, true);
93
+ // If dark mode is allowed, force it on because it looks so much better.
94
+ if (!disableDarkMode) toggleDarkTheme(this._dom, true);
95
95
  }
96
96
 
97
97
  // Show the metric descriptions by default when there is an error.
@@ -345,13 +345,15 @@ export class ReportUIFeatures {
345
345
  }
346
346
 
347
347
  /**
348
- * DevTools uses its own file manager to download files, so it redefines this function.
349
- * Wrapper is necessary so DevTools can still override this function.
350
- *
351
348
  * @param {Blob|File} blob
352
349
  */
353
350
  _saveFile(blob) {
354
- const filename = getLhrFilenamePrefix(this.json);
355
- this._dom.saveFile(blob, filename);
351
+ const ext = blob.type.match('json') ? '.json' : '.html';
352
+ const filename = getLhrFilenamePrefix(this.json) + ext;
353
+ if (this._opts.onSaveFileOverride) {
354
+ this._opts.onSaveFileOverride(blob, filename);
355
+ } else {
356
+ this._dom.saveFile(blob, filename);
357
+ }
356
358
  }
357
359
  }
@@ -199,7 +199,11 @@ export class TopbarFeatures {
199
199
  }
200
200
 
201
201
  _print() {
202
- self.print();
202
+ if (this._reportUIFeatures._opts.onPrintOverride) {
203
+ this._reportUIFeatures._opts.onPrintOverride(this._dom.rootEl);
204
+ } else {
205
+ self.print();
206
+ }
203
207
  }
204
208
 
205
209
  /**
@@ -46,7 +46,8 @@ async function renderLHReport() {
46
46
 
47
47
  const reportRootEl = lighthouseRenderer.renderReport(lhr, {
48
48
  omitTopbar: true,
49
- disableAutoDarkModeAndFireworks: true,
49
+ disableFireworks: true,
50
+ disableDarkMode: true,
50
51
  });
51
52
  // TODO: display warnings if appropriate.
52
53
  for (const el of reportRootEl.querySelectorAll('.lh-warnings--toplevel')) {
@@ -12,8 +12,16 @@ declare module Renderer {
12
12
 
13
13
  interface Options {
14
14
  /**
15
- * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.)
16
- * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */
15
+ * Disables automatically applying dark mode based on `prefers-color-scheme: dark`. Dark mode can still
16
+ * be manually applied by assigning the class `lh-dark` to the report element.
17
+ */
18
+ disableDarkMode?: boolean;
19
+ /** Disables the fireworks animation that plays when all core categories have a 100 score. */
20
+ disableFireworks?: boolean;
21
+ /**
22
+ * Disable dark mode and fireworks.
23
+ * @deprecated Use `disableDarkMode` and `disableFireworks` instead.
24
+ */
17
25
  disableAutoDarkModeAndFireworks?: boolean;
18
26
 
19
27
  /** Disable the topbar UI component */
@@ -27,6 +35,10 @@ declare module Renderer {
27
35
  onPageAnchorRendered?: (link: HTMLAnchorElement) => void;
28
36
  /** If defined, `Save as HTML` option is shown in dropdown menu. */
29
37
  getStandaloneReportHTML?: () => string;
38
+ /** If defined, renderer will call this instead of `self.print()` */
39
+ onPrintOverride?: (rootEl: HTMLElement) => Promise<void>;
40
+ /** If defined, renderer will call this instead of using a `<a download>.click()>` to trigger a JSON/HTML download. Blob will be either json or html. */
41
+ onSaveFileOverride?: (blob: Blob, suggestedFilename: string) => Promise<void>;
30
42
  /**
31
43
  * If defined, adds a `View Trace` button to the report, and calls this callback when clicked.
32
44
  * The callback should do something to present the user with a visualization of the trace
@@ -1668,7 +1668,7 @@
1668
1668
  "message": "Best practices"
1669
1669
  },
1670
1670
  "lighthouse-core/config/default-config.js | a11yCategoryDescription": {
1671
- "message": "These checks highlight opportunities to [improve the accessibility of your web app](https://developers.google.com/web/fundamentals/accessibility). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged."
1671
+ "message": "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."
1672
1672
  },
1673
1673
  "lighthouse-core/config/default-config.js | a11yCategoryManualDescription": {
1674
1674
  "message": "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)."
@@ -1668,7 +1668,7 @@
1668
1668
  "message": "B̂éŝt́ p̂ŕâćt̂íĉéŝ"
1669
1669
  },
1670
1670
  "lighthouse-core/config/default-config.js | a11yCategoryDescription": {
1671
- "message": "T̂h́êśê ćĥéĉḱŝ h́îǵĥĺîǵĥt́ ôṕp̂ór̂t́ûńît́îéŝ t́ô [ím̂ṕr̂óv̂é t̂h́ê áĉćêśŝíb̂íl̂ít̂ý ôf́ ŷóûŕ ŵéb̂ áp̂ṕ](https://developers.google.com/web/fundamentals/accessibility). Ôńl̂ý â śûb́ŝét̂ óf̂ áĉćêśŝíb̂íl̂ít̂ý îśŝúêś ĉán̂ b́ê áût́ôḿât́îćâĺl̂ý d̂ét̂éĉt́êd́ ŝó m̂án̂úâĺ t̂éŝt́îńĝ íŝ ál̂śô én̂ćôúr̂áĝéd̂."
1671
+ "message": "T̂h́êśê ćĥéĉḱŝ h́îǵĥĺîǵĥt́ ôṕp̂ór̂t́ûńît́îéŝ t́ô [ím̂ṕr̂óv̂é t̂h́ê áĉćêśŝíb̂íl̂ít̂ý ôf́ ŷóûŕ ŵéb̂ áp̂ṕ](https://web.dev/lighthouse-accessibility/). Ôńl̂ý â śûb́ŝét̂ óf̂ áĉćêśŝíb̂íl̂ít̂ý îśŝúêś ĉán̂ b́ê áût́ôḿât́îćâĺl̂ý d̂ét̂éĉt́êd́ ŝó m̂án̂úâĺ t̂éŝt́îńĝ íŝ ál̂śô én̂ćôúr̂áĝéd̂."
1672
1672
  },
1673
1673
  "lighthouse-core/config/default-config.js | a11yCategoryManualDescription": {
1674
1674
  "message": "T̂h́êśê ít̂ém̂ś âd́d̂ŕêśŝ ár̂éâś ŵh́îćĥ án̂ áût́ôḿât́êd́ t̂éŝt́îńĝ t́ôól̂ ćâńn̂ót̂ ćôv́êŕ. L̂éâŕn̂ ḿôŕê ín̂ óûŕ ĝúîd́ê ón̂ [ćôńd̂úĉt́îńĝ án̂ áĉćêśŝíb̂íl̂ít̂ý r̂év̂íêẃ](https://developers.google.com/web/fundamentals/accessibility/how-to-review)."
@@ -799,7 +799,7 @@ declare module Artifacts {
799
799
  /** If set, the parent form is the index into the associated FormElement array. Otherwise, the input element has no parent form. */
800
800
  parentFormIndex?: number;
801
801
  /** Array of indices into associated LabelElement array. */
802
- labelIndicies: number[];
802
+ labelIndices: number[];
803
803
  id: string;
804
804
  name: string;
805
805
  type: string;
@@ -12,6 +12,7 @@ declare module 'parse-cache-control' {
12
12
  'no-cache'?: boolean;
13
13
  'no-store'?: boolean;
14
14
  'private'?: boolean;
15
+ 'stale-while-revalidate'?: boolean;
15
16
  }
16
17
 
17
18
  function ParseCacheControl(headers?: string): CacheHeaders | null;