lighthouse 9.2.0 → 9.3.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 (141) hide show
  1. package/changelog.md +67 -0
  2. package/dist/report/bundle.esm.js +23 -11
  3. package/dist/report/flow.js +11 -11
  4. package/dist/report/standalone.js +8 -8
  5. package/flow-report/src/common.tsx +1 -1
  6. package/flow-report/src/sidebar/flow.tsx +1 -1
  7. package/flow-report/src/util.ts +1 -2
  8. package/lighthouse-cli/bin.js +0 -4
  9. package/lighthouse-cli/cli-flags.js +22 -8
  10. package/lighthouse-cli/run.js +5 -1
  11. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +4 -2
  12. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  13. package/lighthouse-cli/test/smokehouse/lighthouse-runners/devtools.js +103 -0
  14. package/lighthouse-cli/test/smokehouse/readme.md +10 -8
  15. package/lighthouse-cli/test/smokehouse/report-assert.js +1 -1
  16. package/lighthouse-cli/test/smokehouse/smokehouse.js +4 -5
  17. package/lighthouse-core/audits/accessibility/axe-audit.js +4 -4
  18. package/lighthouse-core/audits/accessibility/object-alt.js +3 -3
  19. package/lighthouse-core/audits/audit.js +19 -4
  20. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
  21. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +1 -1
  22. package/lighthouse-core/audits/deprecations.js +2 -2
  23. package/lighthouse-core/audits/dobetterweb/inspector-issues.js +11 -11
  24. package/lighthouse-core/audits/dobetterweb/uses-http2.js +1 -1
  25. package/lighthouse-core/audits/font-display.js +1 -1
  26. package/lighthouse-core/audits/installable-manifest.js +1 -1
  27. package/lighthouse-core/audits/is-on-https.js +1 -1
  28. package/lighthouse-core/audits/maskable-icon.js +1 -1
  29. package/lighthouse-core/audits/multi-check-audit.js +1 -1
  30. package/lighthouse-core/audits/network-requests.js +4 -4
  31. package/lighthouse-core/audits/preload-fonts.js +2 -3
  32. package/lighthouse-core/audits/preload-lcp-image.js +2 -12
  33. package/lighthouse-core/audits/seo/is-crawlable.js +2 -1
  34. package/lighthouse-core/audits/themed-omnibox.js +1 -1
  35. package/lighthouse-core/audits/uses-rel-preload.js +2 -3
  36. package/lighthouse-core/audits/valid-source-maps.js +2 -2
  37. package/lighthouse-core/computed/image-records.js +1 -1
  38. package/lighthouse-core/computed/js-bundles.js +1 -1
  39. package/lighthouse-core/computed/metrics/lantern-metric.js +1 -1
  40. package/lighthouse-core/computed/metrics/timing-summary.js +35 -35
  41. package/lighthouse-core/computed/page-dependency-graph.js +2 -2
  42. package/lighthouse-core/computed/resource-summary.js +1 -1
  43. package/lighthouse-core/config/config-helpers.js +2 -2
  44. package/lighthouse-core/config/config.js +1 -1
  45. package/lighthouse-core/fraggle-rock/config/filters.js +23 -1
  46. package/lighthouse-core/fraggle-rock/config/validation.js +2 -2
  47. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +5 -4
  48. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +0 -1
  49. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +0 -1
  50. package/lighthouse-core/fraggle-rock/user-flow.js +4 -4
  51. package/lighthouse-core/gather/connections/connection.js +1 -1
  52. package/lighthouse-core/gather/driver/execution-context.js +1 -1
  53. package/lighthouse-core/gather/driver/network-monitor.js +2 -2
  54. package/lighthouse-core/gather/driver/target-manager.js +1 -1
  55. package/lighthouse-core/gather/driver.js +1 -1
  56. package/lighthouse-core/gather/gatherers/console-messages.js +2 -2
  57. package/lighthouse-core/gather/gatherers/inspector-issues.js +34 -46
  58. package/lighthouse-core/gather/gatherers/js-usage.js +1 -1
  59. package/lighthouse-core/gather/gatherers/seo/font-size.js +1 -1
  60. package/lighthouse-core/gather/gatherers/trace-elements.js +8 -43
  61. package/lighthouse-core/index.js +4 -3
  62. package/lighthouse-core/lib/arbitrary-equality-map.js +1 -1
  63. package/lighthouse-core/lib/asset-saver.js +6 -4
  64. package/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js +1 -1
  65. package/lighthouse-core/lib/dependency-graph/simulator/network-analyzer.js +1 -1
  66. package/lighthouse-core/lib/emulation.js +1 -1
  67. package/lighthouse-core/lib/icons.js +1 -2
  68. package/lighthouse-core/lib/median-run.js +1 -2
  69. package/lighthouse-core/lib/network-recorder.js +2 -2
  70. package/lighthouse-core/lib/network-request.js +1 -1
  71. package/lighthouse-core/lib/stack-packs.js +5 -0
  72. package/lighthouse-core/lib/tracehouse/cpu-profile-model.js +4 -6
  73. package/lighthouse-core/lib/tracehouse/main-thread-tasks.js +1 -1
  74. package/lighthouse-core/lib/tracehouse/trace-processor.js +14 -17
  75. package/lighthouse-core/lib/url-shim.js +15 -0
  76. package/lighthouse-core/runner.js +62 -49
  77. package/lighthouse-core/scripts/download-chrome.sh +15 -2
  78. package/package.json +21 -25
  79. package/report/assets/templates.html +2 -1
  80. package/report/clients/standalone.js +5 -1
  81. package/report/renderer/category-renderer.js +1 -1
  82. package/report/renderer/components.js +2 -2
  83. package/report/renderer/performance-category-renderer.js +4 -4
  84. package/report/renderer/report-renderer.js +1 -1
  85. package/report/renderer/report-ui-features.js +15 -3
  86. package/report/test/generator/report-generator-test.js +1 -1
  87. package/report/test/renderer/report-ui-features-test.js +22 -4
  88. package/report/test-assets/faux-psi.js +2 -3
  89. package/report/types/report-renderer.d.ts +2 -0
  90. package/shared/localization/locales/ar-XB.json +40 -4
  91. package/shared/localization/locales/ar.json +59 -23
  92. package/shared/localization/locales/bg.json +40 -4
  93. package/shared/localization/locales/ca.json +45 -9
  94. package/shared/localization/locales/cs.json +39 -3
  95. package/shared/localization/locales/da.json +43 -7
  96. package/shared/localization/locales/de.json +42 -6
  97. package/shared/localization/locales/el.json +59 -23
  98. package/shared/localization/locales/en-GB.json +43 -7
  99. package/shared/localization/locales/en-US.json +48 -6
  100. package/shared/localization/locales/en-XA.json +40 -4
  101. package/shared/localization/locales/en-XL.json +48 -6
  102. package/shared/localization/locales/es-419.json +39 -3
  103. package/shared/localization/locales/es.json +46 -10
  104. package/shared/localization/locales/fi.json +57 -21
  105. package/shared/localization/locales/fil.json +40 -4
  106. package/shared/localization/locales/fr.json +45 -9
  107. package/shared/localization/locales/he.json +41 -5
  108. package/shared/localization/locales/hi.json +59 -23
  109. package/shared/localization/locales/hr.json +57 -21
  110. package/shared/localization/locales/hu.json +39 -3
  111. package/shared/localization/locales/id.json +40 -4
  112. package/shared/localization/locales/it.json +40 -4
  113. package/shared/localization/locales/ja.json +39 -3
  114. package/shared/localization/locales/ko.json +40 -4
  115. package/shared/localization/locales/lt.json +40 -4
  116. package/shared/localization/locales/lv.json +56 -20
  117. package/shared/localization/locales/nl.json +40 -4
  118. package/shared/localization/locales/no.json +56 -20
  119. package/shared/localization/locales/pl.json +56 -20
  120. package/shared/localization/locales/pt-PT.json +57 -21
  121. package/shared/localization/locales/pt.json +41 -5
  122. package/shared/localization/locales/ro.json +39 -3
  123. package/shared/localization/locales/ru.json +40 -4
  124. package/shared/localization/locales/sk.json +39 -3
  125. package/shared/localization/locales/sl.json +39 -3
  126. package/shared/localization/locales/sr-Latn.json +40 -4
  127. package/shared/localization/locales/sr.json +40 -4
  128. package/shared/localization/locales/sv.json +39 -3
  129. package/shared/localization/locales/ta.json +47 -11
  130. package/shared/localization/locales/te.json +61 -25
  131. package/shared/localization/locales/th.json +39 -3
  132. package/shared/localization/locales/tr.json +40 -4
  133. package/shared/localization/locales/uk.json +40 -4
  134. package/shared/localization/locales/vi.json +63 -27
  135. package/shared/localization/locales/zh-HK.json +40 -4
  136. package/shared/localization/locales/zh-TW.json +45 -9
  137. package/shared/localization/locales/zh.json +41 -5
  138. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -1
  139. package/third-party/snyk/snapshot.json +4 -1
  140. package/types/artifacts.d.ts +16 -6
  141. package/types/robots-parser/index.d.ts +0 -20
@@ -93,7 +93,7 @@ const FlowStepThumbnail: FunctionComponent<{
93
93
  }
94
94
 
95
95
  let thumbnail;
96
- if (frames && frames.length) {
96
+ if (frames?.length) {
97
97
  thumbnail = frames[frames.length - 1].data;
98
98
  if (lhr.gatherMode === 'timespan') {
99
99
  return <FlowStepAnimatedThumbnail frames={frames} width={width} height={height} />;
@@ -59,7 +59,7 @@ export const SidebarFlow: FunctionComponent = () => {
59
59
  mode={lhr.gatherMode}
60
60
  href={`#index=${index}`}
61
61
  label={name}
62
- isCurrent={index === (hashState && hashState.index)}
62
+ isCurrent={index === hashState?.index}
63
63
  />
64
64
  </>;
65
65
  })
@@ -44,8 +44,7 @@ function getScreenDimensions(reportResult: LH.Result) {
44
44
  function getFullPageScreenshot(reportResult: LH.Result) {
45
45
  const fullPageScreenshotAudit = reportResult.audits['full-page-screenshot'];
46
46
  const fullPageScreenshot =
47
- fullPageScreenshotAudit &&
48
- fullPageScreenshotAudit.details &&
47
+ fullPageScreenshotAudit?.details &&
49
48
  fullPageScreenshotAudit.details.type === 'full-page-screenshot' &&
50
49
  fullPageScreenshotAudit.details;
51
50
 
@@ -24,7 +24,6 @@ import url from 'url';
24
24
  import module from 'module';
25
25
 
26
26
  import log from 'lighthouse-logger';
27
- import updateNotifier from 'update-notifier';
28
27
 
29
28
  import * as commands from './commands/commands.js';
30
29
  import * as Printer from './printer.js';
@@ -51,9 +50,6 @@ function isDev() {
51
50
  * @return {Promise<LH.RunnerResult|void>}
52
51
  */
53
52
  async function begin() {
54
- // Tell user if there's a newer version of LH.
55
- updateNotifier({pkg}).notify();
56
-
57
53
  const cliFlags = getFlags();
58
54
 
59
55
  // Process terminating command
@@ -178,7 +178,6 @@ function getFlags(manualArgv, options = {}) {
178
178
  },
179
179
  'enable-error-reporting': {
180
180
  type: 'boolean',
181
- default: undefined, // Explicitly `undefined` so prompted by default.
182
181
  describe: 'Enables error reporting, overriding any saved preference. --no-enable-error-reporting will do the opposite. More: https://git.io/vFFTO',
183
182
  },
184
183
  'gather-mode': {
@@ -343,19 +342,26 @@ function getFlags(manualArgv, options = {}) {
343
342
 
344
343
  // Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
345
344
  // so for now cast to add yarg's camelCase properties to type.
346
- const argv = parser.argv;
345
+ const argv = /** @type {Awaited<typeof parser.argv>} */ (parser.argv);
347
346
  const cliFlags = /** @type {typeof argv & CamelCasify<typeof argv>} */ (argv);
348
347
 
348
+ // yargs will return `undefined` for options that have a `coerce` function but
349
+ // are not actually present in the user input. Instead of passing properties
350
+ // explicitly set to undefined, delete them from the flags object.
351
+ for (const [k, v] of Object.entries(cliFlags)) {
352
+ if (v === undefined) delete cliFlags[k];
353
+ }
354
+
349
355
  return cliFlags;
350
356
  }
351
357
 
352
358
  /**
353
359
  * Support comma-separated values for some array flags by splitting on any ',' found.
354
360
  * @param {Array<string>=} strings
355
- * @return {Array<string>|undefined}
361
+ * @return {Array<string>=}
356
362
  */
357
363
  function splitCommaSeparatedValues(strings) {
358
- if (!strings) return strings;
364
+ if (!strings) return;
359
365
 
360
366
  return strings.flatMap(value => value.split(','));
361
367
  }
@@ -365,7 +371,9 @@ function splitCommaSeparatedValues(strings) {
365
371
  * @return {boolean|string|undefined}
366
372
  */
367
373
  function coerceOptionalStringBoolean(value) {
368
- if (typeof value !== 'undefined' && typeof value !== 'string' && typeof value !== 'boolean') {
374
+ if (value === undefined) return;
375
+
376
+ if (typeof value !== 'string' && typeof value !== 'boolean') {
369
377
  throw new Error('Invalid value: Argument must be a string or a boolean');
370
378
  }
371
379
  return value;
@@ -399,9 +407,11 @@ function coerceOutput(values) {
399
407
  * allowlist specific locales. Why? So we can support the user who requests 'es-MX' (unsupported)
400
408
  * and we'll fall back to 'es' (supported).
401
409
  * @param {unknown} value
402
- * @return {LH.Locale}
410
+ * @return {LH.Locale|undefined}
403
411
  */
404
412
  function coerceLocale(value) {
413
+ if (value === undefined) return;
414
+
405
415
  if (typeof value !== 'string') throw new Error(`Invalid value: Argument 'locale' must be a string`);
406
416
  return /** @type {LH.Locale} */ (value);
407
417
  }
@@ -431,9 +441,11 @@ function coerceExtraHeaders(value) {
431
441
  /**
432
442
  * Take yarg's unchecked object value and ensure it's proper throttling settings.
433
443
  * @param {unknown} value
434
- * @return {LH.ThrottlingSettings}
444
+ * @return {LH.ThrottlingSettings|undefined}
435
445
  */
436
446
  function coerceThrottling(value) {
447
+ if (value === undefined) return;
448
+
437
449
  if (!isObjectOfUnknownValues(value)) {
438
450
  throw new Error(`Invalid value: Argument 'throttling' must be an object, specified per-property ('throttling.rttMs', 'throttling.throughputKbps', etc)`);
439
451
  }
@@ -465,9 +477,11 @@ function coerceThrottling(value) {
465
477
  /**
466
478
  * Take yarg's unchecked object value and ensure it is a proper LH.screenEmulationSettings.
467
479
  * @param {unknown} value
468
- * @return {Partial<LH.ScreenEmulationSettings>}
480
+ * @return {Partial<LH.ScreenEmulationSettings>|undefined}
469
481
  */
470
482
  function coerceScreenEmulation(value) {
483
+ if (value === undefined) return;
484
+
471
485
  if (!isObjectOfUnknownValues(value)) {
472
486
  throw new Error(`Invalid value: Argument 'screenEmulation' must be an object, specified per-property ('screenEmulation.width', 'screenEmulation.deviceScaleFactor', etc)`);
473
487
  }
@@ -232,6 +232,10 @@ async function runLighthouse(url, flags, config) {
232
232
  let launchedChrome;
233
233
 
234
234
  try {
235
+ if (url && flags.auditMode && !flags.gatherMode) {
236
+ log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
237
+ }
238
+
235
239
  const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
236
240
  const shouldUseLocalChrome = URL.isLikeLocalhost(flags.hostname);
237
241
  if (shouldGather && shouldUseLocalChrome) {
@@ -254,7 +258,7 @@ async function runLighthouse(url, flags, config) {
254
258
  // Runtime errors indicate something was *very* wrong with the page result.
255
259
  // We don't want the user to have to parse the report to figure it out, so we'll still exit
256
260
  // with an error code after we saved the results.
257
- if (runnerResult && runnerResult.lhr.runtimeError) {
261
+ if (runnerResult?.lhr.runtimeError) {
258
262
  const {runtimeError} = runnerResult.lhr;
259
263
  return printErrorAndExit({
260
264
  name: 'LHError',
@@ -36,6 +36,7 @@ const coreTestDefnsPath =
36
36
  const runnerPaths = {
37
37
  cli: '../lighthouse-runners/cli.js',
38
38
  bundle: '../lighthouse-runners/bundle.js',
39
+ devtools: '../lighthouse-runners/devtools.js',
39
40
  };
40
41
 
41
42
  /**
@@ -151,7 +152,7 @@ async function begin() {
151
152
  },
152
153
  'runner': {
153
154
  default: 'cli',
154
- choices: ['cli', 'bundle'],
155
+ choices: ['cli', 'bundle', 'devtools'],
155
156
  describe: 'The method of running Lighthouse',
156
157
  },
157
158
  'tests-path': {
@@ -174,7 +175,8 @@ async function begin() {
174
175
 
175
176
  // Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
176
177
  // so for now cast to add yarg's camelCase properties to type.
177
- const argv = /** @type {typeof rawArgv & CamelCasify<typeof rawArgv>} */ (rawArgv);
178
+ const argv =
179
+ /** @type {Awaited<typeof rawArgv> & CamelCasify<Awaited<typeof rawArgv>>} */ (rawArgv);
178
180
 
179
181
  const jobs = Number.isFinite(argv.jobs) ? argv.jobs : undefined;
180
182
  const retries = Number.isFinite(argv.retries) ? argv.retries : undefined;
@@ -133,7 +133,7 @@ async function internalRun(url, tmpPath, configJson, options) {
133
133
 
134
134
  // There should either be both an error exitCode and a lhr.runtimeError or neither.
135
135
  if (Boolean(exitCode) !== Boolean(lhr.runtimeError)) {
136
- const runtimeErrorCode = lhr.runtimeError && lhr.runtimeError.code;
136
+ const runtimeErrorCode = lhr.runtimeError?.code;
137
137
  throw new ChildProcessError(`Lighthouse did not exit with an error correctly, exiting with ${exitCode} but with runtimeError '${runtimeErrorCode}'`, // eslint-disable-line max-len
138
138
  localConsole.getLog());
139
139
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @license Copyright 2021 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
+ /**
9
+ * @fileoverview A runner that launches Chrome and executes Lighthouse via DevTools.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import os from 'os';
14
+ import {spawn} from 'child_process';
15
+
16
+ import {LH_ROOT} from '../../../../root.js';
17
+
18
+ const devtoolsDir =
19
+ process.env.DEVTOOLS_PATH || `${LH_ROOT}/.tmp/chromium-web-tests/devtools/devtools-frontend`;
20
+
21
+ /**
22
+ * @param {string} command
23
+ * @param {string[]} args
24
+ */
25
+ async function spawnAndLog(command, args) {
26
+ let log = '';
27
+
28
+ /** @type {Promise<void>} */
29
+ const promise = new Promise((resolve, reject) => {
30
+ const spawnHandle = spawn(command, args);
31
+ spawnHandle.on('close', code => {
32
+ if (code === 0) resolve();
33
+ else reject(new Error(`Command exited with code ${code}`));
34
+ });
35
+ spawnHandle.on('error', reject);
36
+ spawnHandle.stdout.on('data', data => {
37
+ console.log(data.toString());
38
+ log += `STDOUT: ${data.toString()}`;
39
+ });
40
+ spawnHandle.stderr.on('data', data => {
41
+ console.log(data.toString());
42
+ log += `STDERR: ${data.toString()}`;
43
+ });
44
+ });
45
+ await promise;
46
+
47
+ return log;
48
+ }
49
+
50
+ /** @type {Promise<void>} */
51
+ let buildDevtoolsPromise;
52
+ /**
53
+ * Download/pull latest DevTools, build Lighthouse for DevTools, roll to DevTools, and build DevTools.
54
+ */
55
+ async function buildDevtools() {
56
+ if (process.env.CI) return;
57
+
58
+ process.env.DEVTOOLS_PATH = devtoolsDir;
59
+ await spawnAndLog('bash', ['lighthouse-core/test/chromium-web-tests/download-devtools.sh']);
60
+ await spawnAndLog('bash', ['lighthouse-core/test/chromium-web-tests/roll-devtools.sh']);
61
+ }
62
+
63
+ /**
64
+ * Launch Chrome and do a full Lighthouse run via DevTools.
65
+ * By default, the latest DevTools frontend is used (.tmp/chromium-web-tests/devtools/devtools-frontend)
66
+ * unless DEVTOOLS_PATH is set.
67
+ * CHROME_PATH determines which Chrome is used–otherwise the default is puppeteer's chrome binary.
68
+ * @param {string} url
69
+ * @param {LH.Config.Json=} configJson
70
+ * @param {{isDebug?: boolean}=} testRunnerOptions
71
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
72
+ */
73
+ async function runLighthouse(url, configJson, testRunnerOptions = {}) {
74
+ if (!buildDevtoolsPromise) buildDevtoolsPromise = buildDevtools();
75
+ await buildDevtoolsPromise;
76
+
77
+ const outputDir = fs.mkdtempSync(os.tmpdir() + '/lh-smoke-cdt-runner-');
78
+ const args = [
79
+ 'run-devtools',
80
+ url,
81
+ `--custom-devtools-frontend=file://${devtoolsDir}/out/Default/gen/front_end`,
82
+ '--output-dir', outputDir,
83
+ ];
84
+ if (configJson) {
85
+ args.push('--config', JSON.stringify(configJson));
86
+ }
87
+
88
+ const log = await spawnAndLog('yarn', args);
89
+ const lhr = JSON.parse(fs.readFileSync(`${outputDir}/lhr-0.json`, 'utf-8'));
90
+ const artifacts = JSON.parse(fs.readFileSync(`${outputDir}/artifacts-0.json`, 'utf-8'));
91
+
92
+ if (testRunnerOptions.isDebug) {
93
+ console.log(`${url} results saved at ${outputDir}`);
94
+ } else {
95
+ fs.rmSync(outputDir, {recursive: true, force: true});
96
+ }
97
+
98
+ return {lhr, artifacts, log};
99
+ }
100
+
101
+ export {
102
+ runLighthouse,
103
+ };
@@ -118,14 +118,15 @@ Smokehouse Frontends Lighthouse Runners
118
118
  | | | | | <lhr | +--------------+
119
119
  +------------+ | +-------+-------+ | | |
120
120
  | ^ +-->+ bundle.js |
121
- +------------+ | | | |
122
- | | | | +--------------+
123
- | lib.js +----+ v
124
- | | +--------+--------+
125
- +------------+ | |
126
- | report/assert |
127
- | |
128
- +-----------------+
121
+ +------------+ | | | | |
122
+ | | | | | +--------------+
123
+ | lib.js +----+ v |
124
+ | | +--------+--------+ |
125
+ +------------+ | | | +--------------+
126
+ | report/assert | | | |
127
+ | | +-->+ devtools.js |
128
+ +-----------------+ | |
129
+ +--------------+
129
130
  ```
130
131
 
131
132
  ### Smokehouse frontends
@@ -142,6 +143,7 @@ Smokehouse Frontends Lighthouse Runners
142
143
 
143
144
  - `lighthouse-runners/cli.js` - the original test runner, exercising the Lighthouse CLI from command-line argument parsing to the results written to disk on completion.
144
145
  - `lighthouse-runners/bundle.js` - a smoke test runner that operates on an already-bundled version of Lighthouse for end-to-end testing of that version.
146
+ - `lighthouse-runners/devtools.js` - a smoke test runner that operates on Lighthouse running from inside DevTools.
145
147
 
146
148
  ## Custom smoke tests (for plugins et al.)
147
149
 
@@ -162,7 +162,7 @@ function makeComparison(name, actualResult, expectedResult) {
162
162
  */
163
163
  function pruneExpectations(localConsole, lhr, expected, reportOptions) {
164
164
  const isFraggleRock = lhr.configSettings.channel === 'fraggle-rock-cli';
165
- const isBundled = reportOptions && reportOptions.isBundled;
165
+ const isBundled = reportOptions?.isBundled;
166
166
 
167
167
  /**
168
168
  * Lazily compute the Chrome version because some reports are explicitly asserting error conditions.
@@ -32,7 +32,7 @@
32
32
  * @property {Run[]} runs
33
33
  */
34
34
 
35
- import {strict as assert} from 'assert';
35
+ import assert from 'assert';
36
36
 
37
37
  import log from 'lighthouse-logger';
38
38
 
@@ -244,7 +244,7 @@ function getShardedDefinitions(testDefns, shardArg) {
244
244
  // eslint-disable-next-line max-len
245
245
  const errorMessage = `'shard' must be of the form 'n/d' and n and d must be positive integers with 1 ≤ n ≤ d. Got '${shardArg}'`;
246
246
  const match = /^(?<shardNumber>\d+)\/(?<shardTotal>\d+)$/.exec(shardArg);
247
- assert(match && match.groups, errorMessage);
247
+ assert(match?.groups, errorMessage);
248
248
  const shardNumber = Number(match.groups.shardNumber);
249
249
  const shardTotal = Number(match.groups.shardTotal);
250
250
  assert(shardNumber > 0 && Number.isInteger(shardNumber), errorMessage);
@@ -267,15 +267,14 @@ function getShardedDefinitions(testDefns, shardArg) {
267
267
  shards.push(testDefns.slice(index, index + shardSize));
268
268
  index += shardSize;
269
269
  }
270
- assert.equal(shards.length, shardTotal);
271
- assert.deepEqual(shards.flat(), testDefns);
270
+ assert.strictEqual(shards.length, shardTotal);
271
+ assert.deepStrictEqual(shards.flat(), testDefns);
272
272
 
273
273
  const shardDefns = shards[shardNumber - 1];
274
274
  console.log(`In this shard (${shardArg}), running: ${shardDefns.map(d => d.id).join(' ')}\n`);
275
275
  return shardDefns;
276
276
  }
277
277
 
278
-
279
278
  export {
280
279
  runSmokehouse,
281
280
  getShardedDefinitions,
@@ -47,7 +47,7 @@ class AxeAudit extends Audit {
47
47
  // If aXe reports an error, then bubble it up to the caller
48
48
  const incomplete = artifacts.Accessibility.incomplete || [];
49
49
  const incompleteResult = incomplete.find(result => result.id === this.meta.id);
50
- if (incompleteResult && incompleteResult.error) {
50
+ if (incompleteResult?.error) {
51
51
  return {
52
52
  score: null,
53
53
  errorMessage: `axe-core Error: ${incompleteResult.error.message || 'Unknown error'}`,
@@ -58,8 +58,8 @@ class AxeAudit extends Audit {
58
58
  const violations = artifacts.Accessibility.violations || [];
59
59
  const failureCases = isInformative ? violations.concat(incomplete) : violations;
60
60
  const rule = failureCases.find(result => result.id === this.meta.id);
61
- const impact = rule && rule.impact;
62
- const tags = rule && rule.tags;
61
+ const impact = rule?.impact;
62
+ const tags = rule?.tags;
63
63
 
64
64
  // Handle absence of aXe failure results for informative rules as 'not applicable'
65
65
  // This scenario indicates that no action is required by the web property owner
@@ -73,7 +73,7 @@ class AxeAudit extends Audit {
73
73
 
74
74
  /** @type {LH.Audit.Details.Table['items']}>} */
75
75
  let items = [];
76
- if (rule && rule.nodes) {
76
+ if (rule?.nodes) {
77
77
  items = rule.nodes.map(axeNode => ({
78
78
  node: {
79
79
  ...Audit.makeNodeItem(axeNode.node),
@@ -15,11 +15,11 @@ const i18n = require('../../lib/i18n/i18n.js');
15
15
 
16
16
  const UIStrings = {
17
17
  /** Title of an accesibility audit that evaluates if all object elements have an alt HTML attribute that describes their contents. This title is descriptive of the successful state and is shown to users when no user action is required. */
18
- title: '`<object>` elements have `[alt]` text',
18
+ title: '`<object>` elements have alternate text',
19
19
  /** Title of an accesibility audit that evaluates if all object elements have an alt HTML attribute that describes their contents. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
20
- failureTitle: '`<object>` elements do not have `[alt]` text',
20
+ failureTitle: '`<object>` elements do not have alternate text',
21
21
  /** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
22
- description: 'Screen readers cannot translate non-text content. Adding alt text to ' +
22
+ description: 'Screen readers cannot translate non-text content. Adding alternate text to ' +
23
23
  '`<object>` elements helps screen readers convey meaning to users. ' +
24
24
  '[Learn more](https://web.dev/object-alt/).',
25
25
  };
@@ -73,13 +73,28 @@ class Audit {
73
73
  * considering a log-normal distribution governed by two control points (the 10th
74
74
  * percentile value and the median value) and represents the percentage of sites that are
75
75
  * greater than `value`.
76
+ *
77
+ * Score characteristics:
78
+ * - within [0, 1]
79
+ * - rounded to two digits
80
+ * - value must meet or beat a controlPoint value to meet or exceed its percentile score:
81
+ * - value > median will give a score < 0.5; value ≤ median will give a score ≥ 0.5.
82
+ * - value > p10 will give a score < 0.9; value ≤ p10 will give a score ≥ 0.9.
83
+ * - values < p10 will get a slight boost so a score of 1 is achievable by a
84
+ * `value` other than those close to 0. Scores of > ~0.99524 end up rounded to 1.
76
85
  * @param {{median: number, p10: number}} controlPoints
77
86
  * @param {number} value
78
87
  * @return {number}
79
88
  */
80
89
  static computeLogNormalScore(controlPoints, value) {
81
- const percentile = statistics.getLogNormalScore(controlPoints, value);
82
- return clampTo2Decimals(percentile);
90
+ let percentile = statistics.getLogNormalScore(controlPoints, value);
91
+ // Add a boost to scores of 90+, linearly ramping from 0 at 0.9 to half a
92
+ // point (0.005) at 1. Expands scores in (0.9, 1] to (0.9, 1.005], so more top
93
+ // scores will be a perfect 1 after the two-digit `Math.floor()` rounding below.
94
+ if (percentile > 0.9) { // getLogNormalScore ensures `percentile` can't exceed 1.
95
+ percentile += 0.05 * (percentile - 0.9);
96
+ }
97
+ return Math.floor(percentile * 100) / 100;
83
98
  }
84
99
 
85
100
  /**
@@ -360,8 +375,8 @@ class Audit {
360
375
 
361
376
  score,
362
377
  scoreDisplayMode,
363
- numericValue: numericProduct && numericProduct.numericValue,
364
- numericUnit: numericProduct && numericProduct.numericUnit,
378
+ numericValue: numericProduct?.numericValue,
379
+ numericUnit: numericProduct?.numericUnit,
365
380
 
366
381
  displayValue: product.displayValue,
367
382
  explanation: product.explanation,
@@ -109,7 +109,7 @@ class UnusedBytes extends Audit {
109
109
  const gatherContext = artifacts.GatherContext;
110
110
  const trace = artifacts.traces[Audit.DEFAULT_PASS];
111
111
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
112
- const settings = context && context.settings || {};
112
+ const settings = context?.settings || {};
113
113
  const simulatorOptions = {
114
114
  devtoolsLog,
115
115
  settings,
@@ -127,7 +127,7 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
127
127
  */
128
128
  static async audit_(artifacts, networkRecords, context) {
129
129
  const ignoreThresholdInBytes =
130
- context.options && context.options.ignoreThresholdInBytes || IGNORE_THRESHOLD_IN_BYTES;
130
+ context.options?.ignoreThresholdInBytes || IGNORE_THRESHOLD_IN_BYTES;
131
131
  const duplication =
132
132
  await DuplicatedJavascript._getDuplicationGroupedByNodeModules(artifacts, context);
133
133
  const mainDocumentRecord = NetworkAnalyzer.findOptionalMainDocument(networkRecords);
@@ -62,8 +62,8 @@ class Deprecations extends Audit {
62
62
  const bundles = await JsBundles.request(artifacts, context);
63
63
 
64
64
  let deprecations;
65
- if (artifacts.InspectorIssues.deprecations.length) {
66
- deprecations = artifacts.InspectorIssues.deprecations
65
+ if (artifacts.InspectorIssues.deprecationIssue.length) {
66
+ deprecations = artifacts.InspectorIssues.deprecationIssue
67
67
  .map(deprecation => {
68
68
  const {url, lineNumber, columnNumber} = deprecation.sourceCodeLocation;
69
69
  const bundle = bundles.find(bundle => bundle.script.src === url);
@@ -58,7 +58,7 @@ class IssuesPanelEntries extends Audit {
58
58
  static getMixedContentRow(mixedContentIssues) {
59
59
  const requestUrls = new Set();
60
60
  for (const issue of mixedContentIssues) {
61
- const requestUrl = (issue.request && issue.request.url) || issue.mainResourceURL;
61
+ const requestUrl = issue.request?.url || issue.mainResourceURL;
62
62
  requestUrls.add(requestUrl);
63
63
  }
64
64
  return {
@@ -77,7 +77,7 @@ class IssuesPanelEntries extends Audit {
77
77
  static getSameSiteCookieRow(sameSiteCookieIssues) {
78
78
  const requestUrls = new Set();
79
79
  for (const issue of sameSiteCookieIssues) {
80
- const requestUrl = (issue.request && issue.request.url) || issue.cookieUrl;
80
+ const requestUrl = (issue.request?.url) || issue.cookieUrl;
81
81
  if (requestUrl) {
82
82
  requestUrls.add(requestUrl);
83
83
  }
@@ -102,7 +102,7 @@ class IssuesPanelEntries extends Audit {
102
102
  static getBlockedByResponseRow(blockedByResponseIssues) {
103
103
  const requestUrls = new Set();
104
104
  for (const issue of blockedByResponseIssues) {
105
- const requestUrl = issue.request && issue.request.url;
105
+ const requestUrl = issue.request?.url;
106
106
  if (requestUrl) {
107
107
  requestUrls.add(requestUrl);
108
108
  }
@@ -161,19 +161,19 @@ class IssuesPanelEntries extends Audit {
161
161
  /** @type LH.Audit.Details.TableItem[] */
162
162
  const items = [];
163
163
 
164
- if (issues.mixedContent.length) {
165
- items.push(this.getMixedContentRow(issues.mixedContent));
164
+ if (issues.mixedContentIssue.length) {
165
+ items.push(this.getMixedContentRow(issues.mixedContentIssue));
166
166
  }
167
- if (issues.sameSiteCookies.length) {
168
- items.push(this.getSameSiteCookieRow(issues.sameSiteCookies));
167
+ if (issues.sameSiteCookieIssue.length) {
168
+ items.push(this.getSameSiteCookieRow(issues.sameSiteCookieIssue));
169
169
  }
170
- if (issues.blockedByResponse.length) {
171
- items.push(this.getBlockedByResponseRow(issues.blockedByResponse));
170
+ if (issues.blockedByResponseIssue.length) {
171
+ items.push(this.getBlockedByResponseRow(issues.blockedByResponseIssue));
172
172
  }
173
- if (issues.heavyAds.length) {
173
+ if (issues.heavyAdIssue.length) {
174
174
  items.push({issueType: str_(UIStrings.issueTypeHeavyAds)});
175
175
  }
176
- const cspIssues = issues.contentSecurityPolicy.filter(issue => {
176
+ const cspIssues = issues.contentSecurityPolicyIssue.filter(issue => {
177
177
  // kTrustedTypesSinkViolation and kTrustedTypesPolicyViolation aren't currently supported by the Issues panel
178
178
  return issue.contentSecurityPolicyViolationType !== 'kTrustedTypesSinkViolation' &&
179
179
  issue.contentSecurityPolicyViolationType !== 'kTrustedTypesPolicyViolation';
@@ -227,7 +227,7 @@ class UsesHTTP2Audit extends Audit {
227
227
  };
228
228
  }
229
229
 
230
- const settings = context && context.settings || {};
230
+ const settings = context?.settings || {};
231
231
  const simulatorOptions = {
232
232
  devtoolsLog,
233
233
  settings,
@@ -79,7 +79,7 @@ class FontDisplay extends Audit {
79
79
  // Find the font-display value by matching a single token, optionally surrounded by whitespace,
80
80
  // followed either by a semicolon or the end of a block.
81
81
  const fontDisplayMatch = declaration.match(/font-display\s*:\s*(\w+)\s*(;|\})/);
82
- const rawFontDisplay = (fontDisplayMatch && fontDisplayMatch[1]) || '';
82
+ const rawFontDisplay = fontDisplayMatch?.[1] || '';
83
83
  const hasPassingFontDisplay = passingFontDisplayRegex.test(rawFontDisplay);
84
84
  const targetURLSet = hasPassingFontDisplay ? passingURLs : failingURLs;
85
85
 
@@ -169,7 +169,7 @@ class InstallableManifest extends Audit {
169
169
  * If there is an argument value, get it.
170
170
  * We only expect a `minimum-icon-size-in-pixels` errorArg[0] for two errorIds, currently.
171
171
  */
172
- const value0 = err.errorArguments && err.errorArguments.length && err.errorArguments[0].value;
172
+ const value0 = err.errorArguments?.length && err.errorArguments[0].value;
173
173
 
174
174
  if (matchingString && err.errorArguments.length !== UIStringArguments.length) {
175
175
  // Matching string, but have the incorrect number of arguments for the message.
@@ -87,7 +87,7 @@ class HTTPS extends Audit {
87
87
  {key: 'resolution', itemType: 'text', text: str_(UIStrings.columnResolution)},
88
88
  ];
89
89
 
90
- for (const details of artifacts.InspectorIssues.mixedContent) {
90
+ for (const details of artifacts.InspectorIssues.mixedContentIssue) {
91
91
  let item = items.find(item => item.url === details.insecureURL);
92
92
  if (!item) {
93
93
  item = {url: details.insecureURL};
@@ -62,7 +62,7 @@ class MaskableIcon extends Audit {
62
62
  }
63
63
  const maskableIconCheck = manifestValues.allChecks.find(i => i.id === 'hasMaskableIcon');
64
64
  return {
65
- score: (maskableIconCheck && maskableIconCheck.passing) ? 1 : 0,
65
+ score: maskableIconCheck?.passing ? 1 : 0,
66
66
  };
67
67
  }
68
68
  }
@@ -35,7 +35,7 @@ class MultiCheckAudit extends Audit {
35
35
  allChecks: undefined,
36
36
  };
37
37
 
38
- if (result.manifestValues && result.manifestValues.allChecks) {
38
+ if (result.manifestValues?.allChecks) {
39
39
  result.manifestValues.allChecks.forEach(check => {
40
40
  detailsItem[check.id] = check.passing;
41
41
  });
@@ -41,10 +41,10 @@ class NetworkRequests extends Audit {
41
41
  undefined : (time - earliestStartTime) * 1000;
42
42
 
43
43
  const results = records.map(record => {
44
- const endTimeDeltaMs = record.lrStatistics && record.lrStatistics.endTimeDeltaMs;
45
- const TCPMs = record.lrStatistics && record.lrStatistics.TCPMs;
46
- const requestMs = record.lrStatistics && record.lrStatistics.requestMs;
47
- const responseMs = record.lrStatistics && record.lrStatistics.responseMs;
44
+ const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
45
+ const TCPMs = record.lrStatistics?.TCPMs;
46
+ const requestMs = record.lrStatistics?.requestMs;
47
+ const responseMs = record.lrStatistics?.responseMs;
48
48
 
49
49
  return {
50
50
  url: URL.elideDataURI(record.url),
@@ -97,9 +97,8 @@ class PreloadFontsAudit extends Audit {
97
97
  * @return {Promise<LH.Audit.Product>}
98
98
  */
99
99
  static async audit() {
100
- // Preload advice is dangerous until https://bugs.chromium.org/p/chromium/issues/detail?id=788757
101
- // has been fixed and validated. All preload audits are on hold until then.
102
- // See https://github.com/GoogleChrome/lighthouse/issues/11960 for more discussion.
100
+ // Preload advice is on hold until https://github.com/GoogleChrome/lighthouse/issues/11960
101
+ // is resolved.
103
102
  return {score: 1, notApplicable: true};
104
103
  }
105
104
  }