lighthouse 8.6.0-dev.20211109 → 9.0.0-dev.20211117

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 (76) hide show
  1. package/changelog.md +121 -0
  2. package/dist/report/bundle.esm.js +5994 -0
  3. package/dist/report/flow.js +16 -16
  4. package/dist/report/standalone.js +8 -8
  5. package/flow-report/src/app.tsx +10 -9
  6. package/flow-report/src/help-dialog.tsx +0 -1
  7. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -0
  8. package/lighthouse-core/audits/dobetterweb/uses-http2.js +27 -11
  9. package/lighthouse-core/audits/installable-manifest.js +16 -7
  10. package/lighthouse-core/config/default-config.js +2 -2
  11. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -1
  12. package/lighthouse-core/gather/gather-runner.js +29 -6
  13. package/lighthouse-core/lib/emulation.js +45 -1
  14. package/lighthouse-core/lib/page-functions.js +2 -1
  15. package/lighthouse-core/util-commonjs.js +17 -0
  16. package/package.json +4 -4
  17. package/report/assets/styles.css +11 -10
  18. package/report/renderer/components.js +1 -1
  19. package/report/renderer/performance-category-renderer.js +0 -1
  20. package/report/renderer/report-renderer.js +1 -1
  21. package/report/renderer/topbar-features.js +2 -1
  22. package/report/renderer/util.js +17 -0
  23. package/report/test/renderer/performance-category-renderer-test.js +15 -4
  24. package/report/test/renderer/report-renderer-axe-test.js +2 -2
  25. package/report/test/renderer/util-test.js +22 -0
  26. package/shared/localization/locales/ar-XB.json +73 -25
  27. package/shared/localization/locales/ar.json +73 -25
  28. package/shared/localization/locales/bg.json +73 -25
  29. package/shared/localization/locales/ca.json +73 -25
  30. package/shared/localization/locales/cs.json +90 -42
  31. package/shared/localization/locales/da.json +73 -25
  32. package/shared/localization/locales/de.json +90 -42
  33. package/shared/localization/locales/el.json +73 -25
  34. package/shared/localization/locales/en-GB.json +73 -25
  35. package/shared/localization/locales/en-US.json +6 -0
  36. package/shared/localization/locales/en-XA.json +73 -25
  37. package/shared/localization/locales/en-XL.json +6 -0
  38. package/shared/localization/locales/es-419.json +91 -43
  39. package/shared/localization/locales/es.json +91 -43
  40. package/shared/localization/locales/fi.json +73 -25
  41. package/shared/localization/locales/fil.json +73 -25
  42. package/shared/localization/locales/fr.json +90 -42
  43. package/shared/localization/locales/he.json +74 -26
  44. package/shared/localization/locales/hi.json +73 -25
  45. package/shared/localization/locales/hr.json +73 -25
  46. package/shared/localization/locales/hu.json +90 -42
  47. package/shared/localization/locales/id.json +90 -42
  48. package/shared/localization/locales/it.json +90 -42
  49. package/shared/localization/locales/ja.json +90 -42
  50. package/shared/localization/locales/ko.json +90 -42
  51. package/shared/localization/locales/lt.json +90 -42
  52. package/shared/localization/locales/lv.json +96 -48
  53. package/shared/localization/locales/nl.json +90 -42
  54. package/shared/localization/locales/no.json +73 -25
  55. package/shared/localization/locales/pl.json +73 -25
  56. package/shared/localization/locales/pt-PT.json +73 -25
  57. package/shared/localization/locales/pt.json +90 -42
  58. package/shared/localization/locales/ro.json +91 -43
  59. package/shared/localization/locales/ru.json +90 -42
  60. package/shared/localization/locales/sk.json +90 -42
  61. package/shared/localization/locales/sl.json +90 -42
  62. package/shared/localization/locales/sr-Latn.json +90 -42
  63. package/shared/localization/locales/sr.json +90 -42
  64. package/shared/localization/locales/sv.json +73 -25
  65. package/shared/localization/locales/ta.json +90 -42
  66. package/shared/localization/locales/te.json +90 -42
  67. package/shared/localization/locales/th.json +90 -42
  68. package/shared/localization/locales/tr.json +90 -42
  69. package/shared/localization/locales/uk.json +93 -45
  70. package/shared/localization/locales/vi.json +90 -42
  71. package/shared/localization/locales/zh-HK.json +90 -42
  72. package/shared/localization/locales/zh-TW.json +90 -42
  73. package/shared/localization/locales/zh.json +90 -42
  74. package/third-party/chromium-synchronization/installability-errors-test.js +1 -2
  75. package/third-party/snyk/snapshot.json +4 -1
  76. package/types/lhr/lhr.d.ts +1 -1
@@ -16,21 +16,22 @@ import {Topbar} from './topbar';
16
16
  import {Header} from './header';
17
17
  import {I18nProvider} from './i18n/i18n';
18
18
 
19
+ function getAnchorElement(hashState: LH.FlowResult.HashState|null) {
20
+ if (!hashState || !hashState.anchor) return null;
21
+ return document.getElementById(hashState.anchor);
22
+ }
23
+
19
24
  const Content: FunctionComponent = () => {
20
25
  const hashState = useHashState();
21
26
  const ref = useRef<HTMLDivElement>(null);
22
27
 
23
28
  useLayoutEffect(() => {
24
- if (hashState && hashState.anchor) {
25
- const el = document.getElementById(hashState.anchor);
26
- if (el) {
27
- el.scrollIntoView({behavior: 'smooth'});
28
- return;
29
- }
29
+ const el = getAnchorElement(hashState);
30
+ if (el) {
31
+ el.scrollIntoView();
32
+ } else if (ref.current) {
33
+ ref.current.scrollTop = 0;
30
34
  }
31
-
32
- // Scroll to top no anchor is found.
33
- if (ref.current) ref.current.scrollTop = 0;
34
35
  }, [hashState]);
35
36
 
36
37
  return (
@@ -97,7 +97,6 @@ export const HelpDialog: FunctionComponent<{onClose: () => void}> = ({
97
97
  availableCategories={[
98
98
  strings.categoryPerformance,
99
99
  strings.categoryBestPractices,
100
- strings.categorySeo,
101
100
  ]}
102
101
  />
103
102
  <HelpDialogColumn
@@ -206,6 +206,8 @@ class UnusedBytes extends Audit {
206
206
  */
207
207
  static computeWastedMsWithThroughput(wastedBytes, simulator) {
208
208
  const bitsPerSecond = simulator.getOptions().throughput;
209
+ // https://github.com/GoogleChrome/lighthouse/pull/13323#issuecomment-962031709
210
+ if (bitsPerSecond === 0) return 0;
209
211
  const wastedBits = wastedBytes * 8;
210
212
  const wastedMs = wastedBits / bitsPerSecond * 1000;
211
213
  return wastedMs;
@@ -61,8 +61,8 @@ class UsesHTTP2Audit extends Audit {
61
61
  title: str_(UIStrings.title),
62
62
  description: str_(UIStrings.description),
63
63
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
64
- supportedModes: ['navigation'],
65
- requiredArtifacts: ['URL', 'devtoolsLogs', 'traces'],
64
+ supportedModes: ['timespan', 'navigation'],
65
+ requiredArtifacts: ['URL', 'devtoolsLogs', 'traces', 'GatherContext'],
66
66
  };
67
67
  }
68
68
 
@@ -202,24 +202,40 @@ class UsesHTTP2Audit extends Audit {
202
202
  static async audit(artifacts, context) {
203
203
  const trace = artifacts.traces[Audit.DEFAULT_PASS];
204
204
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
205
+ const networkRecords = await NetworkRecords.request(devtoolsLog, context);
206
+ const resources = UsesHTTP2Audit.determineNonHttp2Resources(networkRecords);
207
+
208
+ let displayValue;
209
+ if (resources.length > 0) {
210
+ displayValue = str_(UIStrings.displayValue, {itemCount: resources.length});
211
+ }
212
+
213
+ // TODO: Compute actual savings for timespan mode.
214
+ if (artifacts.GatherContext.gatherMode === 'timespan') {
215
+ /** @type {LH.Audit.Details.Table['headings']} */
216
+ const headings = [
217
+ {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
218
+ {key: 'protocol', itemType: 'text', text: str_(UIStrings.columnProtocol)},
219
+ ];
220
+
221
+ const details = Audit.makeTableDetails(headings, resources);
222
+
223
+ return {
224
+ displayValue,
225
+ score: resources.length ? 0 : 1,
226
+ details,
227
+ };
228
+ }
229
+
205
230
  const settings = context && context.settings || {};
206
231
  const simulatorOptions = {
207
232
  devtoolsLog,
208
233
  settings,
209
234
  };
210
-
211
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
212
235
  const graph = await PageDependencyGraph.request({trace, devtoolsLog}, context);
213
236
  const simulator = await LoadSimulator.request(simulatorOptions, context);
214
-
215
- const resources = UsesHTTP2Audit.determineNonHttp2Resources(networkRecords);
216
237
  const wastedMs = UsesHTTP2Audit.computeWasteWithTTIGraph(resources, graph, simulator);
217
238
 
218
- let displayValue;
219
- if (resources.length > 0) {
220
- displayValue = str_(UIStrings.displayValue, {itemCount: resources.length});
221
- }
222
-
223
239
  /** @type {LH.Audit.Details.Opportunity['headings']} */
224
240
  const headings = [
225
241
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
@@ -21,7 +21,7 @@ const UIStrings = {
21
21
  'columnValue': 'Failure reason',
22
22
  /**
23
23
  * @description [ICU Syntax] Label for an audit identifying the number of installability errors found in the page.
24
- */
24
+ */
25
25
  'displayValue': `{itemCount, plural,
26
26
  =1 {1 reason}
27
27
  other {# reasons}
@@ -50,15 +50,15 @@ const UIStrings = {
50
50
  * for the current page encloses the scope and start URL from the manifest. */
51
51
  'no-matching-service-worker': `No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest.`,
52
52
  /**
53
- * @description Error message explaining that the manifest does not contain a suitable icon.
54
- * @example {192} value0
55
- */
53
+ * @description Error message explaining that the manifest does not contain a suitable icon.
54
+ * @example {192} value0
55
+ */
56
56
  'manifest-missing-suitable-icon': `Manifest does not contain a suitable icon - PNG, SVG or WebP format of at least {value0}\xa0px is required, the sizes attribute must be set, and the purpose attribute, if set, must include "any".`,
57
57
 
58
58
  /**
59
- * @description Error message explaining that the manifest does not supply an icon of the correct format.
60
- * @example {192} value0
61
- */
59
+ * @description Error message explaining that the manifest does not supply an icon of the correct format.
60
+ * @example {192} value0
61
+ */
62
62
  'no-acceptable-icon': `No supplied icon is at least {value0}\xa0px square in PNG, SVG or WebP format, with the purpose attribute unset or set to "any"`,
63
63
 
64
64
  /** Error message explaining that the icon could not be downloaded. */
@@ -93,6 +93,10 @@ const UIStrings = {
93
93
  'manifest-location-changed': `Manifest URL changed while the manifest was being fetched.`,
94
94
  /** Warning message explaining that the page does not work offline. */
95
95
  'warn-not-offline-capable': `Page does not work offline. The page will not be regarded as installable after Chrome 93, stable release August 2021.`,
96
+ /** Error message explaining that Lighthouse failed while detecting a service worker, and directing the user to try again in a new Chrome. */
97
+ 'protocol-timeout': `Lighthouse could not determine if there was a service worker. Please try with a newer version of Chrome.`,
98
+ /** Message logged when the web app has been uninstalled o desktop, signalling that the install banner state is being reset. */
99
+ 'pipeline-restarted': 'PWA has been uninstalled and installability checks resetting.',
96
100
  };
97
101
  /* eslint-enable max-len */
98
102
 
@@ -144,6 +148,11 @@ class InstallableManifest extends Audit {
144
148
  continue;
145
149
  }
146
150
 
151
+ // Filter out errorId 'pipeline-restarted' since it only applies when the PWA is uninstalled.
152
+ if (err.errorId === 'pipeline-restarted') {
153
+ continue;
154
+ }
155
+
147
156
  // @ts-expect-error errorIds from protocol should match up against the strings dict
148
157
  const matchingString = UIStrings[err.errorId];
149
158
 
@@ -413,10 +413,10 @@ const defaultConfig = {
413
413
  supportedModes: ['navigation', 'timespan', 'snapshot'],
414
414
  auditRefs: [
415
415
  {id: 'first-contentful-paint', weight: 10, group: 'metrics', acronym: 'FCP', relevantAudits: m2a.fcpRelevantAudits},
416
- {id: 'speed-index', weight: 10, group: 'metrics', acronym: 'SI'},
417
- {id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP', relevantAudits: m2a.lcpRelevantAudits},
418
416
  {id: 'interactive', weight: 10, group: 'metrics', acronym: 'TTI'},
417
+ {id: 'speed-index', weight: 10, group: 'metrics', acronym: 'SI'},
419
418
  {id: 'total-blocking-time', weight: 30, group: 'metrics', acronym: 'TBT', relevantAudits: m2a.tbtRelevantAudits},
419
+ {id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP', relevantAudits: m2a.lcpRelevantAudits},
420
420
  {id: 'cumulative-layout-shift', weight: 15, group: 'metrics', acronym: 'CLS', relevantAudits: m2a.clsRelevantAudits},
421
421
 
422
422
  // These are our "invisible" metrics. Not displayed, but still in the LHR.
@@ -6,6 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const legacyDefaultConfig = require('../../config/default-config.js');
9
+ const {deepClone} = require('../../config/config-helpers.js');
9
10
 
10
11
  /** @type {LH.Config.AuditJson[]} */
11
12
  const frAudits = [
@@ -22,7 +23,8 @@ const frCategoryAuditRefExtensions = {
22
23
  /** @return {LH.Config.Json['categories']} */
23
24
  function mergeCategories() {
24
25
  if (!legacyDefaultConfig.categories) return {};
25
- const categories = legacyDefaultConfig.categories;
26
+ // Don't modify original default config.
27
+ const categories = deepClone(legacyDefaultConfig.categories);
26
28
  for (const key of Object.keys(frCategoryAuditRefExtensions)) {
27
29
  if (!categories[key]) continue;
28
30
  categories[key].auditRefs.push(...frCategoryAuditRefExtensions[key]);
@@ -422,21 +422,44 @@ class GatherRunner {
422
422
  static async populateBaseArtifacts(passContext) {
423
423
  const status = {msg: 'Populate base artifacts', id: 'lh:gather:populateBaseArtifacts'};
424
424
  log.time(status);
425
+
425
426
  const baseArtifacts = passContext.baseArtifacts;
426
427
 
427
428
  // Copy redirected URL to artifact.
428
429
  baseArtifacts.URL.finalUrl = passContext.url;
429
430
 
430
431
  // Fetch the manifest, if it exists.
431
- baseArtifacts.WebAppManifest = await WebAppManifest.getWebAppManifest(
432
- passContext.driver.defaultSession, passContext.url);
432
+ try {
433
+ baseArtifacts.WebAppManifest = await WebAppManifest.getWebAppManifest(
434
+ passContext.driver.defaultSession, passContext.url);
435
+ } catch (err) {
436
+ log.error('GatherRunner WebAppManifest', err);
437
+ baseArtifacts.WebAppManifest = null;
438
+ }
433
439
 
434
- if (baseArtifacts.WebAppManifest) {
435
- baseArtifacts.InstallabilityErrors = await InstallabilityErrors.getInstallabilityErrors(
436
- passContext.driver.defaultSession);
440
+ try {
441
+ if (baseArtifacts.WebAppManifest) {
442
+ baseArtifacts.InstallabilityErrors = await InstallabilityErrors.getInstallabilityErrors(
443
+ passContext.driver.defaultSession);
444
+ }
445
+ } catch (err) {
446
+ log.error('GatherRunner InstallabilityErrors', err);
447
+ baseArtifacts.InstallabilityErrors = {
448
+ errors: [
449
+ {
450
+ errorId: 'protocol-timeout',
451
+ errorArguments: [],
452
+ },
453
+ ],
454
+ };
437
455
  }
438
456
 
439
- baseArtifacts.Stacks = await Stacks.collectStacks(passContext.driver.executionContext);
457
+ try {
458
+ baseArtifacts.Stacks = await Stacks.collectStacks(passContext.driver.executionContext);
459
+ } catch (err) {
460
+ log.error('GatherRunner Stacks', err);
461
+ baseArtifacts.Stacks = [];
462
+ }
440
463
 
441
464
  // Find the NetworkUserAgent actually used in the devtoolsLogs.
442
465
  const devtoolsLog = baseArtifacts.devtoolsLogs[passContext.passConfig.passName];
@@ -5,6 +5,8 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ const {version: lighthouseVersion} = require('../../package.json');
9
+
8
10
  const NO_THROTTLING_METRICS = {
9
11
  latency: 0,
10
12
  downloadThroughput: 0,
@@ -16,6 +18,46 @@ const NO_CPU_THROTTLE_METRICS = {
16
18
  rate: 1,
17
19
  };
18
20
 
21
+ /**
22
+ * @param {string} userAgent
23
+ * @param {LH.Config.Settings['formFactor']} formFactor
24
+ * @return {LH.Crdp.Emulation.SetUserAgentOverrideRequest['userAgentMetadata']}
25
+ */
26
+ function parseUseragentIntoMetadata(userAgent, formFactor) {
27
+ const match = userAgent.match(/Chrome\/([\d.]+)/); // eg 'Chrome/(71.0.3577.0)'
28
+ const fullVersion = (match && match[1]) || '99.0.1234.0';
29
+ const [version] = fullVersion.split('.', 1);
30
+ const brands = [
31
+ {brand: 'Chromium', version},
32
+ {brand: 'Google Chrome', version},
33
+ {brand: 'Lighthouse', version: lighthouseVersion},
34
+ ];
35
+
36
+ const motoG4Details = {
37
+ platform: 'Android',
38
+ platformVersion: '6.0',
39
+ architecture: '',
40
+ model: 'Moto G4',
41
+ };
42
+ const macDesktopDetails = {
43
+ platform: 'macOS',
44
+ platformVersion: '10.15.7',
45
+ architecture: 'x86',
46
+ model: '',
47
+ };
48
+ const mobile = formFactor === 'mobile';
49
+
50
+ return {
51
+ brands,
52
+ fullVersion,
53
+ // Since config users can supply a custom useragent, they likely are emulating something
54
+ // other than Moto G4 and MacOS Desktop.
55
+ // TODO: Determine how to thoughtfully expose this metadata/client-hints configurability.
56
+ ...(mobile ? motoG4Details : macDesktopDetails),
57
+ mobile,
58
+ };
59
+ }
60
+
19
61
  /**
20
62
  * @param {LH.Gatherer.FRProtocolSession} session
21
63
  * @param {LH.Config.Settings} settings
@@ -23,8 +65,10 @@ const NO_CPU_THROTTLE_METRICS = {
23
65
  */
24
66
  async function emulate(session, settings) {
25
67
  if (settings.emulatedUserAgent !== false) {
68
+ const userAgent = /** @type {string} */ (settings.emulatedUserAgent);
26
69
  await session.sendCommand('Network.setUserAgentOverride', {
27
- userAgent: /** @type {string} */ (settings.emulatedUserAgent),
70
+ userAgent,
71
+ userAgentMetadata: parseUseragentIntoMetadata(userAgent, settings.formFactor),
28
72
  });
29
73
  }
30
74
  // See devtools-entry for one usecase for disabling screenEmulation
@@ -208,7 +208,8 @@ function computeBenchmarkIndex() {
208
208
 
209
209
  while (Date.now() - start < 500) {
210
210
  let s = '';
211
- for (let j = 0; j < 10000; j++) s += 'a'; // eslint-disable-line no-unused-vars
211
+ for (let j = 0; j < 10000; j++) s += 'a';
212
+ if (s.length === 1) throw new Error('will never happen, but prevents compiler optimizations');
212
213
 
213
214
  iterations++;
214
215
  }
@@ -111,6 +111,23 @@ class Util {
111
111
  /** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
112
112
  const relevantAuditToMetricsMap = new Map();
113
113
 
114
+ // This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
115
+ // Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
116
+ // Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
117
+ const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
118
+ const perfCategory = clone.categories['performance'];
119
+ if (majorVersion < 9 && perfCategory) {
120
+ if (!clone.categoryGroups) clone.categoryGroups = {};
121
+ clone.categoryGroups['hidden'] = {title: ''};
122
+ for (const auditRef of perfCategory.auditRefs) {
123
+ if (!auditRef.group) {
124
+ auditRef.group = 'hidden';
125
+ } else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
126
+ delete auditRef.group;
127
+ }
128
+ }
129
+ }
130
+
114
131
  for (const category of Object.values(clone.categories)) {
115
132
  // Make basic lookup table for relevantAudits
116
133
  category.auditRefs.forEach(metricRef => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "8.6.0-dev.20211109",
3
+ "version": "9.0.0-dev.20211117",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "node": ">=14.15"
13
13
  },
14
14
  "scripts": {
15
- "prepack": "yarn build-report --standalone --flow",
15
+ "prepack": "yarn build-report --standalone --flow --esm",
16
16
  "build-all": "npm-run-posix-or-windows build-all:task",
17
17
  "build-all:task": "yarn build-report && yarn build-cdt-lib && yarn build-devtools && (yarn build-extension & yarn build-lr & yarn build-viewer & yarn build-treemap & yarn build-smokehouse-bundle & wait) && yarn build-pack",
18
18
  "build-all:task:windows": "yarn build-report && yarn build-cdt-lib && yarn build-extension && yarn build-devtools && yarn build-lr && yarn build-viewer && yarn build-treemap && yarn build-smokehouse-bundle",
@@ -162,7 +162,7 @@
162
162
  "jsdom": "^12.2.0",
163
163
  "jsonld": "^5.2.0",
164
164
  "jsonlint-mod": "^1.7.6",
165
- "lighthouse-plugin-publisher-ads": "^1.4.1",
165
+ "lighthouse-plugin-publisher-ads": "^1.5.4",
166
166
  "magic-string": "^0.25.7",
167
167
  "mime-types": "^2.1.30",
168
168
  "node-fetch": "^2.6.1",
@@ -188,7 +188,7 @@
188
188
  },
189
189
  "dependencies": {
190
190
  "axe-core": "4.2.3",
191
- "chrome-launcher": "^0.14.2",
191
+ "chrome-launcher": "^0.15.0",
192
192
  "configstore": "^5.0.1",
193
193
  "csp_evaluator": "1.1.0",
194
194
  "cssstyle": "1.2.1",
@@ -482,9 +482,7 @@
482
482
  border-radius: 3px;
483
483
  color: var(--link-color);
484
484
  background-color: var(--report-background-color);
485
- padding: var(--default-padding);
486
- font-size: var(--report-font-size-secondary);
487
- line-height: var(--report-line-height-secondary);
485
+ margin: 5px;
488
486
  }
489
487
 
490
488
  .lh-button:first-of-type {
@@ -685,9 +683,8 @@
685
683
 
686
684
  .lh-metrics-container {
687
685
  display: grid;
688
- grid-template-rows: 1fr 1fr 1fr;
686
+ grid-auto-rows: 1fr;
689
687
  grid-template-columns: 1fr 1fr;
690
- grid-auto-flow: column;
691
688
  grid-column-gap: var(--report-line-height);
692
689
  }
693
690
 
@@ -695,7 +692,7 @@
695
692
  border-top: 1px solid var(--report-border-color-secondary);
696
693
  }
697
694
 
698
- .lh-metric:nth-child(3n+3) {
695
+ .lh-metric:nth-last-child(-n+2) {
699
696
  border-bottom: 1px solid var(--report-border-color-secondary);
700
697
  }
701
698
 
@@ -743,8 +740,11 @@
743
740
  display: block;
744
741
  }
745
742
 
746
- .lh-metric:nth-last-child(-n+1) {
747
- border-bottom: 1px solid var(--report-border-color-secondary);
743
+ .lh-metric {
744
+ border-bottom: none !important;
745
+ }
746
+ .lh-metric:nth-last-child(1) {
747
+ border-bottom: 1px solid var(--report-border-color-secondary) !important;
748
748
  }
749
749
 
750
750
  /* Change the grid to 3 columns for narrow viewport. */
@@ -943,7 +943,7 @@
943
943
 
944
944
 
945
945
  .lh-audit--error .lh-audit__display-text {
946
- color: var(--color-fail);
946
+ color: var(--color-fail-secondary);
947
947
  }
948
948
 
949
949
  /* Audit Group */
@@ -1055,7 +1055,7 @@
1055
1055
  }
1056
1056
 
1057
1057
  .lh-audit--fail .lh-audit-explanation {
1058
- color: var(--color-fail);
1058
+ color: var(--color-fail-secondary);
1059
1059
  }
1060
1060
 
1061
1061
  /* Report */
@@ -1332,6 +1332,7 @@
1332
1332
  margin-top: 10px;
1333
1333
  text-align: center;
1334
1334
  color: var(--report-text-color);
1335
+ word-break: keep-all;
1335
1336
  }
1336
1337
 
1337
1338
  /* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */