chrome-devtools-frontend 1.0.942095 → 1.0.942529
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/gni/all_devtools_files.gni +0 -44
- package/config/gni/devtools_grd_files.gni +42 -2
- package/config/gni/devtools_image_files.gni +1 -1
- package/front_end/.eslintrc.js +11 -1
- package/front_end/Images/src/{feedback_thin_16x16_icon.svg → survey_feedback_icon.svg} +1 -1
- package/front_end/core/sdk/DebuggerModel.ts +4 -14
- package/front_end/emulated_devices/module.json +1 -3
- package/front_end/entrypoints/devtools_app/devtools_app.json +0 -2
- package/front_end/entrypoints/shell/shell.json +0 -1
- package/front_end/legacy_test_runner/bindings_test_runner/IsolatedFilesystemTestRunner.js +2 -2
- package/front_end/legacy_test_runner/test_runner/TestRunner.js +3 -1
- package/front_end/panels/application/components/EndpointsGrid.ts +1 -1
- package/front_end/panels/application/components/ReportsGrid.ts +1 -1
- package/front_end/panels/elements/components/StylePropertyEditor.ts +2 -0
- package/front_end/panels/emulation/DeviceModeToolbar.ts +3 -1
- package/front_end/panels/emulation/DeviceModeView.ts +2 -1
- package/front_end/panels/emulation/InspectedPagePlaceholder.ts +3 -1
- package/front_end/panels/emulation/MediaQueryInspector.ts +3 -1
- package/front_end/panels/emulation/emulation-meta.ts +0 -2
- package/front_end/panels/lighthouse/LighthousePanel.ts +2 -4
- package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +1 -4
- package/front_end/panels/lighthouse/module.json +1 -4
- package/front_end/panels/profiler/module.json +1 -3
- package/front_end/panels/screencast/module.json +1 -4
- package/front_end/panels/timeline/module.json +0 -1
- package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1128 -1158
- package/front_end/third_party/lighthouse/locales/ar-XB.json +211 -79
- package/front_end/third_party/lighthouse/locales/ar.json +213 -81
- package/front_end/third_party/lighthouse/locales/bg.json +211 -79
- package/front_end/third_party/lighthouse/locales/ca.json +212 -80
- package/front_end/third_party/lighthouse/locales/cs.json +211 -79
- package/front_end/third_party/lighthouse/locales/da.json +211 -79
- package/front_end/third_party/lighthouse/locales/de.json +211 -79
- package/front_end/third_party/lighthouse/locales/el.json +213 -81
- package/front_end/third_party/lighthouse/locales/en-GB.json +211 -79
- package/front_end/third_party/lighthouse/locales/en-US.json +186 -75
- package/front_end/third_party/lighthouse/locales/en-XA.json +211 -79
- package/front_end/third_party/lighthouse/locales/en-XL.json +186 -75
- package/front_end/third_party/lighthouse/locales/es-419.json +211 -79
- package/front_end/third_party/lighthouse/locales/es.json +212 -80
- package/front_end/third_party/lighthouse/locales/fi.json +211 -79
- package/front_end/third_party/lighthouse/locales/fil.json +211 -79
- package/front_end/third_party/lighthouse/locales/fr.json +211 -79
- package/front_end/third_party/lighthouse/locales/he.json +212 -80
- package/front_end/third_party/lighthouse/locales/hi.json +214 -82
- package/front_end/third_party/lighthouse/locales/hr.json +211 -79
- package/front_end/third_party/lighthouse/locales/hu.json +211 -79
- package/front_end/third_party/lighthouse/locales/id.json +211 -79
- package/front_end/third_party/lighthouse/locales/it.json +211 -79
- package/front_end/third_party/lighthouse/locales/ja.json +211 -79
- package/front_end/third_party/lighthouse/locales/ko.json +211 -79
- package/front_end/third_party/lighthouse/locales/lt.json +211 -79
- package/front_end/third_party/lighthouse/locales/lv.json +214 -82
- package/front_end/third_party/lighthouse/locales/nl.json +211 -79
- package/front_end/third_party/lighthouse/locales/no.json +211 -79
- package/front_end/third_party/lighthouse/locales/pl.json +211 -79
- package/front_end/third_party/lighthouse/locales/pt-PT.json +211 -79
- package/front_end/third_party/lighthouse/locales/pt.json +211 -79
- package/front_end/third_party/lighthouse/locales/ro.json +212 -80
- package/front_end/third_party/lighthouse/locales/ru.json +211 -79
- package/front_end/third_party/lighthouse/locales/sk.json +211 -79
- package/front_end/third_party/lighthouse/locales/sl.json +211 -79
- package/front_end/third_party/lighthouse/locales/sr-Latn.json +211 -79
- package/front_end/third_party/lighthouse/locales/sr.json +211 -79
- package/front_end/third_party/lighthouse/locales/sv.json +211 -79
- package/front_end/third_party/lighthouse/locales/ta.json +218 -86
- package/front_end/third_party/lighthouse/locales/te.json +251 -119
- package/front_end/third_party/lighthouse/locales/th.json +211 -79
- package/front_end/third_party/lighthouse/locales/tr.json +211 -79
- package/front_end/third_party/lighthouse/locales/uk.json +212 -80
- package/front_end/third_party/lighthouse/locales/vi.json +211 -79
- package/front_end/third_party/lighthouse/locales/zh-HK.json +211 -79
- package/front_end/third_party/lighthouse/locales/zh-TW.json +211 -79
- package/front_end/third_party/lighthouse/locales/zh.json +211 -79
- package/front_end/third_party/lighthouse/report/bundle.d.ts +72 -34
- package/front_end/third_party/lighthouse/report/bundle.js +698 -492
- package/front_end/third_party/lighthouse/report-assets/report-generator.js +1 -2
- package/front_end/third_party/lighthouse/report-assets/report.js +40 -35
- package/front_end/third_party/lighthouse/report-assets/standalone-template.html +2 -4
- package/front_end/ui/components/docs/icon_button/basic.ts +3 -3
- package/front_end/ui/components/helpers/get-stylesheet.ts +0 -14
- package/front_end/ui/components/markdown_view/MarkdownImagesMap.ts +1 -1
- package/front_end/ui/components/survey_link/SurveyLink.ts +1 -1
- package/front_end/ui/legacy/Dialog.ts +3 -1
- package/front_end/ui/legacy/DropTarget.ts +2 -1
- package/front_end/ui/legacy/EmptyWidget.ts +2 -1
- package/front_end/ui/legacy/FilterBar.ts +2 -1
- package/front_end/ui/legacy/GlassPane.ts +4 -2
- package/front_end/ui/legacy/Infobar.ts +3 -2
- package/front_end/ui/legacy/InspectorView.ts +6 -1
- package/front_end/ui/legacy/ListWidget.ts +2 -1
- package/front_end/ui/legacy/PopoverHelper.ts +2 -1
- package/front_end/ui/legacy/ProgressIndicator.ts +2 -1
- package/front_end/ui/legacy/RemoteDebuggingTerminatedScreen.ts +2 -1
- package/front_end/ui/legacy/ReportView.ts +2 -1
- package/front_end/ui/legacy/RootView.ts +2 -1
- package/front_end/ui/legacy/SearchableView.ts +2 -1
- package/front_end/ui/legacy/SoftContextMenu.ts +2 -1
- package/front_end/ui/legacy/SoftDropDown.ts +4 -2
- package/front_end/ui/legacy/SplitWidget.ts +2 -1
- package/front_end/ui/legacy/SuggestBox.ts +2 -1
- package/front_end/ui/legacy/TabbedPane.ts +2 -1
- package/front_end/ui/legacy/TargetCrashedScreen.ts +2 -1
- package/front_end/ui/legacy/TextPrompt.ts +2 -1
- package/front_end/ui/legacy/Toolbar.ts +3 -2
- package/front_end/ui/legacy/Treeoutline.ts +3 -2
- package/front_end/ui/legacy/UIUtils.ts +16 -13
- package/front_end/ui/legacy/ViewManager.ts +2 -1
- package/front_end/ui/legacy/components/quick_open/filteredListWidget.css +2 -2
- package/front_end/ui/legacy/components/source_frame/FontView.ts +1 -0
- package/front_end/ui/legacy/components/source_frame/ImageView.ts +1 -0
- package/front_end/ui/legacy/components/source_frame/JSONView.ts +1 -0
- package/front_end/ui/legacy/components/source_frame/ResourceSourceFrame.ts +1 -0
- package/front_end/ui/legacy/components/source_frame/XMLView.ts +2 -0
- package/front_end/ui/legacy/components/source_frame/module.json +0 -3
- package/front_end/ui/legacy/components/text_editor/CodeMirrorTextEditor.ts +2 -0
- package/front_end/ui/legacy/components/text_editor/module.json +0 -3
- package/front_end/ui/legacy/components/utils/Linkifier.ts +7 -15
- package/front_end/ui/legacy/theme_support/theme_support_impl.ts +7 -9
- package/front_end/ui/legacy/utils/create-shadow-root-with-core-styles.ts +2 -2
- package/front_end/ui/legacy/utils/inject-core-styles.ts +7 -4
- package/package.json +1 -1
- package/front_end/panels/emulation/module.json +0 -11
- package/front_end/third_party/lighthouse/report-assets/report.css +0 -1774
- package/front_end/ui/legacy/module.json +0 -41
|
@@ -38,6 +38,10 @@ const listOfTlds = [
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
class Util {
|
|
41
|
+
/** @type {I18n<typeof UIStrings>} */
|
|
42
|
+
// @ts-expect-error: Is set in report renderer.
|
|
43
|
+
static i18n = null;
|
|
44
|
+
|
|
41
45
|
static get PASS_THRESHOLD() {
|
|
42
46
|
return PASS_THRESHOLD;
|
|
43
47
|
}
|
|
@@ -103,6 +107,23 @@ class Util {
|
|
|
103
107
|
/** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
|
|
104
108
|
const relevantAuditToMetricsMap = new Map();
|
|
105
109
|
|
|
110
|
+
// This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
|
|
111
|
+
// Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
|
|
112
|
+
// Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
|
|
113
|
+
const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
|
|
114
|
+
const perfCategory = clone.categories['performance'];
|
|
115
|
+
if (majorVersion < 9 && perfCategory) {
|
|
116
|
+
if (!clone.categoryGroups) clone.categoryGroups = {};
|
|
117
|
+
clone.categoryGroups['hidden'] = {title: ''};
|
|
118
|
+
for (const auditRef of perfCategory.auditRefs) {
|
|
119
|
+
if (!auditRef.group) {
|
|
120
|
+
auditRef.group = 'hidden';
|
|
121
|
+
} else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
|
|
122
|
+
delete auditRef.group;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
for (const category of Object.values(clone.categories)) {
|
|
107
128
|
// Make basic lookup table for relevantAudits
|
|
108
129
|
category.auditRefs.forEach(metricRef => {
|
|
@@ -166,6 +187,8 @@ class Util {
|
|
|
166
187
|
|
|
167
188
|
/**
|
|
168
189
|
* Convert a score to a rating label.
|
|
190
|
+
* TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
|
|
191
|
+
*
|
|
169
192
|
* @param {number|null} score
|
|
170
193
|
* @param {string=} scoreDisplayMode
|
|
171
194
|
* @return {string}
|
|
@@ -386,43 +409,21 @@ class Util {
|
|
|
386
409
|
return hostname.split('.').slice(-splitTld.length).join('.');
|
|
387
410
|
}
|
|
388
411
|
|
|
389
|
-
/**
|
|
390
|
-
* @param {LH.Result['configSettings']} settings
|
|
391
|
-
* @return {!Array<{name: string, description: string}>}
|
|
392
|
-
*/
|
|
393
|
-
static getEnvironmentDisplayValues(settings) {
|
|
394
|
-
const emulationDesc = Util.getEmulationDescriptions(settings);
|
|
395
|
-
|
|
396
|
-
return [
|
|
397
|
-
{
|
|
398
|
-
name: Util.i18n.strings.runtimeSettingsDevice,
|
|
399
|
-
description: emulationDesc.deviceEmulation,
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
name: Util.i18n.strings.runtimeSettingsNetworkThrottling,
|
|
403
|
-
description: emulationDesc.networkThrottling,
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: Util.i18n.strings.runtimeSettingsCPUThrottling,
|
|
407
|
-
description: emulationDesc.cpuThrottling,
|
|
408
|
-
},
|
|
409
|
-
];
|
|
410
|
-
}
|
|
411
412
|
|
|
412
413
|
/**
|
|
413
414
|
* @param {LH.Result['configSettings']} settings
|
|
414
|
-
* @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}}
|
|
415
|
+
* @return {!{deviceEmulation: string, networkThrottling: string, cpuThrottling: string, summary: string}}
|
|
415
416
|
*/
|
|
416
417
|
static getEmulationDescriptions(settings) {
|
|
417
418
|
let cpuThrottling;
|
|
418
419
|
let networkThrottling;
|
|
420
|
+
let summary;
|
|
419
421
|
|
|
420
422
|
const throttling = settings.throttling;
|
|
421
423
|
|
|
422
424
|
switch (settings.throttlingMethod) {
|
|
423
425
|
case 'provided':
|
|
424
|
-
cpuThrottling = Util.i18n.strings.throttlingProvided;
|
|
425
|
-
networkThrottling = Util.i18n.strings.throttlingProvided;
|
|
426
|
+
summary = networkThrottling = cpuThrottling = Util.i18n.strings.throttlingProvided;
|
|
426
427
|
break;
|
|
427
428
|
case 'devtools': {
|
|
428
429
|
const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
|
|
@@ -430,6 +431,13 @@ class Util {
|
|
|
430
431
|
networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
|
|
431
432
|
`${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
|
|
432
433
|
`${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
|
|
434
|
+
|
|
435
|
+
const isSlow4G = () => {
|
|
436
|
+
return requestLatencyMs === 150 * 3.75 &&
|
|
437
|
+
throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
|
|
438
|
+
throttling.uploadThroughputKbps === 750 * 0.9;
|
|
439
|
+
};
|
|
440
|
+
summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
|
|
433
441
|
break;
|
|
434
442
|
}
|
|
435
443
|
case 'simulate': {
|
|
@@ -437,11 +445,15 @@ class Util {
|
|
|
437
445
|
cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
|
|
438
446
|
networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
|
|
439
447
|
`${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
|
|
448
|
+
|
|
449
|
+
const isSlow4G = () => {
|
|
450
|
+
return rttMs === 150 && throughputKbps === 1.6 * 1024;
|
|
451
|
+
};
|
|
452
|
+
summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
|
|
440
453
|
break;
|
|
441
454
|
}
|
|
442
455
|
default:
|
|
443
|
-
cpuThrottling = Util.i18n.strings.runtimeUnknown;
|
|
444
|
-
networkThrottling = Util.i18n.strings.runtimeUnknown;
|
|
456
|
+
summary = cpuThrottling = networkThrottling = Util.i18n.strings.runtimeUnknown;
|
|
445
457
|
}
|
|
446
458
|
|
|
447
459
|
// TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796
|
|
@@ -454,6 +466,7 @@ class Util {
|
|
|
454
466
|
deviceEmulation,
|
|
455
467
|
cpuThrottling,
|
|
456
468
|
networkThrottling,
|
|
469
|
+
summary,
|
|
457
470
|
};
|
|
458
471
|
}
|
|
459
472
|
|
|
@@ -505,6 +518,43 @@ class Util {
|
|
|
505
518
|
static isPluginCategory(categoryId) {
|
|
506
519
|
return categoryId.startsWith('lighthouse-plugin-');
|
|
507
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @param {LH.Result.GatherMode} gatherMode
|
|
524
|
+
*/
|
|
525
|
+
static shouldDisplayAsFraction(gatherMode) {
|
|
526
|
+
return gatherMode === 'timespan' || gatherMode === 'snapshot';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @param {LH.ReportResult.Category} category
|
|
531
|
+
*/
|
|
532
|
+
static calculateCategoryFraction(category) {
|
|
533
|
+
let numPassableAudits = 0;
|
|
534
|
+
let numPassed = 0;
|
|
535
|
+
let numInformative = 0;
|
|
536
|
+
let totalWeight = 0;
|
|
537
|
+
for (const auditRef of category.auditRefs) {
|
|
538
|
+
const auditPassed = Util.showAsPassed(auditRef.result);
|
|
539
|
+
|
|
540
|
+
// Don't count the audit if it's manual, N/A, or isn't displayed.
|
|
541
|
+
if (auditRef.group === 'hidden' ||
|
|
542
|
+
auditRef.result.scoreDisplayMode === 'manual' ||
|
|
543
|
+
auditRef.result.scoreDisplayMode === 'notApplicable') {
|
|
544
|
+
continue;
|
|
545
|
+
} else if (auditRef.result.scoreDisplayMode === 'informative') {
|
|
546
|
+
if (!auditPassed) {
|
|
547
|
+
++numInformative;
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
++numPassableAudits;
|
|
553
|
+
totalWeight += auditRef.weight;
|
|
554
|
+
if (auditPassed) numPassed++;
|
|
555
|
+
}
|
|
556
|
+
return {numPassed, numPassableAudits, numInformative, totalWeight};
|
|
557
|
+
}
|
|
508
558
|
}
|
|
509
559
|
|
|
510
560
|
/**
|
|
@@ -524,14 +574,10 @@ Util.getUniqueSuffix = (() => {
|
|
|
524
574
|
};
|
|
525
575
|
})();
|
|
526
576
|
|
|
527
|
-
/** @type {I18n<typeof Util['UIStrings']>} */
|
|
528
|
-
// @ts-expect-error: Is set in report renderer.
|
|
529
|
-
Util.i18n = null;
|
|
530
|
-
|
|
531
577
|
/**
|
|
532
578
|
* Report-renderer-specific strings.
|
|
533
579
|
*/
|
|
534
|
-
|
|
580
|
+
const UIStrings = {
|
|
535
581
|
/** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
|
|
536
582
|
varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
|
|
537
583
|
/** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
|
|
@@ -598,22 +644,12 @@ Util.UIStrings = {
|
|
|
598
644
|
/** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
|
|
599
645
|
dropdownDarkTheme: 'Toggle Dark Theme',
|
|
600
646
|
|
|
601
|
-
/** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */
|
|
602
|
-
runtimeSettingsTitle: 'Runtime Settings',
|
|
603
|
-
/** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */
|
|
604
|
-
runtimeSettingsUrl: 'URL',
|
|
605
|
-
/** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */
|
|
606
|
-
runtimeSettingsFetchTime: 'Fetch Time',
|
|
607
647
|
/** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
|
|
608
648
|
runtimeSettingsDevice: 'Device',
|
|
609
649
|
/** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
|
|
610
650
|
runtimeSettingsNetworkThrottling: 'Network throttling',
|
|
611
651
|
/** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
|
|
612
652
|
runtimeSettingsCPUThrottling: 'CPU throttling',
|
|
613
|
-
/** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */
|
|
614
|
-
runtimeSettingsChannel: 'Channel',
|
|
615
|
-
/** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */
|
|
616
|
-
runtimeSettingsUA: 'User agent (host)',
|
|
617
653
|
/** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
|
|
618
654
|
runtimeSettingsUANetwork: 'User agent (network)',
|
|
619
655
|
/** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
|
|
@@ -632,12 +668,29 @@ Util.UIStrings = {
|
|
|
632
668
|
runtimeDesktopEmulation: 'Emulated Desktop',
|
|
633
669
|
/** Descriptive explanation for a runtime setting that is set to an unknown value. */
|
|
634
670
|
runtimeUnknown: 'Unknown',
|
|
671
|
+
/** Descriptive label that this analysis run was from a single pageload of a browser (not a summary of hundreds of loads) */
|
|
672
|
+
runtimeSingleLoad: 'Single page load',
|
|
673
|
+
/** Descriptive label that this analysis only considers the initial load of the page, and no interaction beyond when the page had "fully loaded" */
|
|
674
|
+
runtimeAnalysisWindow: 'Initial page load',
|
|
675
|
+
/** Descriptive explanation that this analysis run was from a single pageload of a browser, whereas field data often summarizes hundreds+ of page loads */
|
|
676
|
+
runtimeSingleLoadTooltip: 'This data is taken from a single page load, as opposed to field data summarizing many sessions.', // eslint-disable-line max-len
|
|
635
677
|
|
|
636
678
|
/** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
|
|
637
679
|
throttlingProvided: 'Provided by environment',
|
|
680
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
681
|
+
show: 'Show',
|
|
682
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
683
|
+
hide: 'Hide',
|
|
684
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
685
|
+
expandView: 'Expand view',
|
|
686
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
687
|
+
collapseView: 'Collapse view',
|
|
688
|
+
/** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
|
|
689
|
+
runtimeSlow4g: 'Slow 4G throttling',
|
|
690
|
+
/** Label indicating that Lighthouse throttled the page using custom throttling settings. */
|
|
691
|
+
runtimeCustom: 'Custom throttling',
|
|
638
692
|
};
|
|
639
|
-
|
|
640
|
-
Util.UIStrings;
|
|
693
|
+
Util.UIStrings = UIStrings;
|
|
641
694
|
|
|
642
695
|
// auto-generated by build/build-report-components.js
|
|
643
696
|
|
|
@@ -650,9 +703,9 @@ Util.UIStrings;
|
|
|
650
703
|
* @param {DOM} dom
|
|
651
704
|
*/
|
|
652
705
|
function create3pFilterComponent(dom) {
|
|
653
|
-
const el0 = dom.
|
|
706
|
+
const el0 = dom.createFragment();
|
|
654
707
|
const el1 = dom.createElement('style');
|
|
655
|
-
el1.append('\n .lh-3p-filter {\n
|
|
708
|
+
el1.append('\n .lh-3p-filter {\n color: var(--color-gray-600);\n float: right;\n padding: 6px var(--stackpack-padding-horizontal);\n }\n .lh-3p-filter-label, .lh-3p-filter-input {\n vertical-align: middle;\n user-select: none;\n }\n .lh-3p-filter-input:disabled + .lh-3p-ui-string {\n text-decoration: line-through;\n }\n ');
|
|
656
709
|
el0.append(el1);
|
|
657
710
|
const el2 = dom.createElement('div', 'lh-3p-filter');
|
|
658
711
|
const el3 = dom.createElement('label', 'lh-3p-filter-label');
|
|
@@ -672,7 +725,7 @@ function create3pFilterComponent(dom) {
|
|
|
672
725
|
* @param {DOM} dom
|
|
673
726
|
*/
|
|
674
727
|
function createAuditComponent(dom) {
|
|
675
|
-
const el0 = dom.
|
|
728
|
+
const el0 = dom.createFragment();
|
|
676
729
|
const el1 = dom.createElement('div', 'lh-audit');
|
|
677
730
|
const el2 = dom.createElement('details', 'lh-expandable-details');
|
|
678
731
|
const el3 = dom.createElement('summary');
|
|
@@ -697,7 +750,7 @@ function createAuditComponent(dom) {
|
|
|
697
750
|
* @param {DOM} dom
|
|
698
751
|
*/
|
|
699
752
|
function createCategoryHeaderComponent(dom) {
|
|
700
|
-
const el0 = dom.
|
|
753
|
+
const el0 = dom.createFragment();
|
|
701
754
|
const el1 = dom.createElement('div', 'lh-category-header');
|
|
702
755
|
const el2 = dom.createElement('div', 'lh-score__gauge');
|
|
703
756
|
el2.setAttribute('role', 'heading');
|
|
@@ -712,7 +765,7 @@ function createCategoryHeaderComponent(dom) {
|
|
|
712
765
|
* @param {DOM} dom
|
|
713
766
|
*/
|
|
714
767
|
function createChevronComponent(dom) {
|
|
715
|
-
const el0 = dom.
|
|
768
|
+
const el0 = dom.createFragment();
|
|
716
769
|
const el1 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-chevron');
|
|
717
770
|
el1.setAttribute('viewBox', '0 0 100 100');
|
|
718
771
|
const el2 = dom.createElementNS('http://www.w3.org/2000/svg', 'g', 'lh-chevron__lines');
|
|
@@ -730,17 +783,23 @@ function createChevronComponent(dom) {
|
|
|
730
783
|
* @param {DOM} dom
|
|
731
784
|
*/
|
|
732
785
|
function createClumpComponent(dom) {
|
|
733
|
-
const el0 = dom.
|
|
734
|
-
const el1 = dom.createElement('
|
|
735
|
-
const el2 = dom.createElement('
|
|
736
|
-
const el3 = dom.createElement('
|
|
737
|
-
const el4 = dom.createElement('div', 'lh-audit-
|
|
738
|
-
const el5 = dom.createElement('
|
|
739
|
-
const el6 = dom.createElement('span', 'lh-audit-
|
|
740
|
-
|
|
786
|
+
const el0 = dom.createFragment();
|
|
787
|
+
const el1 = dom.createElement('div', 'lh-audit-group');
|
|
788
|
+
const el2 = dom.createElement('details', 'lh-clump');
|
|
789
|
+
const el3 = dom.createElement('summary');
|
|
790
|
+
const el4 = dom.createElement('div', 'lh-audit-group__summary');
|
|
791
|
+
const el5 = dom.createElement('div', 'lh-audit-group__header');
|
|
792
|
+
const el6 = dom.createElement('span', 'lh-audit-group__title');
|
|
793
|
+
const el7 = dom.createElement('span', 'lh-audit-group__itemcount');
|
|
794
|
+
el5.append(' ', el6, ' ', el7, ' ', ' ', ' ');
|
|
795
|
+
const el8 = dom.createElement('div', 'lh-clump-toggle');
|
|
796
|
+
const el9 = dom.createElement('span', 'lh-clump-toggletext--show');
|
|
797
|
+
const el10 = dom.createElement('span', 'lh-clump-toggletext--hide');
|
|
798
|
+
el8.append(' ', el9, ' ', el10, ' ');
|
|
799
|
+
el4.append(' ', el5, ' ', el8, ' ');
|
|
741
800
|
el3.append(' ', el4, ' ');
|
|
742
801
|
el2.append(' ', el3, ' ');
|
|
743
|
-
el1.append(' ', el2, ' ');
|
|
802
|
+
el1.append(' ', ' ', el2, ' ');
|
|
744
803
|
el0.append(el1);
|
|
745
804
|
return el0;
|
|
746
805
|
}
|
|
@@ -749,7 +808,7 @@ function createClumpComponent(dom) {
|
|
|
749
808
|
* @param {DOM} dom
|
|
750
809
|
*/
|
|
751
810
|
function createCrcComponent(dom) {
|
|
752
|
-
const el0 = dom.
|
|
811
|
+
const el0 = dom.createFragment();
|
|
753
812
|
const el1 = dom.createElement('div', 'lh-crc-container');
|
|
754
813
|
const el2 = dom.createElement('style');
|
|
755
814
|
el2.append('\n .lh-crc .lh-tree-marker {\n width: 12px;\n height: 26px;\n display: block;\n float: left;\n background-position: top left;\n }\n .lh-crc .lh-horiz-down {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><g fill="%23D8D8D8" fill-rule="evenodd"><path d="M16 12v2H-2v-2z"/><path d="M9 12v14H7V12z"/></g></svg>\');\n }\n .lh-crc .lh-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M16 12v2H0v-2z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-up-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v14H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-vert-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v27H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-vert {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v26H7z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-crc-tree {\n font-size: 14px;\n width: 100%;\n overflow-x: auto;\n }\n .lh-crc .lh-crc-node {\n height: 26px;\n line-height: 26px;\n white-space: nowrap;\n }\n .lh-crc .lh-crc-node__tree-value {\n margin-left: 10px;\n }\n .lh-crc .lh-crc-node__tree-value div {\n display: inline;\n }\n .lh-crc .lh-crc-node__chain-duration {\n font-weight: 700;\n }\n .lh-crc .lh-crc-initial-nav {\n color: #595959;\n font-style: italic;\n }\n .lh-crc__summary-value {\n margin-bottom: 10px;\n }\n ');
|
|
@@ -771,7 +830,7 @@ function createCrcComponent(dom) {
|
|
|
771
830
|
* @param {DOM} dom
|
|
772
831
|
*/
|
|
773
832
|
function createCrcChainComponent(dom) {
|
|
774
|
-
const el0 = dom.
|
|
833
|
+
const el0 = dom.createFragment();
|
|
775
834
|
const el1 = dom.createElement('div', 'lh-crc-node');
|
|
776
835
|
const el2 = dom.createElement('span', 'lh-crc-node__tree-marker');
|
|
777
836
|
const el3 = dom.createElement('span', 'lh-crc-node__tree-value');
|
|
@@ -784,7 +843,7 @@ function createCrcChainComponent(dom) {
|
|
|
784
843
|
* @param {DOM} dom
|
|
785
844
|
*/
|
|
786
845
|
function createElementScreenshotComponent(dom) {
|
|
787
|
-
const el0 = dom.
|
|
846
|
+
const el0 = dom.createFragment();
|
|
788
847
|
const el1 = dom.createElement('div', 'lh-element-screenshot');
|
|
789
848
|
const el2 = dom.createElement('div', 'lh-element-screenshot__content');
|
|
790
849
|
const el3 = dom.createElement('div', 'lh-element-screenshot__mask');
|
|
@@ -805,45 +864,28 @@ function createElementScreenshotComponent(dom) {
|
|
|
805
864
|
return el0;
|
|
806
865
|
}
|
|
807
866
|
|
|
808
|
-
/**
|
|
809
|
-
* @param {DOM} dom
|
|
810
|
-
*/
|
|
811
|
-
function createEnvItemComponent(dom) {
|
|
812
|
-
const el0 = dom.document().createDocumentFragment();
|
|
813
|
-
const el1 = dom.createElement('li', 'lh-env__item');
|
|
814
|
-
const el2 = dom.createElement('span', 'lh-env__name');
|
|
815
|
-
const el3 = dom.createElement('span', 'lh-env__description');
|
|
816
|
-
el1.append(' ', el2, ' ', el3, ' ');
|
|
817
|
-
el0.append(el1);
|
|
818
|
-
return el0;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
867
|
/**
|
|
822
868
|
* @param {DOM} dom
|
|
823
869
|
*/
|
|
824
870
|
function createFooterComponent(dom) {
|
|
825
|
-
const el0 = dom.
|
|
871
|
+
const el0 = dom.createFragment();
|
|
826
872
|
const el1 = dom.createElement('style');
|
|
827
|
-
el1.append('\n .lh-footer {\n padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);\n max-width: var(--report-width);\n margin: 0 auto;\n }\n .lh-footer .lh-generated {\n text-align: center;\n }\n
|
|
873
|
+
el1.append('\n .lh-footer {\n padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);\n max-width: var(--report-content-width);\n margin: 0 auto;\n }\n .lh-footer .lh-generated {\n text-align: center;\n }\n ');
|
|
828
874
|
el0.append(el1);
|
|
829
875
|
const el2 = dom.createElement('footer', 'lh-footer');
|
|
830
|
-
const el3 = dom.createElement('
|
|
831
|
-
|
|
832
|
-
el4.
|
|
833
|
-
const el5 = dom.createElement('
|
|
834
|
-
el5.append('
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
el7.
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
el9.setAttribute('rel', 'noopener');
|
|
844
|
-
el9.append('File an issue');
|
|
845
|
-
el6.append(' ', ' Generated by ', el7, ' ', el8, ' | ', el9, ' ');
|
|
846
|
-
el2.append(' ', ' ', el3, ' ', el6, ' ');
|
|
876
|
+
const el3 = dom.createElement('ul', 'lh-meta__items');
|
|
877
|
+
el3.append(' ');
|
|
878
|
+
const el4 = dom.createElement('div', 'lh-generated');
|
|
879
|
+
const el5 = dom.createElement('b');
|
|
880
|
+
el5.append('Lighthouse');
|
|
881
|
+
const el6 = dom.createElement('span', 'lh-footer__version');
|
|
882
|
+
const el7 = dom.createElement('a', 'lh-footer__version_issue');
|
|
883
|
+
el7.setAttribute('href', 'https://github.com/GoogleChrome/Lighthouse/issues');
|
|
884
|
+
el7.setAttribute('target', '_blank');
|
|
885
|
+
el7.setAttribute('rel', 'noopener');
|
|
886
|
+
el7.append('File an issue');
|
|
887
|
+
el4.append(' ', ' Generated by ', el5, ' ', el6, ' | ', el7, ' ');
|
|
888
|
+
el2.append(' ', el3, ' ', el4, ' ');
|
|
847
889
|
el0.append(el2);
|
|
848
890
|
return el0;
|
|
849
891
|
}
|
|
@@ -852,9 +894,8 @@ function createFooterComponent(dom) {
|
|
|
852
894
|
* @param {DOM} dom
|
|
853
895
|
*/
|
|
854
896
|
function createFractionComponent(dom) {
|
|
855
|
-
const el0 = dom.
|
|
897
|
+
const el0 = dom.createFragment();
|
|
856
898
|
const el1 = dom.createElement('a', 'lh-fraction__wrapper');
|
|
857
|
-
el1.setAttribute('href', '#');
|
|
858
899
|
const el2 = dom.createElement('div', 'lh-fraction__content-wrapper');
|
|
859
900
|
const el3 = dom.createElement('div', 'lh-fraction__content');
|
|
860
901
|
const el4 = dom.createElement('div', 'lh-fraction__background');
|
|
@@ -870,9 +911,8 @@ function createFractionComponent(dom) {
|
|
|
870
911
|
* @param {DOM} dom
|
|
871
912
|
*/
|
|
872
913
|
function createGaugeComponent(dom) {
|
|
873
|
-
const el0 = dom.
|
|
914
|
+
const el0 = dom.createFragment();
|
|
874
915
|
const el1 = dom.createElement('a', 'lh-gauge__wrapper');
|
|
875
|
-
el1.setAttribute('href', '#');
|
|
876
916
|
const el2 = dom.createElement('div', 'lh-gauge__svg-wrapper');
|
|
877
917
|
const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-gauge');
|
|
878
918
|
el3.setAttribute('viewBox', '0 0 120 120');
|
|
@@ -899,12 +939,11 @@ function createGaugeComponent(dom) {
|
|
|
899
939
|
* @param {DOM} dom
|
|
900
940
|
*/
|
|
901
941
|
function createGaugePwaComponent(dom) {
|
|
902
|
-
const el0 = dom.
|
|
942
|
+
const el0 = dom.createFragment();
|
|
903
943
|
const el1 = dom.createElement('style');
|
|
904
944
|
el1.append('\n .lh-gauge--pwa .lh-gauge--pwa__component {\n display: none;\n }\n .lh-gauge--pwa__wrapper:not(.lh-badged--all) .lh-gauge--pwa__logo > path {\n /* Gray logo unless everything is passing. */\n fill: #B0B0B0;\n }\n\n .lh-gauge--pwa__disc {\n fill: var(--color-gray-200);\n }\n\n .lh-gauge--pwa__logo--primary-color {\n fill: #304FFE;\n }\n\n .lh-gauge--pwa__logo--secondary-color {\n fill: #3D3D3D;\n }\n .lh-dark .lh-gauge--pwa__logo--secondary-color {\n fill: #D8B6B6;\n }\n\n /* No passing groups. */\n .lh-gauge--pwa__wrapper:not([class*=\'lh-badged--\']) .lh-gauge--pwa__na-line {\n display: inline;\n }\n /* Just optimized. Same n/a line as no passing groups. */\n .lh-gauge--pwa__wrapper.lh-badged--pwa-optimized:not(.lh-badged--pwa-installable) .lh-gauge--pwa__na-line {\n display: inline;\n }\n\n /* Just installable. */\n .lh-gauge--pwa__wrapper.lh-badged--pwa-installable .lh-gauge--pwa__installable-badge {\n display: inline;\n }\n\n /* All passing groups. */\n .lh-gauge--pwa__wrapper.lh-badged--all .lh-gauge--pwa__check-circle {\n display: inline;\n }\n ');
|
|
905
945
|
el0.append(el1);
|
|
906
946
|
const el2 = dom.createElement('a', 'lh-gauge__wrapper lh-gauge--pwa__wrapper');
|
|
907
|
-
el2.setAttribute('href', '#');
|
|
908
947
|
const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-gauge lh-gauge--pwa');
|
|
909
948
|
el3.setAttribute('viewBox', '0 0 60 60');
|
|
910
949
|
const el4 = dom.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
@@ -1000,7 +1039,7 @@ function createGaugePwaComponent(dom) {
|
|
|
1000
1039
|
* @param {DOM} dom
|
|
1001
1040
|
*/
|
|
1002
1041
|
function createHeadingComponent(dom) {
|
|
1003
|
-
const el0 = dom.
|
|
1042
|
+
const el0 = dom.createFragment();
|
|
1004
1043
|
const el1 = dom.createElement('style');
|
|
1005
1044
|
el1.append('\n /* CSS Fireworks. Originally by Eddie Lin\n https://codepen.io/paulirish/pen/yEVMbP\n */\n .lh-pyro {\n display: none;\n z-index: 1;\n pointer-events: none;\n }\n .lh-score100 .lh-pyro {\n display: block;\n }\n .lh-score100 .lh-lighthouse stop:first-child {\n stop-color: hsla(200, 12%, 95%, 0);\n }\n .lh-score100 .lh-lighthouse stop:last-child {\n stop-color: hsla(65, 81%, 76%, 1);\n }\n\n .lh-pyro > .lh-pyro-before, .lh-pyro > .lh-pyro-after {\n position: absolute;\n width: 5px;\n height: 5px;\n border-radius: 2.5px;\n box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;\n animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;\n animation-delay: 1s, 1s, 1s;\n }\n\n .lh-pyro > .lh-pyro-after {\n animation-delay: 2.25s, 2.25s, 2.25s;\n animation-duration: 1.25s, 1.25s, 6.25s;\n }\n .lh-fireworks-paused .lh-pyro > div {\n animation-play-state: paused;\n }\n\n @keyframes bang {\n to {\n box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;\n }\n }\n @keyframes gravity {\n to {\n transform: translateY(80px);\n opacity: 0;\n }\n }\n @keyframes position {\n 0%, 19.9% {\n margin-top: 4%;\n margin-left: 47%;\n }\n 20%, 39.9% {\n margin-top: 7%;\n margin-left: 30%;\n }\n 40%, 59.9% {\n margin-top: 6%;\n margin-left: 70%;\n }\n 60%, 79.9% {\n margin-top: 3%;\n margin-left: 20%;\n }\n 80%, 99.9% {\n margin-top: 3%;\n margin-left: 80%;\n }\n }\n ');
|
|
1006
1045
|
el0.append(el1);
|
|
@@ -1015,7 +1054,7 @@ function createHeadingComponent(dom) {
|
|
|
1015
1054
|
* @param {DOM} dom
|
|
1016
1055
|
*/
|
|
1017
1056
|
function createMetricComponent(dom) {
|
|
1018
|
-
const el0 = dom.
|
|
1057
|
+
const el0 = dom.createFragment();
|
|
1019
1058
|
const el1 = dom.createElement('div', 'lh-metric');
|
|
1020
1059
|
const el2 = dom.createElement('div', 'lh-metric__innerwrap');
|
|
1021
1060
|
const el3 = dom.createElement('div', 'lh-metric__icon');
|
|
@@ -1028,53 +1067,15 @@ function createMetricComponent(dom) {
|
|
|
1028
1067
|
return el0;
|
|
1029
1068
|
}
|
|
1030
1069
|
|
|
1031
|
-
/**
|
|
1032
|
-
* @param {DOM} dom
|
|
1033
|
-
*/
|
|
1034
|
-
function createMetricsToggleComponent(dom) {
|
|
1035
|
-
const el0 = dom.document().createDocumentFragment();
|
|
1036
|
-
const el1 = dom.createElement('div', 'lh-metrics-toggle');
|
|
1037
|
-
const el2 = dom.createElement('input', 'lh-metrics-toggle__input');
|
|
1038
|
-
el2.setAttribute('type', 'checkbox');
|
|
1039
|
-
el2.setAttribute('id', 'toggle-metric-descriptions');
|
|
1040
|
-
el2.setAttribute('aria-label', 'Toggle the display of metric descriptions');
|
|
1041
|
-
const el3 = dom.createElement('label', 'lh-metrics-toggle__label');
|
|
1042
|
-
el3.setAttribute('for', 'toggle-metric-descriptions');
|
|
1043
|
-
const el4 = dom.createElement('div', 'lh-metrics-toggle__icon lh-metrics-toggle__icon--less');
|
|
1044
|
-
el4.setAttribute('aria-hidden', 'true');
|
|
1045
|
-
const el5 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1046
|
-
el5.setAttribute('width', '24');
|
|
1047
|
-
el5.setAttribute('height', '24');
|
|
1048
|
-
el5.setAttribute('viewBox', '0 0 24 24');
|
|
1049
|
-
const el6 = dom.createElementNS('http://www.w3.org/2000/svg', 'path', 'lh-metrics-toggle__lines');
|
|
1050
|
-
el6.setAttribute('d', 'M4 9h16v2H4zm0 4h10v2H4z');
|
|
1051
|
-
el5.append(' ', el6, ' ');
|
|
1052
|
-
el4.append(' ', el5, ' ');
|
|
1053
|
-
const el7 = dom.createElement('div', 'lh-metrics-toggle__icon lh-metrics-toggle__icon--more');
|
|
1054
|
-
el7.setAttribute('aria-hidden', 'true');
|
|
1055
|
-
const el8 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1056
|
-
el8.setAttribute('width', '24');
|
|
1057
|
-
el8.setAttribute('height', '24');
|
|
1058
|
-
el8.setAttribute('viewBox', '0 0 24 24');
|
|
1059
|
-
const el9 = dom.createElementNS('http://www.w3.org/2000/svg', 'path', 'lh-metrics-toggle__lines');
|
|
1060
|
-
el9.setAttribute('d', 'M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z');
|
|
1061
|
-
el8.append(' ', el9, ' ');
|
|
1062
|
-
el7.append(' ', el8, ' ');
|
|
1063
|
-
el3.append(' ', el4, ' ', el7, ' ');
|
|
1064
|
-
el1.append(' ', el2, ' ', el3, ' ');
|
|
1065
|
-
el0.append(el1);
|
|
1066
|
-
return el0;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
1070
|
/**
|
|
1070
1071
|
* @param {DOM} dom
|
|
1071
1072
|
*/
|
|
1072
1073
|
function createOpportunityComponent(dom) {
|
|
1073
|
-
const el0 = dom.
|
|
1074
|
+
const el0 = dom.createFragment();
|
|
1074
1075
|
const el1 = dom.createElement('div', 'lh-audit lh-audit--load-opportunity');
|
|
1075
1076
|
const el2 = dom.createElement('details', 'lh-expandable-details');
|
|
1076
1077
|
const el3 = dom.createElement('summary');
|
|
1077
|
-
const el4 = dom.createElement('div', 'lh-audit__header
|
|
1078
|
+
const el4 = dom.createElement('div', 'lh-audit__header');
|
|
1078
1079
|
const el5 = dom.createElement('div', 'lh-load-opportunity__cols');
|
|
1079
1080
|
const el6 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--one');
|
|
1080
1081
|
const el7 = dom.createElement('span', 'lh-audit__score-icon');
|
|
@@ -1104,7 +1105,7 @@ function createOpportunityComponent(dom) {
|
|
|
1104
1105
|
* @param {DOM} dom
|
|
1105
1106
|
*/
|
|
1106
1107
|
function createOpportunityHeaderComponent(dom) {
|
|
1107
|
-
const el0 = dom.
|
|
1108
|
+
const el0 = dom.createFragment();
|
|
1108
1109
|
const el1 = dom.createElement('div', 'lh-load-opportunity__header lh-load-opportunity__cols');
|
|
1109
1110
|
const el2 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--one');
|
|
1110
1111
|
const el3 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--two');
|
|
@@ -1117,7 +1118,7 @@ function createOpportunityHeaderComponent(dom) {
|
|
|
1117
1118
|
* @param {DOM} dom
|
|
1118
1119
|
*/
|
|
1119
1120
|
function createScorescaleComponent(dom) {
|
|
1120
|
-
const el0 = dom.
|
|
1121
|
+
const el0 = dom.createFragment();
|
|
1121
1122
|
const el1 = dom.createElement('div', 'lh-scorescale');
|
|
1122
1123
|
const el2 = dom.createElement('span', 'lh-scorescale-range lh-scorescale-range--fail');
|
|
1123
1124
|
el2.append('0–49');
|
|
@@ -1134,9 +1135,9 @@ function createScorescaleComponent(dom) {
|
|
|
1134
1135
|
* @param {DOM} dom
|
|
1135
1136
|
*/
|
|
1136
1137
|
function createScoresWrapperComponent(dom) {
|
|
1137
|
-
const el0 = dom.
|
|
1138
|
+
const el0 = dom.createFragment();
|
|
1138
1139
|
const el1 = dom.createElement('style');
|
|
1139
|
-
el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--
|
|
1140
|
+
el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--default-padding) 0;\n position: relative;\n width: 100%;\n }\n\n .lh-sticky-header {\n --gauge-circle-size: var(--gauge-circle-size-sm);\n --plugin-badge-size: 16px;\n --plugin-icon-size: 75%;\n --gauge-wrapper-width: 60px;\n --gauge-percentage-font-size: 13px;\n position: fixed;\n left: 0;\n right: 0;\n top: var(--topbar-height);\n font-weight: 500;\n display: none;\n justify-content: center;\n background-color: var(--sticky-header-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n padding-top: var(--score-container-padding);\n padding-bottom: 4px;\n z-index: 1;\n pointer-events: none;\n }\n\n .lh-devtools .lh-sticky-header {\n /* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */\n position: sticky;\n }\n\n .lh-sticky-header--visible {\n display: grid;\n grid-auto-flow: column;\n pointer-events: auto;\n }\n\n /* Disable the gauge arc animation for the sticky header, so toggling display: none\n does not play the animation. */\n .lh-sticky-header .lh-gauge-arc {\n animation: none;\n }\n\n .lh-sticky-header .lh-gauge__label {\n display: none;\n }\n\n .lh-highlighter {\n width: var(--gauge-wrapper-width);\n height: 1px;\n background-color: var(--highlighter-background-color);\n /* Position at bottom of first gauge in sticky header. */\n position: absolute;\n grid-column: 1;\n bottom: -1px;\n }\n\n .lh-gauge__wrapper:first-of-type {\n contain: none;\n }\n ');
|
|
1140
1141
|
el0.append(el1);
|
|
1141
1142
|
const el2 = dom.createElement('div', 'lh-scores-wrapper');
|
|
1142
1143
|
const el3 = dom.createElement('div', 'lh-scores-container');
|
|
@@ -1154,7 +1155,7 @@ function createScoresWrapperComponent(dom) {
|
|
|
1154
1155
|
* @param {DOM} dom
|
|
1155
1156
|
*/
|
|
1156
1157
|
function createSnippetComponent(dom) {
|
|
1157
|
-
const el0 = dom.
|
|
1158
|
+
const el0 = dom.createFragment();
|
|
1158
1159
|
const el1 = dom.createElement('div', 'lh-snippet');
|
|
1159
1160
|
const el2 = dom.createElement('style');
|
|
1160
1161
|
el2.append('\n :root {\n --snippet-highlight-light: #fbf1f2;\n --snippet-highlight-dark: #ffd6d8;\n }\n\n .lh-snippet__header {\n position: relative;\n overflow: hidden;\n padding: 10px;\n border-bottom: none;\n color: var(--snippet-color);\n background-color: var(--snippet-background-color);\n border: 1px solid var(--report-border-color-secondary);\n }\n .lh-snippet__title {\n font-weight: bold;\n float: left;\n }\n .lh-snippet__node {\n float: left;\n margin-left: 4px;\n }\n .lh-snippet__toggle-expand {\n padding: 1px 7px;\n margin-top: -1px;\n margin-right: -7px;\n float: right;\n background: transparent;\n border: none;\n cursor: pointer;\n font-size: 14px;\n color: #0c50c7;\n }\n\n .lh-snippet__snippet {\n overflow: auto;\n border: 1px solid var(--report-border-color-secondary);\n }\n /* Container needed so that all children grow to the width of the scroll container */\n .lh-snippet__snippet-inner {\n display: inline-block;\n min-width: 100%;\n }\n\n .lh-snippet:not(.lh-snippet--expanded) .lh-snippet__show-if-expanded {\n display: none;\n }\n .lh-snippet.lh-snippet--expanded .lh-snippet__show-if-collapsed {\n display: none;\n }\n\n .lh-snippet__line {\n background: white;\n white-space: pre;\n display: flex;\n }\n .lh-snippet__line:not(.lh-snippet__line--message):first-child {\n padding-top: 4px;\n }\n .lh-snippet__line:not(.lh-snippet__line--message):last-child {\n padding-bottom: 4px;\n }\n .lh-snippet__line--content-highlighted {\n background: var(--snippet-highlight-dark);\n }\n .lh-snippet__line--message {\n background: var(--snippet-highlight-light);\n }\n .lh-snippet__line--message .lh-snippet__line-number {\n padding-top: 10px;\n padding-bottom: 10px;\n }\n .lh-snippet__line--message code {\n padding: 10px;\n padding-left: 5px;\n color: var(--color-fail);\n font-family: var(--report-font-family);\n }\n .lh-snippet__line--message code {\n white-space: normal;\n }\n .lh-snippet__line-icon {\n padding-top: 10px;\n display: none;\n }\n .lh-snippet__line--message .lh-snippet__line-icon {\n display: block;\n }\n .lh-snippet__line-icon:before {\n content: "";\n display: inline-block;\n vertical-align: middle;\n margin-right: 4px;\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n background-image: var(--fail-icon-url);\n }\n .lh-snippet__line-number {\n flex-shrink: 0;\n width: 40px;\n text-align: right;\n font-family: monospace;\n padding-right: 5px;\n margin-right: 5px;\n color: var(--color-gray-600);\n user-select: none;\n }\n ');
|
|
@@ -1167,7 +1168,7 @@ function createSnippetComponent(dom) {
|
|
|
1167
1168
|
* @param {DOM} dom
|
|
1168
1169
|
*/
|
|
1169
1170
|
function createSnippetContentComponent(dom) {
|
|
1170
|
-
const el0 = dom.
|
|
1171
|
+
const el0 = dom.createFragment();
|
|
1171
1172
|
const el1 = dom.createElement('div', 'lh-snippet__snippet');
|
|
1172
1173
|
const el2 = dom.createElement('div', 'lh-snippet__snippet-inner');
|
|
1173
1174
|
el1.append(' ', el2, ' ');
|
|
@@ -1179,7 +1180,7 @@ function createSnippetContentComponent(dom) {
|
|
|
1179
1180
|
* @param {DOM} dom
|
|
1180
1181
|
*/
|
|
1181
1182
|
function createSnippetHeaderComponent(dom) {
|
|
1182
|
-
const el0 = dom.
|
|
1183
|
+
const el0 = dom.createFragment();
|
|
1183
1184
|
const el1 = dom.createElement('div', 'lh-snippet__header');
|
|
1184
1185
|
const el2 = dom.createElement('div', 'lh-snippet__title');
|
|
1185
1186
|
const el3 = dom.createElement('div', 'lh-snippet__node');
|
|
@@ -1196,7 +1197,7 @@ function createSnippetHeaderComponent(dom) {
|
|
|
1196
1197
|
* @param {DOM} dom
|
|
1197
1198
|
*/
|
|
1198
1199
|
function createSnippetLineComponent(dom) {
|
|
1199
|
-
const el0 = dom.
|
|
1200
|
+
const el0 = dom.createFragment();
|
|
1200
1201
|
const el1 = dom.createElement('div', 'lh-snippet__line');
|
|
1201
1202
|
const el2 = dom.createElement('div', 'lh-snippet__line-number');
|
|
1202
1203
|
const el3 = dom.createElement('div', 'lh-snippet__line-icon');
|
|
@@ -1206,13 +1207,24 @@ function createSnippetLineComponent(dom) {
|
|
|
1206
1207
|
return el0;
|
|
1207
1208
|
}
|
|
1208
1209
|
|
|
1210
|
+
/**
|
|
1211
|
+
* @param {DOM} dom
|
|
1212
|
+
*/
|
|
1213
|
+
function createStylesComponent(dom) {
|
|
1214
|
+
const el0 = dom.createFragment();
|
|
1215
|
+
const el1 = dom.createElement('style');
|
|
1216
|
+
el1.append('/**\n * @license\n * Copyright 2017 The Lighthouse Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS-IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n Naming convention:\n\n If a variable is used for a specific component: --{component}-{property name}-{modifier}\n\n Both {component} and {property name} should be kebab-case. If the target is the entire page,\n use \'report\' for the component. The property name should not be abbreviated. Use the\n property name the variable is intended for - if it\'s used for multiple, a common descriptor\n is fine (ex: \'size\' for a variable applied to \'width\' and \'height\'). If a variable is shared\n across multiple components, either create more variables or just drop the "{component}-"\n part of the name. Append any modifiers at the end (ex: \'big\', \'dark\').\n\n For colors: --color-{hue}-{intensity}\n\n {intensity} is the Material Design tag - 700, A700, etc.\n*/\n.lh-vars {\n /* Palette using Material Design Colors\n * https://www.materialui.co/colors */\n --color-amber-50: #FFF8E1;\n --color-blue-200: #90CAF9;\n --color-blue-900: #0D47A1;\n --color-blue-A700: #2962FF;\n --color-blue-primary: #06f;\n --color-cyan-500: #00BCD4;\n --color-gray-100: #F5F5F5;\n --color-gray-300: #CFCFCF;\n --color-gray-200: #E0E0E0;\n --color-gray-400: #BDBDBD;\n --color-gray-50: #FAFAFA;\n --color-gray-500: #9E9E9E;\n --color-gray-600: #757575;\n --color-gray-700: #616161;\n --color-gray-800: #424242;\n --color-gray-900: #212121;\n --color-gray: #000000;\n --color-green-700: #080;\n --color-green: #0c6;\n --color-lime-400: #D3E156;\n --color-orange-50: #FFF3E0;\n --color-orange-700: #C33300;\n --color-orange: #fa3;\n --color-red-700: #c00;\n --color-red: #f33;\n --color-teal-600: #00897B;\n --color-white: #FFFFFF;\n\n /* Context-specific colors */\n --color-average-secondary: var(--color-orange-700);\n --color-average: var(--color-orange);\n --color-fail-secondary: var(--color-red-700);\n --color-fail: var(--color-red);\n --color-hover: var(--color-gray-50);\n --color-informative: var(--color-blue-900);\n --color-pass-secondary: var(--color-green-700);\n --color-pass: var(--color-green);\n --color-not-applicable: var(--color-gray-600);\n\n /* Component variables */\n --audit-description-padding-left: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right));\n --audit-explanation-line-height: 16px;\n --audit-group-margin-bottom: calc(var(--default-padding) * 6);\n --audit-group-padding-vertical: 8px;\n --audit-margin-horizontal: 5px;\n --audit-padding-vertical: 8px;\n --category-padding: calc(var(--default-padding) * 6) calc(var(--default-padding) * 4) calc(var(--default-padding) * 4);\n --chevron-line-stroke: var(--color-gray-600);\n --chevron-size: 12px;\n --default-padding: 8px;\n --env-item-background-color: var(--color-gray-100);\n --env-item-font-size: 28px;\n --env-item-line-height: 36px;\n --env-item-padding: 10px 0px;\n --env-name-min-width: 220px;\n --footer-padding-vertical: 16px;\n --gauge-circle-size-big: 96px;\n --gauge-circle-size: 48px;\n --gauge-circle-size-sm: 32px;\n --gauge-label-font-size-big: 18px;\n --gauge-label-font-size: var(--report-font-size-secondary);\n --gauge-label-line-height-big: 24px;\n --gauge-label-line-height: var(--report-line-height-secondary);\n --gauge-percentage-font-size-big: 38px;\n --gauge-percentage-font-size: var(--report-font-size-secondary);\n --gauge-wrapper-width: 120px;\n --header-line-height: 24px;\n --highlighter-background-color: var(--report-text-color);\n --icon-square-size: calc(var(--score-icon-size) * 0.88);\n --image-preview-size: 48px;\n --link-color: var(--color-blue-primary);\n --locale-selector-background-color: var(--color-white);\n --metric-toggle-lines-fill: #7F7F7F;\n --metric-value-font-size: calc(var(--report-font-size) * 1.8);\n --metrics-toggle-background-color: var(--color-gray-200);\n --plugin-badge-background-color: var(--color-white);\n --plugin-badge-size-big: calc(var(--gauge-circle-size-big) / 2.7);\n --plugin-badge-size: calc(var(--gauge-circle-size) / 2.7);\n --plugin-icon-size: 65%;\n --pwa-icon-margin: 0 var(--default-padding);\n --pwa-icon-size: var(--topbar-logo-size);\n --report-background-color: #fff;\n --report-border-color-secondary: #ebebeb;\n --report-font-family-monospace: \'Roboto Mono\', \'Menlo\', \'dejavu sans mono\', \'Consolas\', \'Lucida Console\', monospace;\n --report-font-family: Roboto, Helvetica, Arial, sans-serif;\n --report-font-size: 14px;\n --report-font-size-secondary: 12px;\n --report-icon-size: var(--score-icon-background-size);\n --report-line-height: 24px;\n --report-line-height-secondary: 20px;\n --report-min-width: 360px;\n --report-monospace-font-size: calc(var(--report-font-size) * 0.85);\n --report-text-color-secondary: var(--color-gray-800);\n --report-text-color: var(--color-gray-900);\n --report-content-width: calc(60 * var(--report-font-size)); /* defaults to 840px */\n --score-container-padding: 8px;\n --score-icon-background-size: 24px;\n --score-icon-margin-left: 6px;\n --score-icon-margin-right: 14px;\n --score-icon-margin: 0 var(--score-icon-margin-right) 0 var(--score-icon-margin-left);\n --score-icon-size: 12px;\n --score-icon-size-big: 16px;\n --screenshot-overlay-background: rgba(0, 0, 0, 0.3);\n --section-padding-vertical: calc(var(--default-padding) * 6);\n --snippet-background-color: var(--color-gray-50);\n --snippet-color: #0938C2;\n --sparkline-height: 5px;\n --stackpack-padding-horizontal: 10px;\n --sticky-header-background-color: var(--report-background-color);\n --table-higlight-background-color: hsla(210, 17%, 77%, 0.1);\n --tools-icon-color: var(--color-gray-600);\n --topbar-background-color: var(--color-white);\n --topbar-height: 32px;\n --topbar-logo-size: 24px;\n --topbar-padding: 0 8px;\n --toplevel-warning-background-color: hsla(30, 100%, 75%, 10%);\n --toplevel-warning-message-text-color: var(--color-average-secondary);\n --toplevel-warning-padding: 18px;\n --toplevel-warning-text-color: var(--report-text-color);\n\n /* SVGs */\n --plugin-icon-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>\');\n --plugin-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23757575"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>\');\n\n --pass-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>check</title><path fill="%23178239" d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>\');\n --average-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>info</title><path fill="%23E67700" d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>\');\n --fail-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>warn</title><path fill="%23C7221F" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>\');\n\n --pwa-installable-gray-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23DAE0E3" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-gray-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23DAE0E3" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>\');\n\n --pwa-installable-gray-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23424242" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-gray-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23424242" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>\');\n\n --pwa-installable-color-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%230CCE6B" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-color-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%230CCE6B" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>\');\n\n --swap-locale-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>\');\n}\n\n@media not print {\n .lh-dark {\n /* Pallete */\n --color-gray-200: var(--color-gray-800);\n --color-gray-300: #616161;\n --color-gray-400: var(--color-gray-600);\n --color-gray-700: var(--color-gray-400);\n --color-gray-50: #757575;\n --color-gray-600: var(--color-gray-500);\n --color-green-700: var(--color-green);\n --color-orange-700: var(--color-orange);\n --color-red-700: var(--color-red);\n --color-teal-600: var(--color-cyan-500);\n\n /* Context-specific colors */\n --color-hover: rgba(0, 0, 0, 0.2);\n --color-informative: var(--color-blue-200);\n\n /* Component variables */\n --env-item-background-color: #393535;\n --link-color: var(--color-blue-200);\n --locale-selector-background-color: var(--color-gray-200);\n --plugin-badge-background-color: var(--color-gray-800);\n --report-background-color: var(--color-gray-900);\n --report-border-color-secondary: var(--color-gray-200);\n --report-text-color-secondary: var(--color-gray-400);\n --report-text-color: var(--color-gray-100);\n --snippet-color: var(--color-cyan-500);\n --topbar-background-color: var(--color-gray);\n --toplevel-warning-background-color: hsl(33deg 14% 18%);\n --toplevel-warning-message-text-color: var(--color-orange-700);\n --toplevel-warning-text-color: var(--color-gray-100);\n\n /* SVGs */\n --plugin-icon-url: var(--plugin-icon-url-dark);\n --pwa-installable-gray-url: var(--pwa-installable-gray-url-dark);\n --pwa-optimized-gray-url: var(--pwa-optimized-gray-url-dark);\n }\n}\n\n@media only screen and (max-width: 480px) {\n .lh-vars {\n --audit-group-margin-bottom: 20px;\n --category-padding: 12px;\n --env-name-min-width: 120px;\n --gauge-circle-size-big: 96px;\n --gauge-circle-size: 72px;\n --gauge-label-font-size-big: 22px;\n --gauge-label-font-size: 14px;\n --gauge-label-line-height-big: 26px;\n --gauge-label-line-height: 20px;\n --gauge-percentage-font-size-big: 34px;\n --gauge-percentage-font-size: 26px;\n --gauge-wrapper-width: 112px;\n --header-padding: 16px 0 16px 0;\n --image-preview-size: 24px;\n --plugin-icon-size: 75%;\n --pwa-icon-margin: 0 7px 0 -3px;\n --report-font-size: 14px;\n --report-line-height: 20px;\n --score-icon-margin-left: 2px;\n --score-icon-size: 10px;\n --topbar-height: 28px;\n --topbar-logo-size: 20px;\n }\n\n /* Not enough space to adequately show the relative savings bars. */\n .lh-sparkline {\n display: none;\n }\n}\n\n.lh-vars.lh-devtools {\n --audit-explanation-line-height: 14px;\n --audit-group-margin-bottom: 20px;\n --audit-group-padding-vertical: 12px;\n --audit-padding-vertical: 4px;\n --category-padding: 12px;\n --default-padding: 12px;\n --env-name-min-width: 120px;\n --footer-padding-vertical: 8px;\n --gauge-circle-size-big: 72px;\n --gauge-circle-size: 64px;\n --gauge-label-font-size-big: 22px;\n --gauge-label-font-size: 14px;\n --gauge-label-line-height-big: 26px;\n --gauge-label-line-height: 20px;\n --gauge-percentage-font-size-big: 34px;\n --gauge-percentage-font-size: 26px;\n --gauge-wrapper-width: 97px;\n --header-line-height: 20px;\n --header-padding: 16px 0 16px 0;\n --screenshot-overlay-background: transparent;\n --plugin-icon-size: 75%;\n --pwa-icon-margin: 0 7px 0 -3px;\n --report-font-family-monospace: \'Menlo\', \'dejavu sans mono\', \'Consolas\', \'Lucida Console\', monospace;\n --report-font-family: \'.SFNSDisplay-Regular\', \'Helvetica Neue\', \'Lucida Grande\', sans-serif;\n --report-font-size: 12px;\n --report-line-height: 20px;\n --score-icon-margin-left: 2px;\n --score-icon-size: 10px;\n --section-padding-vertical: 8px;\n}\n\n.lh-devtools.lh-root {\n height: 100%;\n}\n.lh-devtools.lh-root img {\n /* Override devtools default \'min-width: 0\' so svg without size in a flexbox isn\'t collapsed. */\n min-width: auto;\n}\n.lh-devtools .lh-container {\n overflow-y: scroll;\n height: calc(100% - var(--topbar-height));\n}\n@media print {\n .lh-devtools .lh-container {\n overflow: unset;\n }\n}\n.lh-devtools .lh-sticky-header {\n /* This is normally the height of the topbar, but we want it to stick to the top of our scroll container .lh-container` */\n top: 0;\n}\n\n@keyframes fadeIn {\n 0% { opacity: 0;}\n 100% { opacity: 0.6;}\n}\n\n.lh-root *, .lh-root *::before, .lh-root *::after {\n box-sizing: border-box;\n}\n\n.lh-root {\n font-family: var(--report-font-family);\n font-size: var(--report-font-size);\n margin: 0;\n line-height: var(--report-line-height);\n background: var(--report-background-color);\n color: var(--report-text-color);\n}\n\n.lh-root :focus {\n outline: -webkit-focus-ring-color auto 3px;\n}\n.lh-root summary:focus {\n outline: none;\n box-shadow: 0 0 0 1px hsl(217, 89%, 61%);\n}\n\n.lh-root [hidden] {\n display: none !important;\n}\n\n.lh-root pre {\n margin: 0;\n}\n\n.lh-root details > summary {\n cursor: pointer;\n}\n\n.lh-hidden {\n display: none !important;\n}\n\n.lh-container {\n /*\n Text wrapping in the report is so much FUN!\n We have a `word-break: break-word;` globally here to prevent a few common scenarios, namely\n long non-breakable text (usually URLs) found in:\n 1. The footer\n 2. .lh-node (outerHTML)\n 3. .lh-code\n\n With that sorted, the next challenge is appropriate column sizing and text wrapping inside our\n .lh-details tables. Even more fun.\n * We don\'t want table headers ("Potential Savings (ms)") to wrap or their column values, but\n we\'d be happy for the URL column to wrap if the URLs are particularly long.\n * We want the narrow columns to remain narrow, providing the most column width for URL\n * We don\'t want the table to extend past 100% width.\n * Long URLs in the URL column can wrap. Util.getURLDisplayName maxes them out at 64 characters,\n but they do not get any overflow:ellipsis treatment.\n */\n word-break: break-word;\n}\n\n.lh-audit-group a,\n.lh-category-header__description a,\n.lh-audit__description a,\n.lh-warnings a,\n.lh-footer a,\n.lh-table-column--link a {\n color: var(--link-color);\n}\n\n.lh-audit__description, .lh-audit__stackpack {\n --inner-audit-padding-right: var(--stackpack-padding-horizontal);\n padding-left: var(--audit-description-padding-left);\n padding-right: var(--inner-audit-padding-right);\n padding-top: 8px;\n padding-bottom: 8px;\n}\n\n.lh-details {\n margin-top: var(--default-padding);\n margin-bottom: var(--default-padding);\n margin-left: var(--audit-description-padding-left);\n /* whatever the .lh-details side margins are */\n width: 100%;\n}\n\n.lh-audit__stackpack {\n display: flex;\n align-items: center;\n}\n\n.lh-audit__stackpack__img {\n max-width: 30px;\n margin-right: var(--default-padding)\n}\n\n/* Report header */\n\n.lh-report-icon {\n display: flex;\n align-items: center;\n padding: 10px 12px;\n cursor: pointer;\n}\n.lh-report-icon[disabled] {\n opacity: 0.3;\n pointer-events: none;\n}\n\n.lh-report-icon::before {\n content: "";\n margin: 4px;\n background-repeat: no-repeat;\n width: var(--report-icon-size);\n height: var(--report-icon-size);\n opacity: 0.7;\n display: inline-block;\n vertical-align: middle;\n}\n.lh-report-icon:hover::before {\n opacity: 1;\n}\n.lh-dark .lh-report-icon::before {\n filter: invert(1);\n}\n.lh-report-icon--print::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\');\n}\n.lh-report-icon--copy::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>\');\n}\n.lh-report-icon--open::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>\');\n}\n.lh-report-icon--download::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\');\n}\n.lh-report-icon--dark::before {\n background-image:url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 100 125"><path d="M50 23.587c-16.27 0-22.799 12.574-22.799 21.417 0 12.917 10.117 22.451 12.436 32.471h20.726c2.32-10.02 12.436-19.554 12.436-32.471 0-8.843-6.528-21.417-22.799-21.417zM39.637 87.161c0 3.001 1.18 4.181 4.181 4.181h.426l.41 1.231C45.278 94.449 46.042 95 48.019 95h3.963c1.978 0 2.74-.551 3.365-2.427l.409-1.231h.427c3.002 0 4.18-1.18 4.18-4.181V80.91H39.637v6.251zM50 18.265c1.26 0 2.072-.814 2.072-2.073v-9.12C52.072 5.813 51.26 5 50 5c-1.259 0-2.072.813-2.072 2.073v9.12c0 1.259.813 2.072 2.072 2.072zM68.313 23.727c.994.774 2.135.634 2.91-.357l5.614-7.187c.776-.992.636-2.135-.356-2.909-.992-.776-2.135-.636-2.91.357l-5.613 7.186c-.778.993-.636 2.135.355 2.91zM91.157 36.373c-.306-1.222-1.291-1.815-2.513-1.51l-8.85 2.207c-1.222.305-1.814 1.29-1.51 2.512.305 1.223 1.291 1.814 2.513 1.51l8.849-2.206c1.223-.305 1.816-1.291 1.511-2.513zM86.757 60.48l-8.331-3.709c-1.15-.512-2.225-.099-2.736 1.052-.512 1.151-.1 2.224 1.051 2.737l8.33 3.707c1.15.514 2.225.101 2.736-1.05.513-1.149.1-2.223-1.05-2.737zM28.779 23.37c.775.992 1.917 1.131 2.909.357.992-.776 1.132-1.917.357-2.91l-5.615-7.186c-.775-.992-1.917-1.132-2.909-.357s-1.131 1.917-.356 2.909l5.614 7.187zM21.715 39.583c.305-1.223-.288-2.208-1.51-2.513l-8.849-2.207c-1.222-.303-2.208.289-2.513 1.511-.303 1.222.288 2.207 1.511 2.512l8.848 2.206c1.222.304 2.208-.287 2.513-1.509zM21.575 56.771l-8.331 3.711c-1.151.511-1.563 1.586-1.05 2.735.511 1.151 1.586 1.563 2.736 1.052l8.331-3.711c1.151-.511 1.563-1.586 1.05-2.735-.512-1.15-1.585-1.562-2.736-1.052z"/></svg>\');\n}\n.lh-report-icon--treemap::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="black"><path d="M3 5v14h19V5H3zm2 2h15v4H5V7zm0 10v-4h4v4H5zm6 0v-4h9v4h-9z"/></svg>\');\n}\n.lh-report-icon--date::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 11h2v2H7v-2zm14-5v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h1V2h2v2h8V2h2v2h1a2 2 0 012 2zM5 8h14V6H5v2zm14 12V10H5v10h14zm-4-7h2v-2h-2v2zm-4 0h2v-2h-2v2z"/></svg>\');\n}\n.lh-report-icon--devices::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 6h18V4H4a2 2 0 00-2 2v11H0v3h14v-3H4V6zm19 2h-6a1 1 0 00-1 1v10c0 .6.5 1 1 1h6c.6 0 1-.5 1-1V9c0-.6-.5-1-1-1zm-1 9h-4v-7h4v7z"/></svg>\');\n}\n.lh-report-icon--world::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm7 6h-3c-.3-1.3-.8-2.5-1.4-3.6A8 8 0 0 1 18.9 8zm-7-4a14 14 0 0 1 2 4h-4a14 14 0 0 1 2-4zM4.3 14a8.2 8.2 0 0 1 0-4h3.3a16.5 16.5 0 0 0 0 4H4.3zm.8 2h3a14 14 0 0 0 1.3 3.6A8 8 0 0 1 5.1 16zm3-8H5a8 8 0 0 1 4.3-3.6L8 8zM12 20a14 14 0 0 1-2-4h4a14 14 0 0 1-2 4zm2.3-6H9.7a14.7 14.7 0 0 1 0-4h4.6a14.6 14.6 0 0 1 0 4zm.3 5.6c.6-1.2 1-2.4 1.4-3.6h3a8 8 0 0 1-4.4 3.6zm1.8-5.6a16.5 16.5 0 0 0 0-4h3.3a8.2 8.2 0 0 1 0 4h-3.3z"/></svg>\');\n}\n.lh-report-icon--stopwatch::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.1-6.6L20.5 6l-1.4-1.4L17.7 6A9 9 0 0 0 3 13a9 9 0 1 0 16-5.6zm-7 12.6a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/></svg>\');\n}\n.lh-report-icon--networkspeed::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.9 5c-.2 0-.3 0-.4.2v.2L10.1 17a2 2 0 0 0-.2 1 2 2 0 0 0 4 .4l2.4-12.9c0-.3-.2-.5-.5-.5zM1 9l2 2c2.9-2.9 6.8-4 10.5-3.6l1.2-2.7C10 3.8 4.7 5.3 1 9zm20 2 2-2a15.4 15.4 0 0 0-5.6-3.6L17 8.2c1.5.7 2.9 1.6 4.1 2.8zm-4 4 2-2a9.9 9.9 0 0 0-2.7-1.9l-.5 3 1.2.9zM5 13l2 2a7.1 7.1 0 0 1 4-2l1.3-2.9C9.7 10.1 7 11 5 13z"/></svg>\');\n}\n.lh-report-icon--samples-one::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="7" cy="14" r="3"/><path d="M7 18a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5.6 17.6a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>\');\n}\n.lh-report-icon--samples-many::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5.6 17.6a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/><circle cx="7" cy="14" r="3"/><circle cx="11" cy="6" r="3"/></svg>\');\n}\n.lh-report-icon--chrome::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 562 562"><path d="M256 25.6v25.6a204 204 0 0 1 144.8 60 204 204 0 0 1 60 144.8 204 204 0 0 1-60 144.8 204 204 0 0 1-144.8 60 204 204 0 0 1-144.8-60 204 204 0 0 1-60-144.8 204 204 0 0 1 60-144.8 204 204 0 0 1 144.8-60V0a256 256 0 1 0 0 512 256 256 0 0 0 0-512v25.6z"/><path d="M256 179.2v25.6a51.3 51.3 0 0 1 0 102.4 51.3 51.3 0 0 1 0-102.4v-51.2a102.3 102.3 0 1 0-.1 204.7 102.3 102.3 0 0 0 .1-204.7v25.6z"/><path d="M256 204.8h217.6a25.6 25.6 0 0 0 0-51.2H256a25.6 25.6 0 0 0 0 51.2m44.3 76.8L191.5 470.1a25.6 25.6 0 1 0 44.4 25.6l108.8-188.5a25.6 25.6 0 1 0-44.4-25.6m-88.6 0L102.9 93.2a25.7 25.7 0 0 0-35-9.4 25.7 25.7 0 0 0-9.4 35l108.8 188.5a25.7 25.7 0 0 0 35 9.4 25.9 25.9 0 0 0 9.4-35.1"/></svg>\');\n}\n\n\n\n.lh-buttons {\n display: flex;\n flex-wrap: wrap;\n margin: var(--default-padding) 0;\n}\n.lh-button {\n height: 32px;\n border: 1px solid var(--report-border-color-secondary);\n border-radius: 3px;\n color: var(--link-color);\n background-color: var(--report-background-color);\n margin: 5px;\n}\n\n.lh-button:first-of-type {\n margin-left: 0;\n}\n\n/* Node */\n.lh-node__snippet {\n font-family: var(--report-font-family-monospace);\n color: var(--snippet-color);\n font-size: var(--report-monospace-font-size);\n line-height: 20px;\n}\n\n/* Score */\n\n.lh-audit__score-icon {\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n margin: var(--score-icon-margin);\n}\n\n.lh-audit--pass .lh-audit__display-text {\n color: var(--color-pass-secondary);\n}\n.lh-audit--pass .lh-audit__score-icon,\n.lh-scorescale-range--pass::before {\n border-radius: 100%;\n background: var(--color-pass);\n}\n\n.lh-audit--average .lh-audit__display-text {\n color: var(--color-average-secondary);\n}\n.lh-audit--average .lh-audit__score-icon,\n.lh-scorescale-range--average::before {\n background: var(--color-average);\n width: var(--icon-square-size);\n height: var(--icon-square-size);\n}\n\n.lh-audit--fail .lh-audit__display-text {\n color: var(--color-fail-secondary);\n}\n.lh-audit--fail .lh-audit__score-icon,\n.lh-audit--error .lh-audit__score-icon,\n.lh-scorescale-range--fail::before {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n\n.lh-audit--manual .lh-audit__display-text,\n.lh-audit--notapplicable .lh-audit__display-text {\n color: var(--color-gray-600);\n}\n.lh-audit--manual .lh-audit__score-icon,\n.lh-audit--notapplicable .lh-audit__score-icon {\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);\n border-radius: 100%;\n background: none;\n}\n\n.lh-audit--informative .lh-audit__display-text {\n color: var(--color-gray-600);\n}\n\n.lh-audit--informative .lh-audit__score-icon {\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);\n border-radius: 100%;\n}\n\n.lh-audit__description,\n.lh-audit__stackpack {\n color: var(--report-text-color-secondary);\n}\n.lh-audit__adorn {\n border: 1px solid slategray;\n border-radius: 3px;\n margin: 0 3px;\n padding: 0 2px;\n line-height: 1.1;\n display: inline-block;\n font-size: 90%;\n}\n\n.lh-category-header__description {\n text-align: center;\n color: var(--color-gray-700);\n margin: 0px auto;\n max-width: 400px;\n}\n\n\n.lh-audit__display-text,\n.lh-load-opportunity__sparkline,\n.lh-chevron-container {\n margin: 0 var(--audit-margin-horizontal);\n}\n.lh-chevron-container {\n margin-right: 0;\n}\n\n.lh-audit__title-and-text {\n flex: 1;\n}\n\n.lh-audit__title-and-text code {\n color: var(--snippet-color);\n font-size: var(--report-monospace-font-size);\n}\n\n/* Prepend display text with em dash separator. But not in Opportunities. */\n.lh-audit__display-text:not(:empty):before {\n content: \'—\';\n margin-right: var(--audit-margin-horizontal);\n}\n.lh-audit-group.lh-audit-group--load-opportunities .lh-audit__display-text:not(:empty):before {\n display: none;\n}\n\n/* Expandable Details (Audit Groups, Audits) */\n.lh-audit__header {\n display: flex;\n align-items: center;\n padding: var(--default-padding);\n}\n\n.lh-audit--load-opportunity .lh-audit__header {\n display: block;\n}\n\n\n.lh-metricfilter {\n display: grid;\n justify-content: end;\n align-items: center;\n grid-auto-flow: column;\n gap: 4px;\n color: var(--color-gray-700);\n}\n\n.lh-metricfilter__radio {\n position: absolute;\n left: -9999px;\n}\n.lh-metricfilter input[type=\'radio\']:focus-visible + label {\n outline: -webkit-focus-ring-color auto 1px;\n}\n\n.lh-metricfilter__label {\n display: inline-flex;\n padding: 0 4px;\n height: 16px;\n text-decoration: underline;\n align-items: center;\n cursor: pointer;\n font-size: 90%;\n}\n\n.lh-metricfilter__label--active {\n background: var(--color-blue-primary);\n color: var(--color-white);\n border-radius: 3px;\n text-decoration: none;\n}\n/* Give the \'All\' choice a more muted display */\n.lh-metricfilter__label--active[for="metric-All"] {\n background-color: var(--color-blue-200) !important;\n color: black !important;\n}\n\n.lh-metricfilter__text {\n margin-right: 8px;\n}\n\n/* If audits are filtered, hide the itemcount for Passed Audits… */\n.lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {\n display: none;\n}\n\n\n.lh-audit__header:hover {\n background-color: var(--color-hover);\n}\n\n/* We want to hide the browser\'s default arrow marker on summary elements. Admittedly, it\'s complicated. */\n.lh-root details > summary {\n /* Blink 89+ and Firefox will hide the arrow when display is changed from (new) default of `list-item` to block. https://chromestatus.com/feature/6730096436051968*/\n display: block;\n}\n/* Safari and Blink <=88 require using the -webkit-details-marker selector */\n.lh-root details > summary::-webkit-details-marker {\n display: none;\n}\n\n/* Perf Metric */\n\n.lh-metrics-container {\n display: grid;\n grid-auto-rows: 1fr;\n grid-template-columns: 1fr 1fr;\n grid-column-gap: var(--report-line-height);\n}\n\n.lh-metric {\n border-top: 1px solid var(--report-border-color-secondary);\n}\n\n.lh-metric:nth-last-child(-n+2) {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\n\n\n.lh-metric__innerwrap {\n display: grid;\n /**\n * Icon -- Metric Name\n * -- Metric Value\n */\n grid-template-columns: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 1fr;\n align-items: center;\n padding: var(--default-padding);\n}\n\n.lh-metric__details {\n order: -1;\n}\n\n.lh-metric__title {\n flex: 1;\n}\n\n.lh-calclink {\n padding-left: calc(1ex / 3);\n}\n\n.lh-metric__description {\n display: none;\n grid-column-start: 2;\n grid-column-end: 4;\n color: var(--report-text-color-secondary);\n}\n\n.lh-metric__value {\n font-size: var(--metric-value-font-size);\n margin: calc(var(--default-padding) / 2) 0;\n white-space: nowrap; /* No wrapping between metric value and the icon */\n grid-column-start: 2;\n}\n\n\n@media screen and (max-width: 535px) {\n .lh-metrics-container {\n display: block;\n }\n\n .lh-metric {\n border-bottom: none !important;\n }\n .lh-metric:nth-last-child(1) {\n border-bottom: 1px solid var(--report-border-color-secondary) !important;\n }\n\n /* Change the grid to 3 columns for narrow viewport. */\n .lh-metric__innerwrap {\n /**\n * Icon -- Metric Name -- Metric Value\n */\n grid-template-columns: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 2fr 1fr;\n }\n .lh-metric__value {\n justify-self: end;\n grid-column-start: unset;\n }\n}\n\n/* No-JS toggle switch */\n/* Keep this selector sync\'d w/ `magicSelector` in report-ui-features-test.js */\n .lh-metrics-toggle__input:checked ~ .lh-metrics-container .lh-metric__description {\n display: block;\n}\n\n/* TODO get rid of the SVGS and clean up these some more */\n.lh-metrics-toggle__input {\n opacity: 0;\n position: absolute;\n right: 0;\n top: 0px;\n}\n\n.lh-metrics-toggle__input + div > label > .lh-metrics-toggle__labeltext--hide,\n.lh-metrics-toggle__input:checked + div > label > .lh-metrics-toggle__labeltext--show {\n display: none;\n}\n.lh-metrics-toggle__input:checked + div > label > .lh-metrics-toggle__labeltext--hide {\n display: inline;\n}\n.lh-metrics-toggle__input:focus + div > label {\n outline: -webkit-focus-ring-color auto 3px;\n}\n\n.lh-metrics-toggle__label {\n cursor: pointer;\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n color: var(--color-gray-700);\n}\n\n/* Pushes the metric description toggle button to the right. */\n.lh-audit-group--metrics .lh-audit-group__header {\n display: flex;\n justify-content: space-between;\n}\n\n.lh-metric__icon,\n.lh-scorescale-range::before {\n content: \'\';\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n display: inline-block;\n margin: var(--score-icon-margin);\n}\n\n.lh-metric--pass .lh-metric__value {\n color: var(--color-pass-secondary);\n}\n.lh-metric--pass .lh-metric__icon {\n border-radius: 100%;\n background: var(--color-pass);\n}\n\n.lh-metric--average .lh-metric__value {\n color: var(--color-average-secondary);\n}\n.lh-metric--average .lh-metric__icon {\n background: var(--color-average);\n width: var(--icon-square-size);\n height: var(--icon-square-size);\n}\n\n.lh-metric--fail .lh-metric__value {\n color: var(--color-fail-secondary);\n}\n.lh-metric--fail .lh-metric__icon,\n.lh-metric--error .lh-metric__icon {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n\n.lh-metric--error .lh-metric__value,\n.lh-metric--error .lh-metric__description {\n color: var(--color-fail-secondary);\n}\n\n/* Perf load opportunity */\n\n.lh-load-opportunity__cols {\n display: flex;\n align-items: flex-start;\n}\n\n.lh-load-opportunity__header .lh-load-opportunity__col {\n color: var(--color-gray-600);\n display: unset;\n line-height: calc(2.3 * var(--report-font-size));\n}\n\n.lh-load-opportunity__col {\n display: flex;\n}\n\n.lh-load-opportunity__col--one {\n flex: 5;\n align-items: center;\n margin-right: 2px;\n}\n.lh-load-opportunity__col--two {\n flex: 4;\n text-align: right;\n}\n\n.lh-audit--load-opportunity .lh-audit__display-text {\n text-align: right;\n flex: 0 0 calc(3 * var(--report-font-size));\n}\n\n\n/* Sparkline */\n\n.lh-load-opportunity__sparkline {\n flex: 1;\n margin-top: calc((var(--report-line-height) - var(--sparkline-height)) / 2);\n}\n\n.lh-sparkline {\n height: var(--sparkline-height);\n width: 100%;\n}\n\n.lh-sparkline__bar {\n height: 100%;\n float: right;\n}\n\n.lh-audit--pass .lh-sparkline__bar {\n background: var(--color-pass);\n}\n\n.lh-audit--average .lh-sparkline__bar {\n background: var(--color-average);\n}\n\n.lh-audit--fail .lh-sparkline__bar {\n background: var(--color-fail);\n}\n\n/* Filmstrip */\n\n.lh-filmstrip-container {\n /* smaller gap between metrics and filmstrip */\n margin: -8px auto 0 auto;\n}\n\n.lh-filmstrip {\n display: grid;\n justify-content: space-between;\n padding-bottom: var(--default-padding);\n width: 100%;\n grid-template-columns: repeat(auto-fit, 60px);\n}\n\n.lh-filmstrip__frame {\n text-align: right;\n position: relative;\n}\n\n.lh-filmstrip__thumbnail {\n border: 1px solid var(--report-border-color-secondary);\n max-height: 100px;\n max-width: 60px;\n}\n\n/* Audit */\n\n.lh-audit {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\n\n/* Apply border-top to just the first audit. */\n.lh-audit {\n border-top: 1px solid var(--report-border-color-secondary);\n}\n.lh-audit ~ .lh-audit {\n border-top: none;\n}\n\n\n.lh-audit--error .lh-audit__display-text {\n color: var(--color-fail);\n}\n\n/* Audit Group */\n\n.lh-audit-group {\n margin-bottom: var(--audit-group-margin-bottom);\n position: relative;\n}\n.lh-audit-group--metrics {\n margin-bottom: calc(var(--audit-group-margin-bottom) / 2);\n}\n\n.lh-audit-group__header::before {\n /* By default, groups don\'t get an icon */\n content: none;\n width: var(--pwa-icon-size);\n height: var(--pwa-icon-size);\n margin: var(--pwa-icon-margin);\n display: inline-block;\n vertical-align: middle;\n}\n\n/* Style the "over budget" columns red. */\n.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(4),\n.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(5),\n.lh-audit-group--budgets #timing-budget tbody tr td:nth-child(3) {\n color: var(--color-red-700);\n}\n\n/* Align the "over budget request count" text to be close to the "over budget bytes" column. */\n.lh-audit-group--budgets .lh-table tbody tr td:nth-child(4){\n text-align: right;\n}\n\n.lh-audit-group--budgets .lh-details--budget {\n width: 100%;\n margin: 0 0 var(--default-padding);\n}\n\n.lh-audit-group--pwa-installable .lh-audit-group__header::before {\n content: \'\';\n background-image: var(--pwa-installable-gray-url);\n}\n.lh-audit-group--pwa-optimized .lh-audit-group__header::before {\n content: \'\';\n background-image: var(--pwa-optimized-gray-url);\n}\n.lh-audit-group--pwa-installable.lh-badged .lh-audit-group__header::before {\n background-image: var(--pwa-installable-color-url);\n}\n.lh-audit-group--pwa-optimized.lh-badged .lh-audit-group__header::before {\n background-image: var(--pwa-optimized-color-url);\n}\n\n.lh-audit-group--metrics .lh-audit-group__summary {\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.lh-audit-group__summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.lh-audit-group__header .lh-chevron {\n margin-top: calc((var(--report-line-height) - 5px) / 2);\n}\n\n.lh-audit-group__header {\n letter-spacing: 0.8px;\n padding: var(--default-padding);\n padding-left: 0;\n}\n\n.lh-audit-group__header, .lh-audit-group__summary {\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n color: var(--color-gray-700);\n}\n\n.lh-audit-group__title {\n text-transform: uppercase;\n font-weight: 500;\n}\n\n.lh-audit-group__itemcount {\n color: var(--color-gray-600);\n}\n\n.lh-audit-group__footer {\n color: var(--color-gray-600);\n display: block;\n margin-top: var(--default-padding);\n}\n\n.lh-details,\n.lh-category-header__description,\n.lh-load-opportunity__header,\n.lh-audit-group__footer {\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n}\n\n.lh-audit-explanation {\n margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2) var(--audit-margin-horizontal);\n line-height: var(--audit-explanation-line-height);\n display: inline-block;\n}\n\n.lh-audit--fail .lh-audit-explanation {\n color: var(--color-fail);\n}\n\n/* Report */\n.lh-list > div:not(:last-child) {\n padding-bottom: 20px;\n}\n\n.lh-header-container {\n display: block;\n margin: 0 auto;\n position: relative;\n word-wrap: break-word;\n}\n\n.lh-report {\n min-width: var(--report-min-width);\n}\n\n.lh-exception {\n font-size: large;\n}\n\n.lh-code {\n white-space: normal;\n margin-top: 0;\n font-size: var(--report-monospace-font-size);\n}\n\n.lh-warnings {\n --item-margin: calc(var(--report-line-height) / 6);\n color: var(--color-average-secondary);\n margin: var(--audit-padding-vertical) 0;\n padding: var(--default-padding)\n var(--default-padding)\n var(--default-padding)\n calc(var(--audit-description-padding-left));\n background-color: var(--toplevel-warning-background-color);\n}\n.lh-warnings span {\n font-weight: bold;\n}\n\n.lh-warnings--toplevel {\n --item-margin: calc(var(--header-line-height) / 4);\n color: var(--toplevel-warning-text-color);\n margin-left: auto;\n margin-right: auto;\n --content-width-minus-category-padding-sides: calc(var(--report-content-width) - calc(var(--default-padding) * 4) * 2);\n max-width: var(--content-width-minus-category-padding-sides);\n padding: var(--toplevel-warning-padding);\n border-radius: 8px;\n}\n\n.lh-warnings__msg {\n color: var(--toplevel-warning-message-text-color);\n margin: 0;\n}\n\n.lh-warnings ul {\n margin: 0;\n}\n.lh-warnings li {\n margin: var(--item-margin) 0;\n}\n.lh-warnings li:last-of-type {\n margin-bottom: 0;\n}\n\n.lh-scores-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n}\n.lh-scores-header__solo {\n padding: 0;\n border: 0;\n}\n\n/* Gauge */\n\n.lh-gauge__wrapper--pass {\n color: var(--color-pass-secondary);\n fill: var(--color-pass);\n stroke: var(--color-pass);\n}\n\n.lh-gauge__wrapper--average {\n color: var(--color-average-secondary);\n fill: var(--color-average);\n stroke: var(--color-average);\n}\n\n.lh-gauge__wrapper--fail {\n color: var(--color-fail-secondary);\n fill: var(--color-fail);\n stroke: var(--color-fail);\n}\n\n.lh-gauge__wrapper--not-applicable {\n color: var(--color-not-applicable);\n fill: var(--color-not-applicable);\n stroke: var(--color-not-applicable);\n}\n\n.lh-fraction__wrapper .lh-fraction__content::before {\n content: \'\';\n height: var(--score-icon-size);\n width: var(--score-icon-size);\n margin: var(--score-icon-margin);\n display: inline-block;\n}\n.lh-fraction__wrapper--pass .lh-fraction__content {\n color: var(--color-pass-secondary);\n}\n.lh-fraction__wrapper--pass .lh-fraction__background {\n background-color: var(--color-pass);\n}\n.lh-fraction__wrapper--pass .lh-fraction__content::before {\n background-color: var(--color-pass);\n border-radius: 50%;\n}\n.lh-fraction__wrapper--average .lh-fraction__content {\n color: var(--color-average-secondary);\n}\n.lh-fraction__wrapper--average .lh-fraction__background,\n.lh-fraction__wrapper--average .lh-fraction__content::before {\n background-color: var(--color-average);\n}\n.lh-fraction__wrapper--fail .lh-fraction__content {\n color: var(--color-fail);\n}\n.lh-fraction__wrapper--fail .lh-fraction__background {\n background-color: var(--color-fail);\n}\n.lh-fraction__wrapper--fail .lh-fraction__content::before {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n.lh-fraction__wrapper--null .lh-fraction__content {\n color: var(--color-gray-700);\n}\n.lh-fraction__wrapper--null .lh-fraction__background {\n background-color: var(--color-gray-700);\n}\n.lh-fraction__wrapper--null .lh-fraction__content::before {\n border-radius: 50%;\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-700);\n}\n\n.lh-fraction__background {\n position: absolute;\n height: 100%;\n width: 100%;\n border-radius: calc(var(--gauge-circle-size) / 2);\n opacity: 0.1;\n z-index: -1;\n}\n\n.lh-fraction__content-wrapper {\n height: var(--gauge-circle-size);\n display: flex;\n align-items: center;\n}\n\n.lh-fraction__content {\n display: flex;\n position: relative;\n align-items: center;\n justify-content: center;\n font-size: calc(0.3 * var(--gauge-circle-size));\n line-height: calc(0.4 * var(--gauge-circle-size));\n width: max-content;\n min-width: calc(1.5 * var(--gauge-circle-size));\n padding: calc(0.1 * var(--gauge-circle-size)) calc(0.2 * var(--gauge-circle-size));\n --score-icon-size: calc(0.21 * var(--gauge-circle-size));\n --score-icon-margin: 0 calc(0.15 * var(--gauge-circle-size)) 0 0;\n}\n\n.lh-gauge {\n stroke-linecap: round;\n width: var(--gauge-circle-size);\n height: var(--gauge-circle-size);\n}\n\n.lh-category .lh-gauge {\n --gauge-circle-size: var(--gauge-circle-size-big);\n}\n\n.lh-gauge-base {\n opacity: 0.1;\n}\n\n.lh-gauge-arc {\n fill: none;\n transform-origin: 50% 50%;\n animation: load-gauge var(--transition-length) ease forwards;\n animation-delay: 250ms;\n}\n\n.lh-gauge__svg-wrapper {\n position: relative;\n height: var(--gauge-circle-size);\n}\n.lh-category .lh-gauge__svg-wrapper,\n.lh-category .lh-fraction__wrapper {\n --gauge-circle-size: var(--gauge-circle-size-big);\n}\n\n/* The plugin badge overlay */\n.lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {\n width: var(--plugin-badge-size);\n height: var(--plugin-badge-size);\n background-color: var(--plugin-badge-background-color);\n background-image: var(--plugin-icon-url);\n background-repeat: no-repeat;\n background-size: var(--plugin-icon-size);\n background-position: 58% 50%;\n content: "";\n position: absolute;\n right: -6px;\n bottom: 0px;\n display: block;\n z-index: 100;\n box-shadow: 0 0 4px rgba(0,0,0,.2);\n border-radius: 25%;\n}\n.lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {\n width: var(--plugin-badge-size-big);\n height: var(--plugin-badge-size-big);\n}\n\n@keyframes load-gauge {\n from { stroke-dasharray: 0 352; }\n}\n\n.lh-gauge__percentage {\n width: 100%;\n height: var(--gauge-circle-size);\n position: absolute;\n font-family: var(--report-font-family-monospace);\n font-size: calc(var(--gauge-circle-size) * 0.34 + 1.3px);\n line-height: 0;\n text-align: center;\n top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 2);\n}\n\n.lh-category .lh-gauge__percentage {\n --gauge-circle-size: var(--gauge-circle-size-big);\n --gauge-percentage-font-size: var(--gauge-percentage-font-size-big);\n}\n\n.lh-gauge__wrapper,\n.lh-fraction__wrapper {\n position: relative;\n display: flex;\n align-items: center;\n flex-direction: column;\n text-decoration: none;\n padding: var(--score-container-padding);\n\n --transition-length: 1s;\n\n /* Contain the layout style paint & layers during animation*/\n contain: content;\n will-change: opacity; /* Only using for layer promotion */\n}\n\n.lh-gauge__label,\n.lh-fraction__label {\n font-size: var(--gauge-label-font-size);\n font-weight: 500;\n line-height: var(--gauge-label-line-height);\n margin-top: 10px;\n text-align: center;\n color: var(--report-text-color);\n word-break: keep-all;\n}\n\n/* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */\n.lh-category .lh-gauge__label,\n.lh-category .lh-fraction__label {\n --gauge-label-font-size: var(--gauge-label-font-size-big);\n --gauge-label-line-height: var(--gauge-label-line-height-big);\n margin-top: 14px;\n}\n\n.lh-scores-header .lh-gauge__wrapper,\n.lh-scores-header .lh-fraction__wrapper,\n.lh-scores-header .lh-gauge--pwa__wrapper,\n.lh-sticky-header .lh-gauge__wrapper,\n.lh-sticky-header .lh-fraction__wrapper,\n.lh-sticky-header .lh-gauge--pwa__wrapper {\n width: var(--gauge-wrapper-width);\n}\n\n.lh-scorescale {\n display: inline-flex;\n\n gap: calc(var(--default-padding) * 4);\n margin: 16px auto 0 auto;\n font-size: var(--report-font-size-secondary);\n color: var(--color-gray-700);\n\n}\n\n.lh-scorescale-range {\n display: flex;\n align-items: center;\n font-family: var(--report-font-family-monospace);\n white-space: nowrap;\n}\n\n.lh-category-header__finalscreenshot .lh-scorescale {\n border: 0;\n display: flex;\n justify-content: center;\n}\n\n.lh-category-header__finalscreenshot .lh-scorescale-range {\n font-family: unset;\n font-size: 12px;\n}\n\n.lh-scorescale-wrap {\n display: contents;\n}\n\n/* Hide category score gauages if it\'s a single category report */\n.lh-header--solo-category .lh-scores-wrapper {\n display: none;\n}\n\n\n.lh-categories {\n width: 100%;\n overflow: hidden;\n}\n\n.lh-category {\n padding: var(--category-padding);\n max-width: var(--report-content-width);\n margin: 0 auto;\n\n --sticky-header-height: calc(var(--gauge-circle-size-sm) + var(--score-container-padding) * 2);\n --topbar-plus-sticky-header: calc(var(--topbar-height) + var(--sticky-header-height));\n scroll-margin-top: var(--topbar-plus-sticky-header);\n\n /* Faster recalc style & layout of the report. https://web.dev/content-visibility/ */\n content-visibility: auto;\n contain-intrinsic-size: 1000px;\n}\n\n.lh-category-wrapper {\n border-bottom: 1px solid var(--color-gray-200);\n}\n\n.lh-category-wrapper:first-of-type {\n border-top: 1px solid var(--color-gray-200);\n}\n\n.lh-category-header {\n margin-bottom: var(--section-padding-vertical);\n}\n\n.lh-category-header .lh-score__gauge {\n max-width: 400px;\n width: auto;\n margin: 0px auto;\n}\n\n.lh-category-header__finalscreenshot {\n display: grid;\n grid-template: none / 1fr 1px 1fr;\n justify-items: center;\n align-items: center;\n gap: var(--report-line-height);\n min-height: 288px;\n margin-bottom: var(--default-padding);\n}\n\n.lh-final-ss-image {\n /* constrain the size of the image to not be too large */\n max-height: calc(var(--gauge-circle-size-big) * 2.8);\n max-width: calc(var(--gauge-circle-size-big) * 3.5);\n border: 1px solid var(--color-gray-200);\n padding: 4px;\n border-radius: 3px;\n display: block;\n}\n\n.lh-category-headercol--separator {\n background: var(--color-gray-200);\n width: 1px;\n height: var(--gauge-circle-size-big);\n}\n\n@media screen and (max-width: 780px) {\n .lh-category-header__finalscreenshot {\n grid-template: 1fr 1fr / none\n }\n .lh-category-headercol--separator {\n display: none;\n }\n}\n\n\n/* 964 fits the min-width of the filmstrip */\n@media screen and (max-width: 964px) {\n .lh-report {\n margin-left: 0;\n width: 100%;\n }\n}\n\n@media print {\n body {\n -webkit-print-color-adjust: exact; /* print background colors */\n }\n .lh-container {\n display: block;\n }\n .lh-report {\n margin-left: 0;\n padding-top: 0;\n }\n .lh-categories {\n margin-top: 0;\n }\n}\n\n.lh-table {\n border-collapse: collapse;\n /* Can\'t assign padding to table, so shorten the width instead. */\n width: calc(100% - var(--audit-description-padding-left) - var(--stackpack-padding-horizontal));\n border: 1px solid var(--report-border-color-secondary);\n\n}\n\n.lh-table thead th {\n font-weight: normal;\n color: var(--color-gray-600);\n /* See text-wrapping comment on .lh-container. */\n word-break: normal;\n}\n\n.lh-row--even {\n background-color: var(--table-higlight-background-color);\n}\n.lh-row--hidden {\n display: none;\n}\n\n.lh-table th,\n.lh-table td {\n padding: var(--default-padding);\n}\n\n.lh-table tr {\n vertical-align: middle;\n}\n\n/* Looks unnecessary, but mostly for keeping the <th>s left-aligned */\n.lh-table-column--text,\n.lh-table-column--source-location,\n.lh-table-column--url,\n/* .lh-table-column--thumbnail, */\n/* .lh-table-column--empty,*/\n.lh-table-column--code,\n.lh-table-column--node {\n text-align: left;\n}\n\n.lh-table-column--code {\n min-width: 100px;\n}\n\n.lh-table-column--bytes,\n.lh-table-column--timespanMs,\n.lh-table-column--ms,\n.lh-table-column--numeric {\n text-align: right;\n word-break: normal;\n}\n\n\n\n.lh-table .lh-table-column--thumbnail {\n width: var(--image-preview-size);\n}\n\n.lh-table-column--url {\n min-width: 250px;\n}\n\n.lh-table-column--text {\n min-width: 80px;\n}\n\n/* Keep columns narrow if they follow the URL column */\n/* 12% was determined to be a decent narrow width, but wide enough for column headings */\n.lh-table-column--url + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--ms,\n.lh-table-column--url + .lh-table-column--ms + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--timespanMs {\n width: 12%;\n}\n\n.lh-text__url-host {\n display: inline;\n}\n\n.lh-text__url-host {\n margin-left: calc(var(--report-font-size) / 2);\n opacity: 0.6;\n font-size: 90%\n}\n\n.lh-thumbnail {\n object-fit: cover;\n width: var(--image-preview-size);\n height: var(--image-preview-size);\n display: block;\n}\n\n.lh-unknown pre {\n overflow: scroll;\n border: solid 1px var(--color-gray-200);\n}\n\n.lh-text__url > a {\n color: inherit;\n text-decoration: none;\n}\n\n.lh-text__url > a:hover {\n text-decoration: underline dotted #999;\n}\n\n.lh-sub-item-row {\n margin-left: 20px;\n margin-bottom: 0;\n color: var(--color-gray-700);\n}\n.lh-sub-item-row td {\n padding-top: 4px;\n padding-bottom: 4px;\n padding-left: 20px;\n}\n\n/* Chevron\n https://codepen.io/paulirish/pen/LmzEmK\n */\n.lh-chevron {\n --chevron-angle: 42deg;\n /* Edge doesn\'t support transform: rotate(calc(...)), so we define it here */\n --chevron-angle-right: -42deg;\n width: var(--chevron-size);\n height: var(--chevron-size);\n margin-top: calc((var(--report-line-height) - 12px) / 2);\n}\n\n.lh-chevron__lines {\n transition: transform 0.4s;\n transform: translateY(var(--report-line-height));\n}\n.lh-chevron__line {\n stroke: var(--chevron-line-stroke);\n stroke-width: var(--chevron-size);\n stroke-linecap: square;\n transform-origin: 50%;\n transform: rotate(var(--chevron-angle));\n transition: transform 300ms, stroke 300ms;\n}\n\n.lh-expandable-details .lh-chevron__line-right,\n.lh-expandable-details[open] .lh-chevron__line-left {\n transform: rotate(var(--chevron-angle-right));\n}\n\n.lh-expandable-details[open] .lh-chevron__line-right {\n transform: rotate(var(--chevron-angle));\n}\n\n\n.lh-expandable-details[open] .lh-chevron__lines {\n transform: translateY(calc(var(--chevron-size) * -1));\n}\n\n.lh-expandable-details[open] {\n animation: 300ms openDetails forwards;\n padding-bottom: var(--default-padding);\n}\n\n@keyframes openDetails {\n from {\n outline: 1px solid var(--report-background-color);\n }\n to {\n outline: 1px solid;\n box-shadow: 0 2px 4px rgba(0, 0, 0, .24);\n }\n}\n\n@media screen and (max-width: 780px) {\n /* no black outline if we\'re not confident the entire table can be displayed within bounds */\n .lh-expandable-details[open] {\n animation: none;\n }\n}\n\n.lh-expandable-details[open] summary, details.lh-clump > summary {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\ndetails.lh-clump[open] > summary {\n border-bottom-width: 0;\n}\n\n\n\ndetails .lh-clump-toggletext--hide,\ndetails[open] .lh-clump-toggletext--show { display: none; }\ndetails[open] .lh-clump-toggletext--hide { display: block;}\n\n\n/* Tooltip */\n.lh-tooltip-boundary {\n position: relative;\n}\n\n.lh-tooltip {\n position: absolute;\n display: none; /* Don\'t retain these layers when not needed */\n opacity: 0;\n background: #ffffff;\n white-space: pre-line; /* Render newlines in the text */\n min-width: 246px;\n max-width: 275px;\n padding: 15px;\n border-radius: 5px;\n text-align: initial;\n line-height: 1.4;\n}\n/* shrink tooltips to not be cutoff on left edge of narrow viewports\n 45vw is chosen to be ~= width of the left column of metrics\n*/\n@media screen and (max-width: 535px) {\n .lh-tooltip {\n min-width: 45vw;\n padding: 3vw;\n }\n}\n\n.lh-tooltip-boundary:hover .lh-tooltip {\n display: block;\n animation: fadeInTooltip 250ms;\n animation-fill-mode: forwards;\n animation-delay: 850ms;\n bottom: 100%;\n z-index: 1;\n will-change: opacity;\n right: 0;\n pointer-events: none;\n}\n\n.lh-tooltip::before {\n content: "";\n border: solid transparent;\n border-bottom-color: #fff;\n border-width: 10px;\n position: absolute;\n bottom: -20px;\n right: 6px;\n transform: rotate(180deg);\n pointer-events: none;\n}\n\n@keyframes fadeInTooltip {\n 0% { opacity: 0; }\n 75% { opacity: 1; }\n 100% { opacity: 1; filter: drop-shadow(1px 0px 1px #aaa) drop-shadow(0px 2px 4px hsla(206, 6%, 25%, 0.15)); pointer-events: auto; }\n}\n\n/* Element screenshot */\n.lh-element-screenshot {\n position: relative;\n overflow: hidden;\n float: left;\n margin-right: 20px;\n}\n.lh-element-screenshot__content {\n overflow: hidden;\n}\n.lh-element-screenshot__image {\n /* Set by ElementScreenshotRenderer.installFullPageScreenshotCssVariable */\n background-image: var(--element-screenshot-url);\n outline: 2px solid #777;\n background-color: white;\n background-repeat: no-repeat;\n}\n.lh-element-screenshot__mask {\n position: absolute;\n background: #555;\n opacity: 0.8;\n}\n.lh-element-screenshot__element-marker {\n position: absolute;\n outline: 2px solid var(--color-lime-400);\n}\n.lh-element-screenshot__overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2000; /* .lh-topbar is 1000 */\n background: var(--screenshot-overlay-background);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: zoom-out;\n}\n\n.lh-element-screenshot__overlay .lh-element-screenshot {\n margin-right: 0; /* clearing margin used in thumbnail case */\n outline: 1px solid var(--color-gray-700);\n}\n\n.lh-screenshot-overlay--enabled .lh-element-screenshot {\n cursor: zoom-out;\n}\n.lh-screenshot-overlay--enabled .lh-node .lh-element-screenshot {\n cursor: zoom-in;\n}\n\n\n.lh-meta__items {\n --meta-icon-size: calc(var(--report-icon-size) * 0.667);\n padding: var(--default-padding);\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n background-color: var(--env-item-background-color);\n border-radius: 3px;\n margin: 0 0 var(--default-padding) 0;\n font-size: 12px;\n column-gap: var(--default-padding);\n color: var(--color-gray-700);\n}\n\n.lh-meta__item {\n display: block;\n list-style-type: none;\n position: relative;\n padding: 0 0 0 calc(var(--meta-icon-size) + var(--default-padding) * 2);\n cursor: unset; /* disable pointer cursor from report-icon */\n}\n\n.lh-meta__item.lh-tooltip-boundary {\n text-decoration: dotted underline var(--color-gray-500);\n cursor: help;\n}\n\n.lh-meta__item.lh-report-icon::before {\n position: absolute;\n left: var(--default-padding);\n width: var(--meta-icon-size);\n height: var(--meta-icon-size);\n}\n\n.lh-meta__item.lh-report-icon:hover::before {\n opacity: 0.7;\n}\n\n.lh-meta__item .lh-tooltip {\n color: var(--color-gray-800);\n}\n\n.lh-meta__item .lh-tooltip::before {\n right: auto; /* Set the tooltip arrow to the leftside */\n left: 6px;\n}\n\n/* Change the grid for narrow viewport. */\n@media screen and (max-width: 640px) {\n .lh-meta__items {\n grid-template-columns: 1fr 1fr;\n }\n}\n@media screen and (max-width: 535px) {\n .lh-meta__items {\n display: block;\n }\n}\n\n\n/*# sourceURL=report-styles.css */\n');
|
|
1217
|
+
el0.append(el1);
|
|
1218
|
+
return el0;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1209
1221
|
/**
|
|
1210
1222
|
* @param {DOM} dom
|
|
1211
1223
|
*/
|
|
1212
1224
|
function createTopbarComponent(dom) {
|
|
1213
|
-
const el0 = dom.
|
|
1225
|
+
const el0 = dom.createFragment();
|
|
1214
1226
|
const el1 = dom.createElement('style');
|
|
1215
|
-
el1.append('\n .lh-topbar {\n position: sticky;\n top: 0;\n left: 0;\n right: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n height: var(--topbar-height);\n background-color: var(--topbar-background-color);\n
|
|
1227
|
+
el1.append('\n .lh-topbar {\n position: sticky;\n top: 0;\n left: 0;\n right: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n height: var(--topbar-height);\n padding: var(--topbar-padding);\n font-size: var(--report-font-size-secondary);\n background-color: var(--topbar-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n }\n\n .lh-topbar__logo {\n width: var(--topbar-logo-size);\n height: var(--topbar-logo-size);\n user-select: none;\n flex: none;\n }\n\n .lh-topbar__url {\n margin: var(--topbar-padding);\n text-decoration: none;\n color: var(--report-text-color);\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n }\n\n .lh-tools {\n display: flex;\n align-items: center;\n margin-left: auto;\n will-change: transform;\n min-width: var(--report-icon-size);\n }\n .lh-tools__button {\n width: var(--report-icon-size);\n min-width: 24px;\n height: var(--report-icon-size);\n cursor: pointer;\n margin-right: 5px;\n /* This is actually a button element, but we want to style it like a transparent div. */\n display: flex;\n background: none;\n color: inherit;\n border: none;\n padding: 0;\n font: inherit;\n outline: inherit;\n }\n .lh-tools__button svg {\n fill: var(--tools-icon-color);\n }\n .lh-dark .lh-tools__button svg {\n filter: invert(1);\n }\n .lh-tools__button.lh-active + .lh-tools__dropdown {\n opacity: 1;\n clip: rect(-1px, 194px, 242px, -3px);\n visibility: visible;\n }\n .lh-tools__dropdown {\n position: absolute;\n background-color: var(--report-background-color);\n border: 1px solid var(--report-border-color);\n border-radius: 3px;\n padding: calc(var(--default-padding) / 2) 0;\n cursor: pointer;\n top: 36px;\n right: 0;\n box-shadow: 1px 1px 3px #ccc;\n min-width: 125px;\n clip: rect(0, 164px, 0, 0);\n visibility: hidden;\n opacity: 0;\n transition: all 200ms cubic-bezier(0,0,0.2,1);\n }\n .lh-tools__dropdown a {\n color: currentColor;\n text-decoration: none;\n white-space: nowrap;\n padding: 0 6px;\n line-height: 2;\n }\n .lh-tools__dropdown a:hover,\n .lh-tools__dropdown a:focus {\n background-color: var(--color-gray-200);\n outline: none;\n }\n /* save-gist option hidden in report. */\n .lh-tools__dropdown a[data-action=\'save-gist\'] {\n display: none;\n }\n\n .lh-locale-selector {\n width: 100%;\n color: var(--report-text-color);\n background-color: var(--locale-selector-background-color);\n padding: 2px;\n }\n .lh-tools-locale {\n display: flex;\n align-items: center;\n flex-direction: row-reverse;\n }\n .lh-tools-locale__selector-wrapper {\n transition: opacity 0.15s;\n opacity: 0;\n max-width: 200px;\n }\n .lh-button.lh-tool-locale__button {\n height: var(--topbar-height);\n color: var(--tools-icon-color);\n padding: calc(var(--default-padding) / 2);\n }\n .lh-tool-locale__button.lh-active + .lh-tools-locale__selector-wrapper {\n opacity: 1;\n clip: rect(-1px, 194px, 242px, -3px);\n visibility: visible;\n margin: 0 4px;\n }\n\n @media screen and (max-width: 964px) {\n .lh-tools__dropdown {\n right: 0;\n left: initial;\n }\n }\n @media print {\n .lh-topbar {\n position: static;\n margin-left: 0;\n }\n\n .lh-tools__dropdown {\n display: none;\n }\n }\n ');
|
|
1216
1228
|
el0.append(el1);
|
|
1217
1229
|
const el2 = dom.createElement('div', 'lh-topbar');
|
|
1218
1230
|
const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-topbar__logo');
|
|
@@ -1319,78 +1331,105 @@ function createTopbarComponent(dom) {
|
|
|
1319
1331
|
el25.setAttribute('target', '_blank');
|
|
1320
1332
|
el25.setAttribute('rel', 'noopener');
|
|
1321
1333
|
const el26 = dom.createElement('div', 'lh-tools');
|
|
1322
|
-
const el27 = dom.createElement('
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
el29.setAttribute('
|
|
1335
|
-
el29.setAttribute('fill', 'none');
|
|
1334
|
+
const el27 = dom.createElement('div', 'lh-tools-locale lh-hidden');
|
|
1335
|
+
const el28 = dom.createElement('button', 'lh-button lh-tool-locale__button');
|
|
1336
|
+
el28.setAttribute('id', 'lh-button__swap-locales');
|
|
1337
|
+
el28.setAttribute('title', 'Show Language Picker');
|
|
1338
|
+
el28.setAttribute('aria-label', 'Toggle language picker');
|
|
1339
|
+
el28.setAttribute('aria-haspopup', 'menu');
|
|
1340
|
+
el28.setAttribute('aria-expanded', 'false');
|
|
1341
|
+
el28.setAttribute('aria-controls', 'lh-tools-locale__selector-wrapper');
|
|
1342
|
+
const el29 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1343
|
+
el29.setAttribute('width', '20px');
|
|
1344
|
+
el29.setAttribute('height', '20px');
|
|
1345
|
+
el29.setAttribute('viewBox', '0 0 24 24');
|
|
1346
|
+
el29.setAttribute('fill', 'currentColor');
|
|
1336
1347
|
const el30 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1337
|
-
el30.setAttribute('d', '
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
el32.setAttribute('role', '
|
|
1346
|
-
el32.setAttribute('
|
|
1347
|
-
el32.setAttribute('
|
|
1348
|
-
el32.
|
|
1349
|
-
|
|
1350
|
-
const el33 = dom.createElement('
|
|
1351
|
-
el33.setAttribute('
|
|
1352
|
-
el33.setAttribute('
|
|
1353
|
-
el33.setAttribute('
|
|
1354
|
-
el33.setAttribute('
|
|
1355
|
-
el33.setAttribute('
|
|
1356
|
-
|
|
1357
|
-
el34.
|
|
1358
|
-
el34.setAttribute('
|
|
1359
|
-
el34.setAttribute('
|
|
1360
|
-
el34.setAttribute('
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
el35.setAttribute('
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
el36.setAttribute('data-action', 'save-json');
|
|
1374
|
-
const el37 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
|
|
1375
|
-
el37.setAttribute('role', 'menuitem');
|
|
1376
|
-
el37.setAttribute('tabindex', '-1');
|
|
1377
|
-
el37.setAttribute('href', '#');
|
|
1378
|
-
el37.setAttribute('data-i18n', 'dropdownViewer');
|
|
1379
|
-
el37.setAttribute('data-action', 'open-viewer');
|
|
1380
|
-
const el38 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
|
|
1348
|
+
el30.setAttribute('d', 'M0 0h24v24H0V0z');
|
|
1349
|
+
el30.setAttribute('fill', 'none');
|
|
1350
|
+
const el31 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1351
|
+
el31.setAttribute('d', 'M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z');
|
|
1352
|
+
el29.append(el30, el31);
|
|
1353
|
+
el28.append(' ', el29, ' ');
|
|
1354
|
+
const el32 = dom.createElement('div', 'lh-tools-locale__selector-wrapper');
|
|
1355
|
+
el32.setAttribute('id', 'lh-tools-locale__selector-wrapper');
|
|
1356
|
+
el32.setAttribute('role', 'menu');
|
|
1357
|
+
el32.setAttribute('aria-labelledby', 'lh-button__swap-locales');
|
|
1358
|
+
el32.setAttribute('aria-hidden', 'true');
|
|
1359
|
+
el32.append(' ', ' ');
|
|
1360
|
+
el27.append(' ', el28, ' ', el32, ' ');
|
|
1361
|
+
const el33 = dom.createElement('button', 'lh-tools__button');
|
|
1362
|
+
el33.setAttribute('id', 'lh-tools-button');
|
|
1363
|
+
el33.setAttribute('title', 'Tools menu');
|
|
1364
|
+
el33.setAttribute('aria-label', 'Toggle report tools menu');
|
|
1365
|
+
el33.setAttribute('aria-haspopup', 'menu');
|
|
1366
|
+
el33.setAttribute('aria-expanded', 'false');
|
|
1367
|
+
el33.setAttribute('aria-controls', 'lh-tools-dropdown');
|
|
1368
|
+
const el34 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1369
|
+
el34.setAttribute('width', '100%');
|
|
1370
|
+
el34.setAttribute('height', '100%');
|
|
1371
|
+
el34.setAttribute('viewBox', '0 0 24 24');
|
|
1372
|
+
const el35 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1373
|
+
el35.setAttribute('d', 'M0 0h24v24H0z');
|
|
1374
|
+
el35.setAttribute('fill', 'none');
|
|
1375
|
+
const el36 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1376
|
+
el36.setAttribute('d', 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z');
|
|
1377
|
+
el34.append(' ', el35, ' ', el36, ' ');
|
|
1378
|
+
el33.append(' ', el34, ' ');
|
|
1379
|
+
const el37 = dom.createElement('div', 'lh-tools__dropdown');
|
|
1380
|
+
el37.setAttribute('id', 'lh-tools-dropdown');
|
|
1381
|
+
el37.setAttribute('role', 'menu');
|
|
1382
|
+
el37.setAttribute('aria-labelledby', 'lh-tools-button');
|
|
1383
|
+
const el38 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
|
|
1381
1384
|
el38.setAttribute('role', 'menuitem');
|
|
1382
1385
|
el38.setAttribute('tabindex', '-1');
|
|
1383
1386
|
el38.setAttribute('href', '#');
|
|
1384
|
-
el38.setAttribute('data-i18n', '
|
|
1385
|
-
el38.setAttribute('data-action', '
|
|
1386
|
-
const el39 = dom.createElement('a', 'lh-report-icon lh-report-icon--
|
|
1387
|
+
el38.setAttribute('data-i18n', 'dropdownPrintSummary');
|
|
1388
|
+
el38.setAttribute('data-action', 'print-summary');
|
|
1389
|
+
const el39 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
|
|
1387
1390
|
el39.setAttribute('role', 'menuitem');
|
|
1388
1391
|
el39.setAttribute('tabindex', '-1');
|
|
1389
1392
|
el39.setAttribute('href', '#');
|
|
1390
|
-
el39.setAttribute('data-i18n', '
|
|
1391
|
-
el39.setAttribute('data-action', '
|
|
1392
|
-
|
|
1393
|
-
|
|
1393
|
+
el39.setAttribute('data-i18n', 'dropdownPrintExpanded');
|
|
1394
|
+
el39.setAttribute('data-action', 'print-expanded');
|
|
1395
|
+
const el40 = dom.createElement('a', 'lh-report-icon lh-report-icon--copy');
|
|
1396
|
+
el40.setAttribute('role', 'menuitem');
|
|
1397
|
+
el40.setAttribute('tabindex', '-1');
|
|
1398
|
+
el40.setAttribute('href', '#');
|
|
1399
|
+
el40.setAttribute('data-i18n', 'dropdownCopyJSON');
|
|
1400
|
+
el40.setAttribute('data-action', 'copy');
|
|
1401
|
+
const el41 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
|
|
1402
|
+
el41.setAttribute('role', 'menuitem');
|
|
1403
|
+
el41.setAttribute('tabindex', '-1');
|
|
1404
|
+
el41.setAttribute('href', '#');
|
|
1405
|
+
el41.setAttribute('data-i18n', 'dropdownSaveHTML');
|
|
1406
|
+
el41.setAttribute('data-action', 'save-html');
|
|
1407
|
+
const el42 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
|
|
1408
|
+
el42.setAttribute('role', 'menuitem');
|
|
1409
|
+
el42.setAttribute('tabindex', '-1');
|
|
1410
|
+
el42.setAttribute('href', '#');
|
|
1411
|
+
el42.setAttribute('data-i18n', 'dropdownSaveJSON');
|
|
1412
|
+
el42.setAttribute('data-action', 'save-json');
|
|
1413
|
+
const el43 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
|
|
1414
|
+
el43.setAttribute('role', 'menuitem');
|
|
1415
|
+
el43.setAttribute('tabindex', '-1');
|
|
1416
|
+
el43.setAttribute('href', '#');
|
|
1417
|
+
el43.setAttribute('data-i18n', 'dropdownViewer');
|
|
1418
|
+
el43.setAttribute('data-action', 'open-viewer');
|
|
1419
|
+
const el44 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
|
|
1420
|
+
el44.setAttribute('role', 'menuitem');
|
|
1421
|
+
el44.setAttribute('tabindex', '-1');
|
|
1422
|
+
el44.setAttribute('href', '#');
|
|
1423
|
+
el44.setAttribute('data-i18n', 'dropdownSaveGist');
|
|
1424
|
+
el44.setAttribute('data-action', 'save-gist');
|
|
1425
|
+
const el45 = dom.createElement('a', 'lh-report-icon lh-report-icon--dark');
|
|
1426
|
+
el45.setAttribute('role', 'menuitem');
|
|
1427
|
+
el45.setAttribute('tabindex', '-1');
|
|
1428
|
+
el45.setAttribute('href', '#');
|
|
1429
|
+
el45.setAttribute('data-i18n', 'dropdownDarkTheme');
|
|
1430
|
+
el45.setAttribute('data-action', 'toggle-dark');
|
|
1431
|
+
el37.append(' ', el38, ' ', el39, ' ', el40, ' ', el41, ' ', el42, ' ', el43, ' ', el44, ' ', el45, ' ');
|
|
1432
|
+
el26.append(' ', el27, ' ', el33, ' ', el37, ' ');
|
|
1394
1433
|
el2.append(' ', ' ', el3, ' ', el25, ' ', el26, ' ');
|
|
1395
1434
|
el0.append(el2);
|
|
1396
1435
|
return el0;
|
|
@@ -1400,7 +1439,7 @@ function createTopbarComponent(dom) {
|
|
|
1400
1439
|
* @param {DOM} dom
|
|
1401
1440
|
*/
|
|
1402
1441
|
function createWarningsToplevelComponent(dom) {
|
|
1403
|
-
const el0 = dom.
|
|
1442
|
+
const el0 = dom.createFragment();
|
|
1404
1443
|
const el1 = dom.createElement('div', 'lh-warnings lh-warnings--toplevel');
|
|
1405
1444
|
const el2 = dom.createElement('p', 'lh-warnings__msg');
|
|
1406
1445
|
const el3 = dom.createElement('ul');
|
|
@@ -1410,7 +1449,7 @@ function createWarningsToplevelComponent(dom) {
|
|
|
1410
1449
|
}
|
|
1411
1450
|
|
|
1412
1451
|
|
|
1413
|
-
/** @typedef {'3pFilter'|'audit'|'categoryHeader'|'chevron'|'clump'|'crc'|'crcChain'|'elementScreenshot'|'
|
|
1452
|
+
/** @typedef {'3pFilter'|'audit'|'categoryHeader'|'chevron'|'clump'|'crc'|'crcChain'|'elementScreenshot'|'footer'|'fraction'|'gauge'|'gaugePwa'|'heading'|'metric'|'opportunity'|'opportunityHeader'|'scorescale'|'scoresWrapper'|'snippet'|'snippetContent'|'snippetHeader'|'snippetLine'|'styles'|'topbar'|'warningsToplevel'} ComponentName */
|
|
1414
1453
|
/**
|
|
1415
1454
|
* @param {DOM} dom
|
|
1416
1455
|
* @param {ComponentName} componentName
|
|
@@ -1426,14 +1465,12 @@ function createComponent(dom, componentName) {
|
|
|
1426
1465
|
case 'crc': return createCrcComponent(dom);
|
|
1427
1466
|
case 'crcChain': return createCrcChainComponent(dom);
|
|
1428
1467
|
case 'elementScreenshot': return createElementScreenshotComponent(dom);
|
|
1429
|
-
case 'envItem': return createEnvItemComponent(dom);
|
|
1430
1468
|
case 'footer': return createFooterComponent(dom);
|
|
1431
1469
|
case 'fraction': return createFractionComponent(dom);
|
|
1432
1470
|
case 'gauge': return createGaugeComponent(dom);
|
|
1433
1471
|
case 'gaugePwa': return createGaugePwaComponent(dom);
|
|
1434
1472
|
case 'heading': return createHeadingComponent(dom);
|
|
1435
1473
|
case 'metric': return createMetricComponent(dom);
|
|
1436
|
-
case 'metricsToggle': return createMetricsToggleComponent(dom);
|
|
1437
1474
|
case 'opportunity': return createOpportunityComponent(dom);
|
|
1438
1475
|
case 'opportunityHeader': return createOpportunityHeaderComponent(dom);
|
|
1439
1476
|
case 'scorescale': return createScorescaleComponent(dom);
|
|
@@ -1442,6 +1479,7 @@ function createComponent(dom, componentName) {
|
|
|
1442
1479
|
case 'snippetContent': return createSnippetContentComponent(dom);
|
|
1443
1480
|
case 'snippetHeader': return createSnippetHeaderComponent(dom);
|
|
1444
1481
|
case 'snippetLine': return createSnippetLineComponent(dom);
|
|
1482
|
+
case 'styles': return createStylesComponent(dom);
|
|
1445
1483
|
case 'topbar': return createTopbarComponent(dom);
|
|
1446
1484
|
case 'warningsToplevel': return createWarningsToplevelComponent(dom);
|
|
1447
1485
|
}
|
|
@@ -1468,14 +1506,18 @@ function createComponent(dom, componentName) {
|
|
|
1468
1506
|
class DOM {
|
|
1469
1507
|
/**
|
|
1470
1508
|
* @param {Document} document
|
|
1509
|
+
* @param {HTMLElement} rootEl
|
|
1471
1510
|
*/
|
|
1472
|
-
constructor(document) {
|
|
1511
|
+
constructor(document, rootEl) {
|
|
1473
1512
|
/** @type {Document} */
|
|
1474
1513
|
this._document = document;
|
|
1475
1514
|
/** @type {string} */
|
|
1476
1515
|
this._lighthouseChannel = 'unknown';
|
|
1477
1516
|
/** @type {Map<string, DocumentFragment>} */
|
|
1478
1517
|
this._componentCache = new Map();
|
|
1518
|
+
/** @type {HTMLElement} */
|
|
1519
|
+
// For legacy Report API users, this'll be undefined, but set in renderReport
|
|
1520
|
+
this.rootEl = rootEl;
|
|
1479
1521
|
}
|
|
1480
1522
|
|
|
1481
1523
|
/**
|
|
@@ -1517,6 +1559,15 @@ class DOM {
|
|
|
1517
1559
|
return this._document.createDocumentFragment();
|
|
1518
1560
|
}
|
|
1519
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* @param {string} data
|
|
1564
|
+
* @return {!Node}
|
|
1565
|
+
*/
|
|
1566
|
+
createTextNode(data) {
|
|
1567
|
+
return this._document.createTextNode(data);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
|
|
1520
1571
|
/**
|
|
1521
1572
|
* @template {string} T
|
|
1522
1573
|
* @param {Element} parentElem
|
|
@@ -1657,6 +1708,8 @@ class DOM {
|
|
|
1657
1708
|
}
|
|
1658
1709
|
|
|
1659
1710
|
/**
|
|
1711
|
+
* ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred
|
|
1712
|
+
* for all scoping, because a document can have multiple reports within it.
|
|
1660
1713
|
* @return {Document}
|
|
1661
1714
|
*/
|
|
1662
1715
|
document() {
|
|
@@ -1712,6 +1765,25 @@ class DOM {
|
|
|
1712
1765
|
const event = new CustomEvent(name, detail ? {detail} : undefined);
|
|
1713
1766
|
target.dispatchEvent(event);
|
|
1714
1767
|
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Downloads a file (blob) using a[download].
|
|
1771
|
+
* @param {Blob|File} blob The file to save.
|
|
1772
|
+
* @param {string} filename
|
|
1773
|
+
*/
|
|
1774
|
+
saveFile(blob, filename) {
|
|
1775
|
+
const ext = blob.type.match('json') ? '.json' : '.html';
|
|
1776
|
+
|
|
1777
|
+
const a = this.createElement('a');
|
|
1778
|
+
a.download = `${filename}${ext}`;
|
|
1779
|
+
this.safelySetBlobHref(a, blob);
|
|
1780
|
+
this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
|
|
1781
|
+
a.click();
|
|
1782
|
+
|
|
1783
|
+
// cleanup.
|
|
1784
|
+
this._document.body.removeChild(a);
|
|
1785
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 500);
|
|
1786
|
+
}
|
|
1715
1787
|
}
|
|
1716
1788
|
|
|
1717
1789
|
/**
|
|
@@ -1841,7 +1913,7 @@ class CategoryRenderer {
|
|
|
1841
1913
|
const warningsEl = this.dom.createChildOf(summaryEl, 'div', 'lh-warnings');
|
|
1842
1914
|
this.dom.createChildOf(warningsEl, 'span').textContent = strings.warningHeader;
|
|
1843
1915
|
if (warnings.length === 1) {
|
|
1844
|
-
warningsEl.appendChild(this.dom.
|
|
1916
|
+
warningsEl.appendChild(this.dom.createTextNode(warnings.join('')));
|
|
1845
1917
|
} else {
|
|
1846
1918
|
const warningsUl = this.dom.createChildOf(warningsEl, 'ul');
|
|
1847
1919
|
for (const warning of warnings) {
|
|
@@ -1852,6 +1924,35 @@ class CategoryRenderer {
|
|
|
1852
1924
|
return auditEl;
|
|
1853
1925
|
}
|
|
1854
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Inject the final screenshot next to the score gauge of the first category (likely Performance)
|
|
1929
|
+
* @param {HTMLElement} categoriesEl
|
|
1930
|
+
* @param {LH.ReportResult['audits']} audits
|
|
1931
|
+
* @param {Element} scoreScaleEl
|
|
1932
|
+
*/
|
|
1933
|
+
injectFinalScreenshot(categoriesEl, audits, scoreScaleEl) {
|
|
1934
|
+
const audit = audits['final-screenshot'];
|
|
1935
|
+
if (!audit || audit.scoreDisplayMode === 'error') return null;
|
|
1936
|
+
if (!audit.details || audit.details.type !== 'screenshot') return null;
|
|
1937
|
+
|
|
1938
|
+
const imgEl = this.dom.createElement('img', 'lh-final-ss-image');
|
|
1939
|
+
const finalScreenshotDataUri = audit.details.data;
|
|
1940
|
+
imgEl.src = finalScreenshotDataUri;
|
|
1941
|
+
imgEl.alt = audit.title;
|
|
1942
|
+
|
|
1943
|
+
const firstCatHeaderEl = this.dom.find('.lh-category .lh-category-header', categoriesEl);
|
|
1944
|
+
const leftColEl = this.dom.createElement('div', 'lh-category-headercol');
|
|
1945
|
+
const separatorEl = this.dom.createElement('div',
|
|
1946
|
+
'lh-category-headercol lh-category-headercol--separator');
|
|
1947
|
+
const rightColEl = this.dom.createElement('div', 'lh-category-headercol');
|
|
1948
|
+
|
|
1949
|
+
leftColEl.append(...firstCatHeaderEl.childNodes);
|
|
1950
|
+
leftColEl.append(scoreScaleEl);
|
|
1951
|
+
rightColEl.append(imgEl);
|
|
1952
|
+
firstCatHeaderEl.append(leftColEl, separatorEl, rightColEl);
|
|
1953
|
+
firstCatHeaderEl.classList.add('lh-category-header__finalscreenshot');
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1855
1956
|
/**
|
|
1856
1957
|
* @return {Element}
|
|
1857
1958
|
*/
|
|
@@ -1901,7 +2002,7 @@ class CategoryRenderer {
|
|
|
1901
2002
|
* Renders the group container for a group of audits. Individual audit elements can be added
|
|
1902
2003
|
* directly to the returned element.
|
|
1903
2004
|
* @param {LH.Result.ReportGroup} group
|
|
1904
|
-
* @return {Element}
|
|
2005
|
+
* @return {[Element, Element | null]}
|
|
1905
2006
|
*/
|
|
1906
2007
|
renderAuditGroup(group) {
|
|
1907
2008
|
const groupEl = this.dom.createElement('div', 'lh-audit-group');
|
|
@@ -1910,14 +2011,16 @@ class CategoryRenderer {
|
|
|
1910
2011
|
|
|
1911
2012
|
this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title')
|
|
1912
2013
|
.textContent = group.title;
|
|
2014
|
+
groupEl.appendChild(auditGroupHeader);
|
|
2015
|
+
|
|
2016
|
+
let footerEl = null;
|
|
1913
2017
|
if (group.description) {
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2018
|
+
footerEl = this.dom.convertMarkdownLinkSnippets(group.description);
|
|
2019
|
+
footerEl.classList.add('lh-audit-group__description', 'lh-audit-group__footer');
|
|
2020
|
+
groupEl.appendChild(footerEl);
|
|
1917
2021
|
}
|
|
1918
|
-
groupEl.appendChild(auditGroupHeader);
|
|
1919
2022
|
|
|
1920
|
-
return groupEl;
|
|
2023
|
+
return [groupEl, footerEl];
|
|
1921
2024
|
}
|
|
1922
2025
|
|
|
1923
2026
|
/**
|
|
@@ -1957,9 +2060,9 @@ class CategoryRenderer {
|
|
|
1957
2060
|
|
|
1958
2061
|
// Push grouped audits as a group.
|
|
1959
2062
|
const groupDef = groupDefinitions[groupId];
|
|
1960
|
-
const auditGroupElem = this.renderAuditGroup(groupDef);
|
|
2063
|
+
const [auditGroupElem, auditGroupFooterEl] = this.renderAuditGroup(groupDef);
|
|
1961
2064
|
for (const auditRef of groupAuditRefs) {
|
|
1962
|
-
auditGroupElem.
|
|
2065
|
+
auditGroupElem.insertBefore(this.renderAudit(auditRef), auditGroupFooterEl);
|
|
1963
2066
|
}
|
|
1964
2067
|
auditGroupElem.classList.add(`lh-audit-group--${groupId}`);
|
|
1965
2068
|
auditElements.push(auditGroupElem);
|
|
@@ -1997,17 +2100,9 @@ class CategoryRenderer {
|
|
|
1997
2100
|
clumpElement.setAttribute('open', '');
|
|
1998
2101
|
}
|
|
1999
2102
|
|
|
2000
|
-
const summaryInnerEl = this.dom.find('div.lh-audit-group__summary', clumpElement);
|
|
2001
|
-
summaryInnerEl.appendChild(this._createChevron());
|
|
2002
|
-
|
|
2003
2103
|
const headerEl = this.dom.find('.lh-audit-group__header', clumpElement);
|
|
2004
2104
|
const title = this._clumpTitles[clumpId];
|
|
2005
2105
|
this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
|
|
2006
|
-
if (description) {
|
|
2007
|
-
const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
|
|
2008
|
-
descriptionEl.classList.add('lh-audit-group__description');
|
|
2009
|
-
headerEl.appendChild(descriptionEl);
|
|
2010
|
-
}
|
|
2011
2106
|
|
|
2012
2107
|
const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
|
|
2013
2108
|
itemCountEl.textContent = `(${auditRefs.length})`;
|
|
@@ -2016,8 +2111,18 @@ class CategoryRenderer {
|
|
|
2016
2111
|
const auditElements = auditRefs.map(this.renderAudit.bind(this));
|
|
2017
2112
|
clumpElement.append(...auditElements);
|
|
2018
2113
|
|
|
2114
|
+
const el = this.dom.find('.lh-audit-group', clumpComponent);
|
|
2115
|
+
if (description) {
|
|
2116
|
+
const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
|
|
2117
|
+
descriptionEl.classList.add('lh-audit-group__description', 'lh-audit-group__footer');
|
|
2118
|
+
el.appendChild(descriptionEl);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
this.dom.find('.lh-clump-toggletext--show', el).textContent = Util.i18n.strings.show;
|
|
2122
|
+
this.dom.find('.lh-clump-toggletext--hide', el).textContent = Util.i18n.strings.hide;
|
|
2123
|
+
|
|
2019
2124
|
clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`);
|
|
2020
|
-
return
|
|
2125
|
+
return el;
|
|
2021
2126
|
}
|
|
2022
2127
|
|
|
2023
2128
|
/**
|
|
@@ -2027,7 +2132,7 @@ class CategoryRenderer {
|
|
|
2027
2132
|
* @return {DocumentFragment}
|
|
2028
2133
|
*/
|
|
2029
2134
|
renderCategoryScore(category, groupDefinitions, options) {
|
|
2030
|
-
if (options && (options.gatherMode
|
|
2135
|
+
if (options && Util.shouldDisplayAsFraction(options.gatherMode)) {
|
|
2031
2136
|
return this.renderCategoryFraction(category);
|
|
2032
2137
|
}
|
|
2033
2138
|
return this.renderScoreGauge(category, groupDefinitions);
|
|
@@ -2041,7 +2146,6 @@ class CategoryRenderer {
|
|
|
2041
2146
|
renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars
|
|
2042
2147
|
const tmpl = this.dom.createComponent('gauge');
|
|
2043
2148
|
const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl);
|
|
2044
|
-
this.dom.safelySetHref(wrapper, `#${category.id}`);
|
|
2045
2149
|
|
|
2046
2150
|
if (Util.isPluginCategory(category.id)) {
|
|
2047
2151
|
wrapper.classList.add('lh-gauge__wrapper--plugin');
|
|
@@ -2082,21 +2186,13 @@ class CategoryRenderer {
|
|
|
2082
2186
|
renderCategoryFraction(category) {
|
|
2083
2187
|
const tmpl = this.dom.createComponent('fraction');
|
|
2084
2188
|
const wrapper = this.dom.find('a.lh-fraction__wrapper', tmpl);
|
|
2085
|
-
this.dom.safelySetHref(wrapper, `#${category.id}`);
|
|
2086
2189
|
|
|
2087
|
-
const
|
|
2190
|
+
const {numPassed, numPassableAudits, totalWeight} = Util.calculateCategoryFraction(category);
|
|
2088
2191
|
|
|
2089
|
-
|
|
2090
|
-
let totalWeight = 0;
|
|
2091
|
-
for (const auditRef of category.auditRefs) {
|
|
2092
|
-
totalWeight += auditRef.weight;
|
|
2093
|
-
if (Util.showAsPassed(auditRef.result)) numPassed++;
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
const fraction = numPassed / numAudits;
|
|
2192
|
+
const fraction = numPassed / numPassableAudits;
|
|
2097
2193
|
const content = this.dom.find('.lh-fraction__content', tmpl);
|
|
2098
2194
|
const text = this.dom.createElement('span');
|
|
2099
|
-
text.textContent = `${numPassed}/${
|
|
2195
|
+
text.textContent = `${numPassed}/${numPassableAudits}`;
|
|
2100
2196
|
content.appendChild(text);
|
|
2101
2197
|
|
|
2102
2198
|
let rating = Util.calculateRating(fraction);
|
|
@@ -2195,12 +2291,12 @@ class CategoryRenderer {
|
|
|
2195
2291
|
* ⋮
|
|
2196
2292
|
* @param {LH.ReportResult.Category} category
|
|
2197
2293
|
* @param {Object<string, LH.Result.ReportGroup>=} groupDefinitions
|
|
2198
|
-
* @param {{
|
|
2294
|
+
* @param {{gatherMode: LH.Result.GatherMode}=} options
|
|
2199
2295
|
* @return {Element}
|
|
2200
2296
|
*/
|
|
2201
2297
|
render(category, groupDefinitions = {}, options) {
|
|
2202
2298
|
const element = this.dom.createElement('div', 'lh-category');
|
|
2203
|
-
|
|
2299
|
+
element.id = category.id;
|
|
2204
2300
|
element.appendChild(this.renderCategoryHeader(category, groupDefinitions, options));
|
|
2205
2301
|
|
|
2206
2302
|
// Top level clumps for audits, in order they will appear in the report.
|
|
@@ -2220,6 +2316,13 @@ class CategoryRenderer {
|
|
|
2220
2316
|
clumps.set(clumpId, clump);
|
|
2221
2317
|
}
|
|
2222
2318
|
|
|
2319
|
+
// Sort audits by weight.
|
|
2320
|
+
for (const auditRefs of clumps.values()) {
|
|
2321
|
+
auditRefs.sort((a, b) => {
|
|
2322
|
+
return b.weight - a.weight;
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2223
2326
|
// Render each clump.
|
|
2224
2327
|
for (const [clumpId, auditRefs] of clumps) {
|
|
2225
2328
|
if (auditRefs.length === 0) continue;
|
|
@@ -2238,16 +2341,6 @@ class CategoryRenderer {
|
|
|
2238
2341
|
|
|
2239
2342
|
return element;
|
|
2240
2343
|
}
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* Create a non-semantic span used for hash navigation of categories
|
|
2244
|
-
* @param {Element} element
|
|
2245
|
-
* @param {string} id
|
|
2246
|
-
*/
|
|
2247
|
-
createPermalinkSpan(element, id) {
|
|
2248
|
-
const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink');
|
|
2249
|
-
permalinkEl.id = id;
|
|
2250
|
-
}
|
|
2251
2344
|
}
|
|
2252
2345
|
|
|
2253
2346
|
/**
|
|
@@ -2267,6 +2360,18 @@ class CategoryRenderer {
|
|
|
2267
2360
|
* limitations under the License.
|
|
2268
2361
|
*/
|
|
2269
2362
|
|
|
2363
|
+
/** @typedef {import('./dom.js').DOM} DOM */
|
|
2364
|
+
/** @typedef {import('./details-renderer.js').DetailsRenderer} DetailsRenderer */
|
|
2365
|
+
/**
|
|
2366
|
+
* @typedef CRCSegment
|
|
2367
|
+
* @property {LH.Audit.Details.SimpleCriticalRequestNode[string]} node
|
|
2368
|
+
* @property {boolean} isLastChild
|
|
2369
|
+
* @property {boolean} hasChildren
|
|
2370
|
+
* @property {number} startTime
|
|
2371
|
+
* @property {number} transferSize
|
|
2372
|
+
* @property {boolean[]} treeMarkers
|
|
2373
|
+
*/
|
|
2374
|
+
|
|
2270
2375
|
class CriticalRequestChainRenderer {
|
|
2271
2376
|
/**
|
|
2272
2377
|
* Create render context for critical-request-chain tree display.
|
|
@@ -2432,16 +2537,6 @@ class CriticalRequestChainRenderer {
|
|
|
2432
2537
|
// Alias b/c the name is really long.
|
|
2433
2538
|
const CRCRenderer = CriticalRequestChainRenderer;
|
|
2434
2539
|
|
|
2435
|
-
/** @typedef {{
|
|
2436
|
-
node: LH.Audit.Details.SimpleCriticalRequestNode[string],
|
|
2437
|
-
isLastChild: boolean,
|
|
2438
|
-
hasChildren: boolean,
|
|
2439
|
-
startTime: number,
|
|
2440
|
-
transferSize: number,
|
|
2441
|
-
treeMarkers: Array<boolean>
|
|
2442
|
-
}} CRCSegment
|
|
2443
|
-
*/
|
|
2444
|
-
|
|
2445
2540
|
/**
|
|
2446
2541
|
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
|
|
2447
2542
|
* 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
|
|
@@ -2824,7 +2919,7 @@ function clamp(value, min, max) {
|
|
|
2824
2919
|
/**
|
|
2825
2920
|
* @param {Rect} rect
|
|
2826
2921
|
*/
|
|
2827
|
-
function
|
|
2922
|
+
function getElementRectCenterPoint(rect) {
|
|
2828
2923
|
return {
|
|
2829
2924
|
x: rect.left + rect.width / 2,
|
|
2830
2925
|
y: rect.top + rect.height / 2,
|
|
@@ -2841,7 +2936,7 @@ class ElementScreenshotRenderer {
|
|
|
2841
2936
|
* @param {Size} screenshotSize
|
|
2842
2937
|
*/
|
|
2843
2938
|
static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) {
|
|
2844
|
-
const elementRectCenter =
|
|
2939
|
+
const elementRectCenter = getElementRectCenterPoint(elementRectSC);
|
|
2845
2940
|
|
|
2846
2941
|
// Try to center clipped region.
|
|
2847
2942
|
const screenshotLeftVisibleEdge = clamp(
|
|
@@ -2908,7 +3003,7 @@ class ElementScreenshotRenderer {
|
|
|
2908
3003
|
* @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
|
|
2909
3004
|
*/
|
|
2910
3005
|
static installFullPageScreenshot(el, screenshot) {
|
|
2911
|
-
el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`);
|
|
3006
|
+
el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
|
|
2912
3007
|
}
|
|
2913
3008
|
|
|
2914
3009
|
/**
|
|
@@ -2916,14 +3011,14 @@ class ElementScreenshotRenderer {
|
|
|
2916
3011
|
* @param {InstallOverlayFeatureParams} opts
|
|
2917
3012
|
*/
|
|
2918
3013
|
static installOverlayFeature(opts) {
|
|
2919
|
-
const {dom,
|
|
3014
|
+
const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts;
|
|
2920
3015
|
const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
|
|
2921
3016
|
// Don't install the feature more than once.
|
|
2922
|
-
if (
|
|
2923
|
-
|
|
3017
|
+
if (rootEl.classList.contains(screenshotOverlayClass)) return;
|
|
3018
|
+
rootEl.classList.add(screenshotOverlayClass);
|
|
2924
3019
|
|
|
2925
3020
|
// Add a single listener to the provided element to handle all clicks within (event delegation).
|
|
2926
|
-
|
|
3021
|
+
rootEl.addEventListener('click', e => {
|
|
2927
3022
|
const target = /** @type {?HTMLElement} */ (e.target);
|
|
2928
3023
|
if (!target) return;
|
|
2929
3024
|
// Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
|
|
@@ -3994,53 +4089,75 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3994
4089
|
return url.href;
|
|
3995
4090
|
}
|
|
3996
4091
|
|
|
4092
|
+
/**
|
|
4093
|
+
* For performance, audits with no group should be a diagnostic or opportunity.
|
|
4094
|
+
* The audit details type will determine which of the two groups an audit is in.
|
|
4095
|
+
*
|
|
4096
|
+
* @param {LH.ReportResult.AuditRef} audit
|
|
4097
|
+
* @return {'load-opportunity'|'diagnostic'|null}
|
|
4098
|
+
*/
|
|
4099
|
+
_classifyPerformanceAudit(audit) {
|
|
4100
|
+
if (audit.group) return null;
|
|
4101
|
+
if (audit.result.details && audit.result.details.type === 'opportunity') {
|
|
4102
|
+
return 'load-opportunity';
|
|
4103
|
+
}
|
|
4104
|
+
return 'diagnostic';
|
|
4105
|
+
}
|
|
4106
|
+
|
|
3997
4107
|
/**
|
|
3998
4108
|
* @param {LH.ReportResult.Category} category
|
|
3999
4109
|
* @param {Object<string, LH.Result.ReportGroup>} groups
|
|
4000
|
-
* @param {{gatherMode: LH.Result.GatherMode
|
|
4110
|
+
* @param {{gatherMode: LH.Result.GatherMode}=} options
|
|
4001
4111
|
* @return {Element}
|
|
4002
4112
|
* @override
|
|
4003
4113
|
*/
|
|
4004
4114
|
render(category, groups, options) {
|
|
4005
4115
|
const strings = Util.i18n.strings;
|
|
4006
4116
|
const element = this.dom.createElement('div', 'lh-category');
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
gaugeEl.appendChild(this.renderCategoryScore(category, groups, options));
|
|
4010
|
-
element.appendChild(gaugeEl);
|
|
4011
|
-
} else {
|
|
4012
|
-
this.createPermalinkSpan(element, category.id);
|
|
4013
|
-
element.appendChild(this.renderCategoryHeader(category, groups, options));
|
|
4014
|
-
}
|
|
4117
|
+
element.id = category.id;
|
|
4118
|
+
element.appendChild(this.renderCategoryHeader(category, groups, options));
|
|
4015
4119
|
|
|
4016
4120
|
// Metrics.
|
|
4017
|
-
const metricAuditsEl = this.renderAuditGroup(groups.metrics);
|
|
4018
|
-
|
|
4019
|
-
// Metric descriptions toggle.
|
|
4020
|
-
const toggleTmpl = this.dom.createComponent('metricsToggle');
|
|
4021
|
-
const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
|
|
4022
|
-
metricAuditsEl.append(..._toggleEl.childNodes);
|
|
4023
|
-
|
|
4024
4121
|
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4122
|
+
if (metricAudits.length) {
|
|
4123
|
+
const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
|
|
4124
|
+
|
|
4125
|
+
// Metric descriptions toggle.
|
|
4126
|
+
const checkboxEl = this.dom.createElement('input', 'lh-metrics-toggle__input');
|
|
4127
|
+
const checkboxId = `lh-metrics-toggle${Util.getUniqueSuffix()}`;
|
|
4128
|
+
checkboxEl.setAttribute('aria-label', 'Toggle the display of metric descriptions');
|
|
4129
|
+
checkboxEl.type = 'checkbox';
|
|
4130
|
+
checkboxEl.id = checkboxId;
|
|
4131
|
+
metricsGroupEl.prepend(checkboxEl);
|
|
4132
|
+
const metricHeaderEl = this.dom.find('.lh-audit-group__header', metricsGroupEl);
|
|
4133
|
+
const labelEl = this.dom.createChildOf(metricHeaderEl, 'label', 'lh-metrics-toggle__label');
|
|
4134
|
+
labelEl.htmlFor = checkboxId;
|
|
4135
|
+
const showEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--show');
|
|
4136
|
+
const hideEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--hide');
|
|
4137
|
+
showEl.textContent = Util.i18n.strings.expandView;
|
|
4138
|
+
hideEl.textContent = Util.i18n.strings.collapseView;
|
|
4139
|
+
|
|
4140
|
+
const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
|
|
4141
|
+
metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
|
|
4142
|
+
metricAudits.forEach(item => {
|
|
4143
|
+
metricsBoxesEl.appendChild(this._renderMetric(item));
|
|
4144
|
+
});
|
|
4030
4145
|
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4146
|
+
const descriptionEl = this.dom.find('.lh-category-header__description', element);
|
|
4147
|
+
const estValuesEl = this.dom.createChildOf(descriptionEl, 'div', 'lh-metrics__disclaimer');
|
|
4148
|
+
const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
|
|
4149
|
+
estValuesEl.appendChild(disclaimerEl);
|
|
4034
4150
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4151
|
+
// Add link to score calculator.
|
|
4152
|
+
const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
|
|
4153
|
+
calculatorLink.target = '_blank';
|
|
4154
|
+
calculatorLink.textContent = strings.calculatorLink;
|
|
4155
|
+
this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
|
|
4040
4156
|
|
|
4041
4157
|
|
|
4042
|
-
|
|
4043
|
-
|
|
4158
|
+
metricsGroupEl.classList.add('lh-audit-group--metrics');
|
|
4159
|
+
element.appendChild(metricsGroupEl);
|
|
4160
|
+
}
|
|
4044
4161
|
|
|
4045
4162
|
// Filmstrip
|
|
4046
4163
|
const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
|
|
@@ -4054,10 +4171,10 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4054
4171
|
|
|
4055
4172
|
// Opportunities
|
|
4056
4173
|
const opportunityAudits = category.auditRefs
|
|
4057
|
-
.filter(audit => audit
|
|
4174
|
+
.filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
|
|
4175
|
+
.filter(audit => !Util.showAsPassed(audit.result))
|
|
4058
4176
|
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
|
|
4059
4177
|
|
|
4060
|
-
|
|
4061
4178
|
const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
|
|
4062
4179
|
// TODO: only add if there are opportunities & diagnostics rendered.
|
|
4063
4180
|
if (filterableMetrics.length) {
|
|
@@ -4070,7 +4187,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4070
4187
|
const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
|
|
4071
4188
|
const maxWaste = Math.max(...wastedMsValues);
|
|
4072
4189
|
const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
|
|
4073
|
-
const groupEl = this.renderAuditGroup(groups['load-opportunities']);
|
|
4190
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
|
|
4074
4191
|
const tmpl = this.dom.createComponent('opportunityHeader');
|
|
4075
4192
|
|
|
4076
4193
|
this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
|
|
@@ -4079,15 +4196,17 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4079
4196
|
strings.opportunitySavingsColumnLabel;
|
|
4080
4197
|
|
|
4081
4198
|
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
|
|
4082
|
-
groupEl.
|
|
4083
|
-
opportunityAudits.forEach(item =>
|
|
4199
|
+
groupEl.insertBefore(headerEl, footerEl);
|
|
4200
|
+
opportunityAudits.forEach(item =>
|
|
4201
|
+
groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
|
|
4084
4202
|
groupEl.classList.add('lh-audit-group--load-opportunities');
|
|
4085
4203
|
element.appendChild(groupEl);
|
|
4086
4204
|
}
|
|
4087
4205
|
|
|
4088
4206
|
// Diagnostics
|
|
4089
4207
|
const diagnosticAudits = category.auditRefs
|
|
4090
|
-
.filter(audit => audit
|
|
4208
|
+
.filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
|
|
4209
|
+
.filter(audit => !Util.showAsPassed(audit.result))
|
|
4091
4210
|
.sort((a, b) => {
|
|
4092
4211
|
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
|
|
4093
4212
|
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
|
|
@@ -4095,16 +4214,15 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4095
4214
|
});
|
|
4096
4215
|
|
|
4097
4216
|
if (diagnosticAudits.length) {
|
|
4098
|
-
const groupEl = this.renderAuditGroup(groups['diagnostics']);
|
|
4099
|
-
diagnosticAudits.forEach(item => groupEl.
|
|
4217
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
|
|
4218
|
+
diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
|
|
4100
4219
|
groupEl.classList.add('lh-audit-group--diagnostics');
|
|
4101
4220
|
element.appendChild(groupEl);
|
|
4102
4221
|
}
|
|
4103
4222
|
|
|
4104
4223
|
// Passed audits
|
|
4105
4224
|
const passedAudits = category.auditRefs
|
|
4106
|
-
.filter(audit => (audit
|
|
4107
|
-
Util.showAsPassed(audit.result));
|
|
4225
|
+
.filter(audit => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
|
|
4108
4226
|
|
|
4109
4227
|
if (!passedAudits.length) return element;
|
|
4110
4228
|
|
|
@@ -4124,16 +4242,16 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4124
4242
|
const table = this.detailsRenderer.render(audit.result.details);
|
|
4125
4243
|
if (table) {
|
|
4126
4244
|
table.id = id;
|
|
4127
|
-
table.classList.add('lh-audit');
|
|
4245
|
+
table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
|
|
4128
4246
|
budgetTableEls.push(table);
|
|
4129
4247
|
}
|
|
4130
4248
|
}
|
|
4131
4249
|
});
|
|
4132
4250
|
if (budgetTableEls.length > 0) {
|
|
4133
|
-
const
|
|
4134
|
-
budgetTableEls.forEach(table =>
|
|
4135
|
-
|
|
4136
|
-
element.appendChild(
|
|
4251
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups.budgets);
|
|
4252
|
+
budgetTableEls.forEach(table => groupEl.insertBefore(table, footerEl));
|
|
4253
|
+
groupEl.classList.add('lh-audit-group--budgets');
|
|
4254
|
+
element.appendChild(groupEl);
|
|
4137
4255
|
}
|
|
4138
4256
|
|
|
4139
4257
|
return element;
|
|
@@ -4232,7 +4350,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4232
4350
|
*/
|
|
4233
4351
|
render(category, groupDefinitions = {}) {
|
|
4234
4352
|
const categoryElem = this.dom.createElement('div', 'lh-category');
|
|
4235
|
-
|
|
4353
|
+
categoryElem.id = category.id;
|
|
4236
4354
|
categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions));
|
|
4237
4355
|
|
|
4238
4356
|
const auditRefs = category.auditRefs;
|
|
@@ -4252,16 +4370,6 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4252
4370
|
return categoryElem;
|
|
4253
4371
|
}
|
|
4254
4372
|
|
|
4255
|
-
/**
|
|
4256
|
-
* Alias for backcompat.
|
|
4257
|
-
* @param {LH.ReportResult.Category} category
|
|
4258
|
-
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
|
|
4259
|
-
* @return {DocumentFragment}
|
|
4260
|
-
*/
|
|
4261
|
-
renderScoreGauge(category, groupDefinitions) {
|
|
4262
|
-
return this.renderCategoryScore(category, groupDefinitions);
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
4373
|
/**
|
|
4266
4374
|
* @param {LH.ReportResult.Category} category
|
|
4267
4375
|
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
|
|
@@ -4275,7 +4383,6 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4275
4383
|
|
|
4276
4384
|
const tmpl = this.dom.createComponent('gaugePwa');
|
|
4277
4385
|
const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
|
|
4278
|
-
this.dom.safelySetHref(wrapper, `#${category.id}`);
|
|
4279
4386
|
|
|
4280
4387
|
// Correct IDs in case multiple instances end up in the page.
|
|
4281
4388
|
const svgRoot = tmpl.querySelector('svg');
|
|
@@ -4428,22 +4535,43 @@ class ReportRenderer {
|
|
|
4428
4535
|
constructor(dom) {
|
|
4429
4536
|
/** @type {DOM} */
|
|
4430
4537
|
this._dom = dom;
|
|
4538
|
+
/** @type {LH.Renderer.Options} */
|
|
4539
|
+
this._opts = {};
|
|
4431
4540
|
}
|
|
4432
4541
|
|
|
4433
4542
|
/**
|
|
4434
4543
|
* @param {LH.Result} lhr
|
|
4435
|
-
* @param {
|
|
4544
|
+
* @param {HTMLElement?} rootEl Report root element containing the report
|
|
4545
|
+
* @param {LH.Renderer.Options=} opts
|
|
4436
4546
|
* @return {!Element}
|
|
4437
4547
|
*/
|
|
4438
|
-
renderReport(lhr,
|
|
4548
|
+
renderReport(lhr, rootEl, opts) {
|
|
4549
|
+
// Allow legacy report rendering API
|
|
4550
|
+
if (!this._dom.rootEl && rootEl) {
|
|
4551
|
+
console.warn('Please adopt the new report API in renderer/api.js.');
|
|
4552
|
+
const closestRoot = rootEl.closest('.lh-root');
|
|
4553
|
+
if (closestRoot) {
|
|
4554
|
+
this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot);
|
|
4555
|
+
} else {
|
|
4556
|
+
rootEl.classList.add('lh-root', 'lh-vars');
|
|
4557
|
+
this._dom.rootEl = rootEl;
|
|
4558
|
+
}
|
|
4559
|
+
} else if (this._dom.rootEl && rootEl) {
|
|
4560
|
+
// Handle legacy flow-report case
|
|
4561
|
+
this._dom.rootEl = rootEl;
|
|
4562
|
+
}
|
|
4563
|
+
if (opts) {
|
|
4564
|
+
this._opts = opts;
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4439
4567
|
this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown');
|
|
4440
4568
|
|
|
4441
4569
|
const report = Util.prepareReportResult(lhr);
|
|
4442
4570
|
|
|
4443
|
-
|
|
4444
|
-
|
|
4571
|
+
this._dom.rootEl.textContent = ''; // Remove previous report.
|
|
4572
|
+
this._dom.rootEl.appendChild(this._renderReport(report));
|
|
4445
4573
|
|
|
4446
|
-
return
|
|
4574
|
+
return this._dom.rootEl;
|
|
4447
4575
|
}
|
|
4448
4576
|
|
|
4449
4577
|
/**
|
|
@@ -4477,44 +4605,64 @@ class ReportRenderer {
|
|
|
4477
4605
|
_renderReportFooter(report) {
|
|
4478
4606
|
const footer = this._dom.createComponent('footer');
|
|
4479
4607
|
|
|
4480
|
-
|
|
4481
|
-
env.id = 'runtime-settings';
|
|
4482
|
-
this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
|
|
4483
|
-
|
|
4484
|
-
const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
|
|
4485
|
-
const runtimeValues = [
|
|
4486
|
-
{name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
|
|
4487
|
-
{name: Util.i18n.strings.runtimeSettingsFetchTime,
|
|
4488
|
-
description: Util.i18n.formatDateTime(report.fetchTime)},
|
|
4489
|
-
...envValues,
|
|
4490
|
-
{name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
|
|
4491
|
-
{name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
|
|
4492
|
-
{name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
|
|
4493
|
-
report.environment.networkUserAgent},
|
|
4494
|
-
{name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
|
|
4495
|
-
report.environment.benchmarkIndex.toFixed(0)},
|
|
4496
|
-
];
|
|
4497
|
-
if (report.environment.credits && report.environment.credits['axe-core']) {
|
|
4498
|
-
runtimeValues.push({
|
|
4499
|
-
name: Util.i18n.strings.runtimeSettingsAxeVersion,
|
|
4500
|
-
description: report.environment.credits['axe-core'],
|
|
4501
|
-
});
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
|
-
for (const runtime of runtimeValues) {
|
|
4505
|
-
if (!runtime.description) continue;
|
|
4506
|
-
|
|
4507
|
-
const item = this._dom.createComponent('envItem');
|
|
4508
|
-
this._dom.find('.lh-env__name', item).textContent = runtime.name;
|
|
4509
|
-
this._dom.find('.lh-env__description', item).textContent = runtime.description;
|
|
4510
|
-
env.appendChild(item);
|
|
4511
|
-
}
|
|
4608
|
+
this._renderMetaBlock(report, footer);
|
|
4512
4609
|
|
|
4513
4610
|
this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
|
|
4514
4611
|
this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
|
|
4515
4612
|
return footer;
|
|
4516
4613
|
}
|
|
4517
4614
|
|
|
4615
|
+
/**
|
|
4616
|
+
* @param {LH.ReportResult} report
|
|
4617
|
+
* @param {DocumentFragment} footer
|
|
4618
|
+
*/
|
|
4619
|
+
_renderMetaBlock(report, footer) {
|
|
4620
|
+
const envValues = Util.getEmulationDescriptions(report.configSettings || {});
|
|
4621
|
+
|
|
4622
|
+
|
|
4623
|
+
const match = report.userAgent.match(/(\w*Chrome\/[\d.]+)/); // \w* to include 'HeadlessChrome'
|
|
4624
|
+
const chromeVer = Array.isArray(match)
|
|
4625
|
+
? match[1].replace('/', ' ').replace('Chrome', 'Chromium')
|
|
4626
|
+
: 'Chromium';
|
|
4627
|
+
const channel = report.configSettings.channel;
|
|
4628
|
+
const benchmarkIndex = report.environment.benchmarkIndex.toFixed(0);
|
|
4629
|
+
const axeVersion = report.environment.credits?.['axe-core'];
|
|
4630
|
+
|
|
4631
|
+
// [CSS icon class, textContent, tooltipText]
|
|
4632
|
+
const metaItems = [
|
|
4633
|
+
['date',
|
|
4634
|
+
`Captured at ${Util.i18n.formatDateTime(report.fetchTime)}`],
|
|
4635
|
+
['devices',
|
|
4636
|
+
`${envValues.deviceEmulation} with Lighthouse ${report.lighthouseVersion}`,
|
|
4637
|
+
`${Util.i18n.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}` +
|
|
4638
|
+
`\n${Util.i18n.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}` +
|
|
4639
|
+
(axeVersion ? `\n${Util.i18n.strings.runtimeSettingsAxeVersion}: ${axeVersion}` : '')],
|
|
4640
|
+
['samples-one',
|
|
4641
|
+
Util.i18n.strings.runtimeSingleLoad,
|
|
4642
|
+
Util.i18n.strings.runtimeSingleLoadTooltip],
|
|
4643
|
+
['stopwatch',
|
|
4644
|
+
Util.i18n.strings.runtimeAnalysisWindow],
|
|
4645
|
+
['networkspeed',
|
|
4646
|
+
`${envValues.summary}`,
|
|
4647
|
+
`${Util.i18n.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
|
|
4648
|
+
['chrome',
|
|
4649
|
+
`Using ${chromeVer}` + (channel ? ` with ${channel}` : ''),
|
|
4650
|
+
`${Util.i18n.strings.runtimeSettingsUANetwork}: "${report.environment.networkUserAgent}"`],
|
|
4651
|
+
];
|
|
4652
|
+
|
|
4653
|
+
const metaItemsEl = this._dom.find('.lh-meta__items', footer);
|
|
4654
|
+
for (const [iconname, text, tooltip] of metaItems) {
|
|
4655
|
+
const itemEl = this._dom.createChildOf(metaItemsEl, 'li', 'lh-meta__item');
|
|
4656
|
+
itemEl.textContent = text;
|
|
4657
|
+
if (tooltip) {
|
|
4658
|
+
itemEl.classList.add('lh-tooltip-boundary');
|
|
4659
|
+
const tooltipEl = this._dom.createChildOf(itemEl, 'div', 'lh-tooltip');
|
|
4660
|
+
tooltipEl.textContent = tooltip;
|
|
4661
|
+
}
|
|
4662
|
+
itemEl.classList.add('lh-report-icon', `lh-report-icon--${iconname}`);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4518
4666
|
/**
|
|
4519
4667
|
* Returns a div with a list of top-level warnings, or an empty div if no warnings.
|
|
4520
4668
|
* @param {LH.ReportResult} report
|
|
@@ -4558,13 +4706,33 @@ class ReportRenderer {
|
|
|
4558
4706
|
{gatherMode: report.gatherMode}
|
|
4559
4707
|
);
|
|
4560
4708
|
|
|
4709
|
+
const gaugeWrapperEl = this._dom.find('a.lh-gauge__wrapper, a.lh-fraction__wrapper',
|
|
4710
|
+
categoryGauge);
|
|
4711
|
+
if (gaugeWrapperEl) {
|
|
4712
|
+
this._dom.safelySetHref(gaugeWrapperEl, `#${category.id}`);
|
|
4713
|
+
// Handle navigation clicks by scrolling to target without changing the page's URL.
|
|
4714
|
+
// Why? Some report embedding clients have their own routing and updating the location.hash
|
|
4715
|
+
// can introduce problems. Others may have an unpredictable `<base>` URL which ensures
|
|
4716
|
+
// navigation to `${baseURL}#categoryid` will be unintended.
|
|
4717
|
+
gaugeWrapperEl.addEventListener('click', e => {
|
|
4718
|
+
if (!gaugeWrapperEl.matches('[href^="#"]')) return;
|
|
4719
|
+
const selector = gaugeWrapperEl.getAttribute('href');
|
|
4720
|
+
const reportRoot = this._dom.rootEl;
|
|
4721
|
+
if (!selector || !reportRoot) return;
|
|
4722
|
+
const destEl = this._dom.find(selector, reportRoot);
|
|
4723
|
+
e.preventDefault();
|
|
4724
|
+
destEl.scrollIntoView();
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
|
|
4561
4729
|
if (Util.isPluginCategory(category.id)) {
|
|
4562
4730
|
pluginGauges.push(categoryGauge);
|
|
4563
4731
|
} else if (renderer.renderCategoryScore === categoryRenderer.renderCategoryScore) {
|
|
4564
4732
|
// The renderer for default categories is just the default CategoryRenderer.
|
|
4565
4733
|
// If the functions are equal, then renderer is an instance of CategoryRenderer.
|
|
4566
4734
|
// For example, the PWA category uses PwaCategoryRenderer, which overrides
|
|
4567
|
-
// CategoryRenderer.
|
|
4735
|
+
// CategoryRenderer.renderCategoryScore, so it would fail this check and be placed
|
|
4568
4736
|
// in the customGauges bucket.
|
|
4569
4737
|
defaultGauges.push(categoryGauge);
|
|
4570
4738
|
} else {
|
|
@@ -4619,8 +4787,10 @@ class ReportRenderer {
|
|
|
4619
4787
|
headerContainer.classList.add('lh-header--solo-category');
|
|
4620
4788
|
}
|
|
4621
4789
|
|
|
4790
|
+
const scoreScale = this._dom.createElement('div');
|
|
4791
|
+
scoreScale.classList.add('lh-scorescale-wrap');
|
|
4792
|
+
scoreScale.append(this._dom.createComponent('scorescale'));
|
|
4622
4793
|
if (scoreHeader) {
|
|
4623
|
-
const scoreScale = this._dom.createComponent('scorescale');
|
|
4624
4794
|
const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
|
|
4625
4795
|
scoreHeader.append(
|
|
4626
4796
|
...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
|
|
@@ -4638,7 +4808,7 @@ class ReportRenderer {
|
|
|
4638
4808
|
for (const category of Object.values(report.categories)) {
|
|
4639
4809
|
const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
|
|
4640
4810
|
// .lh-category-wrapper is full-width and provides horizontal rules between categories.
|
|
4641
|
-
// .lh-category within has the max-width: var(--report-width);
|
|
4811
|
+
// .lh-category within has the max-width: var(--report-content-width);
|
|
4642
4812
|
const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
|
|
4643
4813
|
wrapper.appendChild(renderer.render(
|
|
4644
4814
|
category,
|
|
@@ -4647,10 +4817,15 @@ class ReportRenderer {
|
|
|
4647
4817
|
));
|
|
4648
4818
|
}
|
|
4649
4819
|
|
|
4820
|
+
categoryRenderer.injectFinalScreenshot(categories, report.audits, scoreScale);
|
|
4821
|
+
|
|
4650
4822
|
const reportFragment = this._dom.createFragment();
|
|
4651
|
-
|
|
4823
|
+
reportFragment.append(this._dom.createComponent('styles'));
|
|
4824
|
+
|
|
4825
|
+
if (!this._opts.omitTopbar) {
|
|
4826
|
+
reportFragment.appendChild(this._renderReportTopbar(report));
|
|
4827
|
+
}
|
|
4652
4828
|
|
|
4653
|
-
reportFragment.appendChild(topbarDocumentFragment);
|
|
4654
4829
|
reportFragment.appendChild(reportContainer);
|
|
4655
4830
|
reportContainer.appendChild(headerContainer);
|
|
4656
4831
|
reportContainer.appendChild(reportSection);
|
|
@@ -4658,7 +4833,7 @@ class ReportRenderer {
|
|
|
4658
4833
|
|
|
4659
4834
|
if (fullPageScreenshot) {
|
|
4660
4835
|
ElementScreenshotRenderer.installFullPageScreenshot(
|
|
4661
|
-
|
|
4836
|
+
this._dom.rootEl, fullPageScreenshot.screenshot);
|
|
4662
4837
|
}
|
|
4663
4838
|
|
|
4664
4839
|
return reportFragment;
|
|
@@ -4680,7 +4855,7 @@ class ReportRenderer {
|
|
|
4680
4855
|
* @param {boolean} [force]
|
|
4681
4856
|
*/
|
|
4682
4857
|
function toggleDarkTheme(dom, force) {
|
|
4683
|
-
const el = dom.
|
|
4858
|
+
const el = dom.rootEl;
|
|
4684
4859
|
// This seems unnecessary, but in DevTools, passing "undefined" as the second
|
|
4685
4860
|
// parameter acts like passing "false".
|
|
4686
4861
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
|
|
@@ -4937,11 +5112,11 @@ class DropDownMenu {
|
|
|
4937
5112
|
* @param {function(MouseEvent): any} menuClickHandler
|
|
4938
5113
|
*/
|
|
4939
5114
|
setup(menuClickHandler) {
|
|
4940
|
-
this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.
|
|
5115
|
+
this._toggleEl = this._dom.find('.lh-topbar button.lh-tools__button', this._dom.rootEl);
|
|
4941
5116
|
this._toggleEl.addEventListener('click', this.onToggleClick);
|
|
4942
5117
|
this._toggleEl.addEventListener('keydown', this.onToggleKeydown);
|
|
4943
5118
|
|
|
4944
|
-
this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.
|
|
5119
|
+
this._menuEl = this._dom.find('.lh-topbar div.lh-tools__dropdown', this._dom.rootEl);
|
|
4945
5120
|
this._menuEl.addEventListener('keydown', this.onMenuKeydown);
|
|
4946
5121
|
this._menuEl.addEventListener('click', menuClickHandler);
|
|
4947
5122
|
}
|
|
@@ -5129,15 +5304,13 @@ class TopbarFeatures {
|
|
|
5129
5304
|
this.lhr; // eslint-disable-line no-unused-expressions
|
|
5130
5305
|
this._reportUIFeatures = reportUIFeatures;
|
|
5131
5306
|
this._dom = dom;
|
|
5132
|
-
/** @type {Document} */
|
|
5133
|
-
this._document = this._dom.document();
|
|
5134
5307
|
this._dropDownMenu = new DropDownMenu(this._dom);
|
|
5135
5308
|
this._copyAttempt = false;
|
|
5136
5309
|
/** @type {HTMLElement} */
|
|
5137
5310
|
this.topbarEl; // eslint-disable-line no-unused-expressions
|
|
5138
5311
|
/** @type {HTMLElement} */
|
|
5139
|
-
this.
|
|
5140
|
-
/** @type {HTMLElement} */
|
|
5312
|
+
this.categoriesEl; // eslint-disable-line no-unused-expressions
|
|
5313
|
+
/** @type {HTMLElement?} */
|
|
5141
5314
|
this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
|
|
5142
5315
|
/** @type {HTMLElement} */
|
|
5143
5316
|
this.highlightEl; // eslint-disable-line no-unused-expressions
|
|
@@ -5145,7 +5318,6 @@ class TopbarFeatures {
|
|
|
5145
5318
|
this.onKeyUp = this.onKeyUp.bind(this);
|
|
5146
5319
|
this.onCopy = this.onCopy.bind(this);
|
|
5147
5320
|
this.collapseAllDetails = this.collapseAllDetails.bind(this);
|
|
5148
|
-
this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
|
|
5149
5321
|
}
|
|
5150
5322
|
|
|
5151
5323
|
/**
|
|
@@ -5153,33 +5325,15 @@ class TopbarFeatures {
|
|
|
5153
5325
|
*/
|
|
5154
5326
|
enable(lhr) {
|
|
5155
5327
|
this.lhr = lhr;
|
|
5156
|
-
this.
|
|
5157
|
-
this.
|
|
5328
|
+
this._dom.rootEl.addEventListener('keyup', this.onKeyUp);
|
|
5329
|
+
this._dom.document().addEventListener('copy', this.onCopy);
|
|
5158
5330
|
this._dropDownMenu.setup(this.onDropDownMenuClick);
|
|
5159
5331
|
this._setUpCollapseDetailsAfterPrinting();
|
|
5160
5332
|
|
|
5161
|
-
const topbarLogo = this._dom.find('.lh-topbar__logo', this.
|
|
5333
|
+
const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl);
|
|
5162
5334
|
topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom));
|
|
5163
5335
|
|
|
5164
|
-
|
|
5165
|
-
if (Object.keys(this.lhr.categories).length >= 2) {
|
|
5166
|
-
this._setupStickyHeaderElements();
|
|
5167
|
-
const containerEl = this._dom.find('.lh-container', this._document);
|
|
5168
|
-
const elToAddScrollListener = this._getScrollParent(containerEl);
|
|
5169
|
-
elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
|
|
5170
|
-
|
|
5171
|
-
// Use ResizeObserver where available.
|
|
5172
|
-
// TODO: there is an issue with incorrect position numbers and, as a result, performance
|
|
5173
|
-
// issues due to layout thrashing.
|
|
5174
|
-
// See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
|
|
5175
|
-
// For now, limit to DevTools.
|
|
5176
|
-
if (this._dom.isDevTools()) {
|
|
5177
|
-
const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
|
|
5178
|
-
resizeObserver.observe(containerEl);
|
|
5179
|
-
} else {
|
|
5180
|
-
window.addEventListener('resize', this._updateStickyHeaderOnScroll);
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5336
|
+
this._setupStickyHeader();
|
|
5183
5337
|
}
|
|
5184
5338
|
|
|
5185
5339
|
/**
|
|
@@ -5217,7 +5371,7 @@ class TopbarFeatures {
|
|
|
5217
5371
|
try {
|
|
5218
5372
|
this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'}));
|
|
5219
5373
|
} catch (e) {
|
|
5220
|
-
this._dom.fireEventOn('lh-log', this.
|
|
5374
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
5221
5375
|
cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
|
|
5222
5376
|
});
|
|
5223
5377
|
}
|
|
@@ -5257,7 +5411,7 @@ class TopbarFeatures {
|
|
|
5257
5411
|
e.preventDefault();
|
|
5258
5412
|
e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2));
|
|
5259
5413
|
|
|
5260
|
-
this._dom.fireEventOn('lh-log', this.
|
|
5414
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
5261
5415
|
cmd: 'log', msg: 'Report JSON copied to clipboard',
|
|
5262
5416
|
});
|
|
5263
5417
|
}
|
|
@@ -5269,28 +5423,28 @@ class TopbarFeatures {
|
|
|
5269
5423
|
* Copies the report JSON to the clipboard (if supported by the browser).
|
|
5270
5424
|
*/
|
|
5271
5425
|
onCopyButtonClick() {
|
|
5272
|
-
this._dom.fireEventOn('lh-analytics', this.
|
|
5426
|
+
this._dom.fireEventOn('lh-analytics', this._dom.document(), {
|
|
5273
5427
|
cmd: 'send',
|
|
5274
5428
|
fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
|
|
5275
5429
|
});
|
|
5276
5430
|
|
|
5277
5431
|
try {
|
|
5278
|
-
if (this.
|
|
5432
|
+
if (this._dom.document().queryCommandSupported('copy')) {
|
|
5279
5433
|
this._copyAttempt = true;
|
|
5280
5434
|
|
|
5281
5435
|
// Note: In Safari 10.0.1, execCommand('copy') returns true if there's
|
|
5282
5436
|
// a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
|
|
5283
|
-
if (!this.
|
|
5437
|
+
if (!this._dom.document().execCommand('copy')) {
|
|
5284
5438
|
this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
|
|
5285
5439
|
|
|
5286
|
-
this._dom.fireEventOn('lh-log', this.
|
|
5440
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
5287
5441
|
cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
|
|
5288
5442
|
});
|
|
5289
5443
|
}
|
|
5290
5444
|
}
|
|
5291
5445
|
} catch (e) {
|
|
5292
5446
|
this._copyAttempt = false;
|
|
5293
|
-
this._dom.fireEventOn('lh-log', this.
|
|
5447
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {cmd: 'log', msg: e.message});
|
|
5294
5448
|
}
|
|
5295
5449
|
}
|
|
5296
5450
|
|
|
@@ -5311,7 +5465,7 @@ class TopbarFeatures {
|
|
|
5311
5465
|
* open a `<details>` element.
|
|
5312
5466
|
*/
|
|
5313
5467
|
expandAllDetails() {
|
|
5314
|
-
const details = this._dom.findAll('.lh-categories details', this.
|
|
5468
|
+
const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
|
|
5315
5469
|
details.map(detail => detail.open = true);
|
|
5316
5470
|
}
|
|
5317
5471
|
|
|
@@ -5320,7 +5474,7 @@ class TopbarFeatures {
|
|
|
5320
5474
|
* open a `<details>` element.
|
|
5321
5475
|
*/
|
|
5322
5476
|
collapseAllDetails() {
|
|
5323
|
-
const details = this._dom.findAll('.lh-categories details', this.
|
|
5477
|
+
const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
|
|
5324
5478
|
details.map(detail => detail.open = false);
|
|
5325
5479
|
}
|
|
5326
5480
|
|
|
@@ -5340,7 +5494,7 @@ class TopbarFeatures {
|
|
|
5340
5494
|
/**
|
|
5341
5495
|
* Finds the first scrollable ancestor of `element`. Falls back to the document.
|
|
5342
5496
|
* @param {Element} element
|
|
5343
|
-
* @return {
|
|
5497
|
+
* @return {Element | Document}
|
|
5344
5498
|
*/
|
|
5345
5499
|
_getScrollParent(element) {
|
|
5346
5500
|
const {overflowY} = window.getComputedStyle(element);
|
|
@@ -5378,24 +5532,50 @@ class TopbarFeatures {
|
|
|
5378
5532
|
}
|
|
5379
5533
|
}
|
|
5380
5534
|
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
this.
|
|
5384
|
-
this.
|
|
5535
|
+
_setupStickyHeader() {
|
|
5536
|
+
// Cache these elements to avoid qSA on each onscroll.
|
|
5537
|
+
this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl);
|
|
5538
|
+
this.categoriesEl = this._dom.find('div.lh-categories', this._dom.rootEl);
|
|
5539
|
+
|
|
5540
|
+
// Defer behind rAF to avoid forcing layout.
|
|
5541
|
+
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
|
|
5542
|
+
// Only present in the DOM if it'll be used (>=2 categories)
|
|
5543
|
+
try {
|
|
5544
|
+
this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
|
|
5545
|
+
} catch {
|
|
5546
|
+
return;
|
|
5547
|
+
}
|
|
5385
5548
|
|
|
5386
|
-
|
|
5387
|
-
|
|
5549
|
+
// Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
|
|
5550
|
+
this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
|
|
5551
|
+
|
|
5552
|
+
// Update sticky header visibility and highlight when page scrolls/resizes.
|
|
5553
|
+
const scrollParent = this._getScrollParent(
|
|
5554
|
+
this._dom.find('.lh-container', this._dom.rootEl));
|
|
5555
|
+
// The 'scroll' handler must be should be on {Element | Document}...
|
|
5556
|
+
scrollParent.addEventListener('scroll', () => this._updateStickyHeader());
|
|
5557
|
+
// However resizeObserver needs an element, *not* the document.
|
|
5558
|
+
const resizeTarget = scrollParent instanceof window.Document
|
|
5559
|
+
? document.documentElement
|
|
5560
|
+
: scrollParent;
|
|
5561
|
+
new window.ResizeObserver(() => this._updateStickyHeader()).observe(resizeTarget);
|
|
5562
|
+
}));
|
|
5388
5563
|
}
|
|
5389
5564
|
|
|
5390
|
-
|
|
5391
|
-
|
|
5565
|
+
/**
|
|
5566
|
+
* Toggle visibility and update highlighter position
|
|
5567
|
+
*/
|
|
5568
|
+
_updateStickyHeader() {
|
|
5569
|
+
if (!this.stickyHeaderEl) return;
|
|
5570
|
+
|
|
5571
|
+
// Show sticky header when the main 5 gauges clear the topbar.
|
|
5392
5572
|
const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
|
|
5393
|
-
const
|
|
5394
|
-
const showStickyHeader = topbarBottom >=
|
|
5573
|
+
const categoriesTop = this.categoriesEl.getBoundingClientRect().top;
|
|
5574
|
+
const showStickyHeader = topbarBottom >= categoriesTop;
|
|
5395
5575
|
|
|
5396
5576
|
// Highlight mini gauge when section is in view.
|
|
5397
5577
|
// In view = the last category that starts above the middle of the window.
|
|
5398
|
-
const categoryEls = Array.from(this.
|
|
5578
|
+
const categoryEls = Array.from(this._dom.rootEl.querySelectorAll('.lh-category'));
|
|
5399
5579
|
const categoriesAboveTheMiddle =
|
|
5400
5580
|
categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
|
|
5401
5581
|
const highlightIndex =
|
|
@@ -5425,15 +5605,14 @@ class TopbarFeatures {
|
|
|
5425
5605
|
*/
|
|
5426
5606
|
|
|
5427
5607
|
/**
|
|
5428
|
-
* Generate a filenamePrefix of
|
|
5608
|
+
* Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS
|
|
5429
5609
|
* Date/time uses the local timezone, however Node has unreliable ICU
|
|
5430
5610
|
* support, so we must construct a YYYY-MM-DD date format manually. :/
|
|
5431
|
-
* @param {
|
|
5432
|
-
* @
|
|
5611
|
+
* @param {string} name
|
|
5612
|
+
* @param {string|undefined} fetchTime
|
|
5433
5613
|
*/
|
|
5434
|
-
function getFilenamePrefix(
|
|
5435
|
-
const
|
|
5436
|
-
const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
|
|
5614
|
+
function getFilenamePrefix(name, fetchTime) {
|
|
5615
|
+
const date = fetchTime ? new Date(fetchTime) : new Date();
|
|
5437
5616
|
|
|
5438
5617
|
const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
|
|
5439
5618
|
const dateParts = date.toLocaleDateString('en-US', {
|
|
@@ -5443,13 +5622,22 @@ function getFilenamePrefix(lhr) {
|
|
|
5443
5622
|
dateParts.unshift(dateParts.pop());
|
|
5444
5623
|
const dateStr = dateParts.join('-');
|
|
5445
5624
|
|
|
5446
|
-
const filenamePrefix = `${
|
|
5625
|
+
const filenamePrefix = `${name}_${dateStr}_${timeStr}`;
|
|
5447
5626
|
// replace characters that are unfriendly to filenames
|
|
5448
5627
|
return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
|
|
5449
5628
|
}
|
|
5450
5629
|
|
|
5451
|
-
|
|
5452
|
-
|
|
5630
|
+
/**
|
|
5631
|
+
* Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS.
|
|
5632
|
+
* @param {{finalUrl: string, fetchTime: string}} lhr
|
|
5633
|
+
* @return {string}
|
|
5634
|
+
*/
|
|
5635
|
+
function getLhrFilenamePrefix(lhr) {
|
|
5636
|
+
const hostname = new URL(lhr.finalUrl).hostname;
|
|
5637
|
+
return getFilenamePrefix(hostname, lhr.fetchTime);
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
var fileNamer = {getLhrFilenamePrefix, getFilenamePrefix};
|
|
5453
5641
|
|
|
5454
5642
|
/**
|
|
5455
5643
|
* @license
|
|
@@ -5478,16 +5666,17 @@ function getTableRows(tableEl) {
|
|
|
5478
5666
|
class ReportUIFeatures {
|
|
5479
5667
|
/**
|
|
5480
5668
|
* @param {DOM} dom
|
|
5669
|
+
* @param {LH.Renderer.Options} opts
|
|
5481
5670
|
*/
|
|
5482
|
-
constructor(dom) {
|
|
5671
|
+
constructor(dom, opts = {}) {
|
|
5483
5672
|
/** @type {LH.Result} */
|
|
5484
5673
|
this.json; // eslint-disable-line no-unused-expressions
|
|
5485
5674
|
/** @type {DOM} */
|
|
5486
5675
|
this._dom = dom;
|
|
5487
|
-
/** @type {Document} */
|
|
5488
|
-
this._document = this._dom.document();
|
|
5489
|
-
this._topbar = new TopbarFeatures(this, dom);
|
|
5490
5676
|
|
|
5677
|
+
this._opts = opts;
|
|
5678
|
+
|
|
5679
|
+
this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom);
|
|
5491
5680
|
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
|
|
5492
5681
|
}
|
|
5493
5682
|
|
|
@@ -5499,16 +5688,19 @@ class ReportUIFeatures {
|
|
|
5499
5688
|
initFeatures(lhr) {
|
|
5500
5689
|
this.json = lhr;
|
|
5501
5690
|
|
|
5502
|
-
this._topbar
|
|
5503
|
-
|
|
5691
|
+
if (this._topbar) {
|
|
5692
|
+
this._topbar.enable(lhr);
|
|
5693
|
+
this._topbar.resetUIState();
|
|
5694
|
+
}
|
|
5504
5695
|
this._setupMediaQueryListeners();
|
|
5505
5696
|
this._setupThirdPartyFilter();
|
|
5506
|
-
this._setupElementScreenshotOverlay(this._dom.
|
|
5697
|
+
this._setupElementScreenshotOverlay(this._dom.rootEl);
|
|
5507
5698
|
|
|
5508
5699
|
let turnOffTheLights = false;
|
|
5509
5700
|
// Do not query the system preferences for DevTools - DevTools should only apply dark theme
|
|
5510
5701
|
// if dark is selected in the settings panel.
|
|
5511
|
-
|
|
5702
|
+
const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
|
|
5703
|
+
if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
5512
5704
|
turnOffTheLights = true;
|
|
5513
5705
|
}
|
|
5514
5706
|
|
|
@@ -5532,7 +5724,7 @@ class ReportUIFeatures {
|
|
|
5532
5724
|
const hasMetricError = lhr.categories.performance && lhr.categories.performance.auditRefs
|
|
5533
5725
|
.some(audit => Boolean(audit.group === 'metrics' && lhr.audits[audit.id].errorMessage));
|
|
5534
5726
|
if (hasMetricError) {
|
|
5535
|
-
const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this.
|
|
5727
|
+
const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._dom.rootEl);
|
|
5536
5728
|
toggleInputEl.checked = true;
|
|
5537
5729
|
}
|
|
5538
5730
|
|
|
@@ -5547,7 +5739,7 @@ class ReportUIFeatures {
|
|
|
5547
5739
|
}
|
|
5548
5740
|
|
|
5549
5741
|
// Fill in all i18n data.
|
|
5550
|
-
for (const node of this._dom.findAll('[data-i18n]', this._dom.
|
|
5742
|
+
for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) {
|
|
5551
5743
|
// These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
|
|
5552
5744
|
// so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
|
|
5553
5745
|
const i18nKey = node.getAttribute('data-i18n');
|
|
@@ -5557,18 +5749,15 @@ class ReportUIFeatures {
|
|
|
5557
5749
|
}
|
|
5558
5750
|
|
|
5559
5751
|
/**
|
|
5560
|
-
* @param {{
|
|
5752
|
+
* @param {{text: string, icon?: string, onClick: () => void}} opts
|
|
5561
5753
|
*/
|
|
5562
5754
|
addButton(opts) {
|
|
5563
|
-
//
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
|
|
5567
|
-
const containerEl = opts.container || metricsEl;
|
|
5568
|
-
if (!containerEl) return;
|
|
5755
|
+
// Use qSA directly to as we don't want to throw (if this element is missing).
|
|
5756
|
+
const metricsEl = this._dom.rootEl.querySelector('.lh-audit-group--metrics');
|
|
5757
|
+
if (!metricsEl) return;
|
|
5569
5758
|
|
|
5570
|
-
let buttonsEl =
|
|
5571
|
-
if (!buttonsEl) buttonsEl = this._dom.createChildOf(
|
|
5759
|
+
let buttonsEl = metricsEl.querySelector('.lh-buttons');
|
|
5760
|
+
if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons');
|
|
5572
5761
|
|
|
5573
5762
|
const classes = [
|
|
5574
5763
|
'lh-button',
|
|
@@ -5588,8 +5777,10 @@ class ReportUIFeatures {
|
|
|
5588
5777
|
* @return {string}
|
|
5589
5778
|
*/
|
|
5590
5779
|
getReportHtml() {
|
|
5591
|
-
this._topbar
|
|
5592
|
-
|
|
5780
|
+
if (this._topbar) {
|
|
5781
|
+
this._topbar.resetUIState();
|
|
5782
|
+
}
|
|
5783
|
+
return `<!doctype html><body>${this._dom.rootEl.outerHTML}`;
|
|
5593
5784
|
}
|
|
5594
5785
|
|
|
5595
5786
|
/**
|
|
@@ -5601,7 +5792,7 @@ class ReportUIFeatures {
|
|
|
5601
5792
|
}
|
|
5602
5793
|
|
|
5603
5794
|
_enableFireworks() {
|
|
5604
|
-
const scoresContainer = this._dom.find('.lh-scores-container', this.
|
|
5795
|
+
const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl);
|
|
5605
5796
|
scoresContainer.classList.add('lh-score100');
|
|
5606
5797
|
scoresContainer.addEventListener('click', _ => {
|
|
5607
5798
|
scoresContainer.classList.toggle('lh-fireworks-paused');
|
|
@@ -5621,7 +5812,9 @@ class ReportUIFeatures {
|
|
|
5621
5812
|
* be in their closed state (not opened) and the templates should be unstamped.
|
|
5622
5813
|
*/
|
|
5623
5814
|
_resetUIState() {
|
|
5624
|
-
this._topbar
|
|
5815
|
+
if (this._topbar) {
|
|
5816
|
+
this._topbar.resetUIState();
|
|
5817
|
+
}
|
|
5625
5818
|
}
|
|
5626
5819
|
|
|
5627
5820
|
/**
|
|
@@ -5629,8 +5822,7 @@ class ReportUIFeatures {
|
|
|
5629
5822
|
* @param {MediaQueryList|MediaQueryListEvent} mql
|
|
5630
5823
|
*/
|
|
5631
5824
|
onMediaQueryChange(mql) {
|
|
5632
|
-
|
|
5633
|
-
root.classList.toggle('lh-narrow', mql.matches);
|
|
5825
|
+
this._dom.rootEl.classList.toggle('lh-narrow', mql.matches);
|
|
5634
5826
|
}
|
|
5635
5827
|
|
|
5636
5828
|
_setupThirdPartyFilter() {
|
|
@@ -5647,7 +5839,7 @@ class ReportUIFeatures {
|
|
|
5647
5839
|
];
|
|
5648
5840
|
|
|
5649
5841
|
// Get all tables with a text url column.
|
|
5650
|
-
const tables = Array.from(this.
|
|
5842
|
+
const tables = Array.from(this._dom.rootEl.querySelectorAll('table.lh-table'));
|
|
5651
5843
|
const tablesWithUrls = tables
|
|
5652
5844
|
.filter(el =>
|
|
5653
5845
|
el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
|
|
@@ -5694,10 +5886,9 @@ class ReportUIFeatures {
|
|
|
5694
5886
|
const allThirdParty = thirdPartyRows.length === rowEls.length;
|
|
5695
5887
|
const allFirstParty = !thirdPartyRows.length;
|
|
5696
5888
|
|
|
5697
|
-
// If all or none of the rows are 3rd party,
|
|
5889
|
+
// If all or none of the rows are 3rd party, hide the control.
|
|
5698
5890
|
if (allThirdParty || allFirstParty) {
|
|
5699
|
-
|
|
5700
|
-
filterInput.checked = allThirdParty;
|
|
5891
|
+
this._dom.find('div.lh-3p-filter', filterTemplate).hidden = true;
|
|
5701
5892
|
}
|
|
5702
5893
|
|
|
5703
5894
|
// Add checkbox to the DOM.
|
|
@@ -5714,9 +5905,9 @@ class ReportUIFeatures {
|
|
|
5714
5905
|
}
|
|
5715
5906
|
|
|
5716
5907
|
/**
|
|
5717
|
-
* @param {Element}
|
|
5908
|
+
* @param {Element} rootEl
|
|
5718
5909
|
*/
|
|
5719
|
-
_setupElementScreenshotOverlay(
|
|
5910
|
+
_setupElementScreenshotOverlay(rootEl) {
|
|
5720
5911
|
const fullPageScreenshot =
|
|
5721
5912
|
this.json.audits['full-page-screenshot'] &&
|
|
5722
5913
|
this.json.audits['full-page-screenshot'].details &&
|
|
@@ -5726,8 +5917,8 @@ class ReportUIFeatures {
|
|
|
5726
5917
|
|
|
5727
5918
|
ElementScreenshotRenderer.installOverlayFeature({
|
|
5728
5919
|
dom: this._dom,
|
|
5729
|
-
|
|
5730
|
-
overlayContainerEl:
|
|
5920
|
+
rootEl: rootEl,
|
|
5921
|
+
overlayContainerEl: rootEl,
|
|
5731
5922
|
fullPageScreenshot,
|
|
5732
5923
|
});
|
|
5733
5924
|
}
|
|
@@ -5762,27 +5953,42 @@ class ReportUIFeatures {
|
|
|
5762
5953
|
}
|
|
5763
5954
|
|
|
5764
5955
|
/**
|
|
5765
|
-
*
|
|
5766
|
-
*
|
|
5956
|
+
* DevTools uses its own file manager to download files, so it redefines this function.
|
|
5957
|
+
* Wrapper is necessary so DevTools can still override this function.
|
|
5958
|
+
*
|
|
5959
|
+
* @param {Blob|File} blob
|
|
5767
5960
|
*/
|
|
5768
5961
|
_saveFile(blob) {
|
|
5769
|
-
const filename =
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5962
|
+
const filename = fileNamer.getLhrFilenamePrefix(this.json);
|
|
5963
|
+
this._dom.saveFile(blob, filename);
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5773
5966
|
|
|
5774
|
-
|
|
5967
|
+
/**
|
|
5968
|
+
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
|
|
5969
|
+
* 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
|
|
5970
|
+
* 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.
|
|
5971
|
+
*/
|
|
5775
5972
|
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5973
|
+
/**
|
|
5974
|
+
* @param {LH.Result} lhr
|
|
5975
|
+
* @param {LH.Renderer.Options} opts
|
|
5976
|
+
* @return {HTMLElement}
|
|
5977
|
+
*/
|
|
5978
|
+
function renderReport(lhr, opts = {}) {
|
|
5979
|
+
const rootEl = document.createElement('article');
|
|
5980
|
+
rootEl.classList.add('lh-root', 'lh-vars');
|
|
5781
5981
|
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5982
|
+
const dom = new DOM(rootEl.ownerDocument, rootEl);
|
|
5983
|
+
const renderer = new ReportRenderer(dom);
|
|
5984
|
+
|
|
5985
|
+
renderer.renderReport(lhr, rootEl, opts);
|
|
5986
|
+
|
|
5987
|
+
// Hook in JS features and page-level event listeners after the report
|
|
5988
|
+
// is in the document.
|
|
5989
|
+
const features = new ReportUIFeatures(dom, opts);
|
|
5990
|
+
features.initFeatures(lhr);
|
|
5991
|
+
return rootEl;
|
|
5786
5992
|
}
|
|
5787
5993
|
|
|
5788
|
-
export { DOM, ReportRenderer, ReportUIFeatures };
|
|
5994
|
+
export { DOM, ReportRenderer, ReportUIFeatures, renderReport };
|