From 60e38ee7cf54429d9b324f05f41b119ee4818d0a Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 9 Jul 2021 16:43:48 -0700 Subject: [PATCH 01/41] report api. (and psi thinger) --- build/build-report.js | 13 ++ .../build-report-for-autodeployment.js | 20 +++ report/assets/psi-template.html | 124 ++++++++++++++++++ report/assets/standalone-template.html | 2 +- report/clients/faux-psi.js | 34 +++++ report/renderer/psi.js | 11 +- report/report-assets.js | 4 +- types/report-renderer.d.ts | 119 +++++++++++++++++ 8 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 report/assets/psi-template.html create mode 100644 report/clients/faux-psi.js create mode 100644 types/report-renderer.d.ts diff --git a/build/build-report.js b/build/build-report.js index 457c601283fe..55da910765d6 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -36,6 +36,18 @@ async function buildStandaloneReport() { fs.writeFileSync(__dirname + '/../dist/report/standalone.js', REPORT_JAVASCRIPT); } +async function buildPSIReport() { + const REPORT_JAVASCRIPT = [ + concatRendererCode(), + fs.readFileSync(__dirname + '/../report/renderer/psi.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/clients/faux-psi.js', 'utf8'), + ].join(';\n'); + fs.mkdirSync(__dirname + '/../dist/report', {recursive: true}); + fs.writeFileSync(__dirname + '/../dist/report/psi-report.js', REPORT_JAVASCRIPT); +} + + + if (require.main === module) { buildStandaloneReport(); } @@ -43,4 +55,5 @@ if (require.main === module) { module.exports = { buildStandaloneReport, concatRendererCode, + buildPSIReport, }; diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js index 963c03e6dea0..56f885e6ad47 100644 --- a/lighthouse-core/scripts/build-report-for-autodeployment.js +++ b/lighthouse-core/scripts/build-report-for-autodeployment.js @@ -11,9 +11,16 @@ const fs = require('fs'); const path = require('path'); const swapLocale = require('../lib/i18n/swap-locale.js'); + +// Must build before importing report-generator. +const br = require('../../build/build-report.js'); +br.buildStandaloneReport(); +br.buildPSIReport(); + const ReportGenerator = require('../../report/report-generator.js'); const {defaultSettings} = require('../config/constants.js'); const lighthouse = require('../index.js'); + const lhr = /** @type {LH.Result} */ (require('../../lighthouse-core/test/results/sample_v2.json')); const {LH_ROOT} = require('../../root.js'); @@ -29,6 +36,8 @@ const DIST = path.join(LH_ROOT, `dist/now`); 'Ι‘rabic': swapLocale(lhr, 'ar').lhr, 'xl-accented': swapLocale(lhr, 'en-XL').lhr, 'error': errorLhr, + 'psi': generatePsiLHR(lhr), + 'soloperf': generatePsiLHR(lhr), }; // Generate and write reports @@ -63,6 +72,17 @@ function addPluginCategory(lhr) { }; } +/** + * @param {LH.Result} lhr + */ +function generatePsiLHR(lhr) { + const clone = JSON.parse(JSON.stringify(lhr)); + clone.categories = { + 'performance': clone.categories.performance, + }; + return clone; +} + /** * Generate an LHR with errors for the renderer to display. * We'll write an "empty" artifacts file to disk, only to use it in auditMode. diff --git a/report/assets/psi-template.html b/report/assets/psi-template.html new file mode 100644 index 000000000000..6624247c5313 --- /dev/null +++ b/report/assets/psi-template.html @@ -0,0 +1,124 @@ + + + + + + + PageSpeed Report + + + + + + + + +
+ +
+
+

Mobile

+
+

This content appears on tab 1.

+
+ +
+

Desktop

+

This content appears on tab 2.

+
+
+ + + + + + + diff --git a/report/assets/standalone-template.html b/report/assets/standalone-template.html index e7cd816d40c0..7b232a748040 100644 --- a/report/assets/standalone-template.html +++ b/report/assets/standalone-template.html @@ -23,7 +23,7 @@ Lighthouse Report - + diff --git a/report/clients/faux-psi.js b/report/clients/faux-psi.js new file mode 100644 index 000000000000..1f9872c4e5f7 --- /dev/null +++ b/report/clients/faux-psi.js @@ -0,0 +1,34 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * 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 + * 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. + */ +'use strict'; + +/* global document window prepareLabData */ + +(function __initLighthouseReport__() { + const mobileLHR = window.__LIGHTHOUSE_JSON__; + const desktopLHR = JSON.parse(JSON.stringify(mobileLHR)); + desktopLHR.categories.performance.score = 0.81; + + const lhrs = { + 'tab1-mobile': mobileLHR, + 'tab2-desktop': desktopLHR, + }; + + for (const [elId, lhr] of Object.entries(lhrs)) { + const {scoreGaugeEl, perfCategoryEl, + finalScreenshotDataUri, scoreScaleEl, installFeatures} = prepareLabData(lhr, document); + + + const container = document.querySelector(`#${elId}`).querySelector('main'); + container.append(scoreGaugeEl); + container.append(scoreScaleEl); + const imgEl = document.createElement('img'); + imgEl.src = finalScreenshotDataUri; + container.append(imgEl); + container.append(perfCategoryEl); + installFeatures(container); + } +})(); diff --git a/report/renderer/psi.js b/report/renderer/psi.js index de6248bcc5e2..0d6a5af72c9f 100644 --- a/report/renderer/psi.js +++ b/report/renderer/psi.js @@ -30,7 +30,12 @@ * * @param {LH.Result | string} LHResult The stringified version of {LH.Result} * @param {Document} document The host page's window.document - * @return {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null, scoreScaleEl: Element, installFeatures: Function}} + * @return {{ + * scoreGaugeEl: Element, + * perfCategoryEl: Element, + * finalScreenshotDataUri: string|null, + * scoreScaleEl: Element, + * installFeatures: Function}} */ function prepareLabData(LHResult, document) { const lhResult = (typeof LHResult === 'string') ? @@ -86,9 +91,13 @@ function prepareLabData(LHResult, document) { /** @param {HTMLElement} reportEl */ const installFeatures = (reportEl) => { if (fullPageScreenshot) { + // 1) Add fpss css var to reportEl parent so any thumbnails will work ElementScreenshotRenderer.installFullPageScreenshot( reportEl, fullPageScreenshot.screenshot); + + // 2) Set up overlay DOM that must z-index overlap everything important + // Append the overlay element to a specific part of the DOM so that // the sticky tab group element renders correctly. If put in the reportEl // like normal, then the sticky header would bleed through the overlay diff --git a/report/report-assets.js b/report/report-assets.js index b54411100f1c..8486ace28429 100644 --- a/report/report-assets.js +++ b/report/report-assets.js @@ -7,8 +7,8 @@ const fs = require('fs'); -const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); -const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/../dist/report/standalone.js', 'utf8'); +const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/psi-template.html', 'utf8'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/../dist/report/psi-report.js', 'utf8'); const REPORT_CSS = fs.readFileSync(__dirname + '/assets/styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/assets/templates.html', 'utf8'); diff --git a/types/report-renderer.d.ts b/types/report-renderer.d.ts new file mode 100644 index 000000000000..979db3460d1c --- /dev/null +++ b/types/report-renderer.d.ts @@ -0,0 +1,119 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * 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 + * 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. + */ + + +declare global { + module LH.Renderer { + + interface ReportRendererOptions { + /** DOM element that will the overlay DOM should be a child of. + * Must z-index overlay everything it should. + * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. */ + overlayParentEl?: HTMLElement + + /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ + onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; + + /** Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSINext don't want this.) + * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ + disableAutoDarkModeAndFireworks?: boolean; + + /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ + onSaveGist?: (lhr: LH.Result) => string + } + + + class ReportRenderer { + constructor(lhr: LH.Result, options?: ReportRendererOptions); + + getContainerEl(): HTMLElement; + getTopBarEl(): HTMLElement; + + + // No need for PSI mode. it'll just getContainerEl and get solo-cat mode and pull the scoregauge out, and remove the footer. + getFinalScreenshot(): void | string; + + // Convenience for folks: + // category handles a bunch of plugin, n/a, and error cases. groupDefs needed only for PWA + renderGaugeForCategory(category: LH.ReportCategory, groupDefinitions?: Object): HTMLElement; + renderGaugeForScore(num0to1: number): HTMLElement // maybe? + } + } +} + +Topbar + + +non-topbar functionality from rUIfeatures + - elem screenshots (add css var and install overlay) + - adding lh-narrow ? + - third-party-filter + - open tab with data (viewer, treemap) + - enable fireworks + - dark mode + - add Button (for treemap or trace) + - show perf metric descriptions when there's a perf metric error + + topbar func form rUIfeatures + - setup stickyheader + - copy LHR to clipboard + - topbar dropdown, incl [data-i18n] + - save json/html + - print & collapse + + +/** + * Full standalone report: + * .lh-root.lh-vars + *
+ * .lh-topbar + * .lh-container + * (cats.length > 1) ? undefined : .lh-sticky-header + * (cats.length > 1) ?
: .lh-header--solo-category + * .lh-header-container + * .lh-report + * .lh-categories + * .lh-footer + * .lh-element-screenshot__overlay + */ + + +/** + * DevTools report: + * .lh-root.lh-vars + * .lh-topbar + * .lh-container + * (same container contents as standalone) + * .lh-element-screenshot__overlay (one level higher than CLI) + */ + +todo: +lh-root and lh-vars should probably merge +psi currently doesnt render toplabel warnings + +/** + * PSI report + * .element-screenshots-container.lh-vars + * .result-tabs + * .result-container + * .goog-control.result.lh-vars.lh-root + * .psi-category-wrapper + * .report-summary + * .lh-score__gauge + * .inspected-url-text + * .lh-scorescale + * .result-body + * .report-body + * .field-data + * .psi-category-wrapper + * .lab-data + * .lh-category + * .goog-control.result.lh-vars.lh-root (the Desktop one) + * (all the same stuff) + */ + +// empty export to keep file a module +export {} From 9d0326bc5ed77b0b688fdb539ba9e1c1efe835fb Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 9 Jul 2021 17:24:50 -0700 Subject: [PATCH 02/41] cleanup --- types/report-renderer.d.ts | 100 ++++++------------------------------- 1 file changed, 16 insertions(+), 84 deletions(-) diff --git a/types/report-renderer.d.ts b/types/report-renderer.d.ts index 979db3460d1c..0e1189b512d8 100644 --- a/types/report-renderer.d.ts +++ b/types/report-renderer.d.ts @@ -7,25 +7,6 @@ declare global { module LH.Renderer { - - interface ReportRendererOptions { - /** DOM element that will the overlay DOM should be a child of. - * Must z-index overlay everything it should. - * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. */ - overlayParentEl?: HTMLElement - - /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ - onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; - - /** Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSINext don't want this.) - * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ - disableAutoDarkModeAndFireworks?: boolean; - - /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ - onSaveGist?: (lhr: LH.Result) => string - } - - class ReportRenderer { constructor(lhr: LH.Result, options?: ReportRendererOptions); @@ -42,78 +23,29 @@ declare global { renderGaugeForScore(num0to1: number): HTMLElement // maybe? } } -} -Topbar + interface ReportRendererOptions { + /** DOM element that will the overlay DOM should be a child of. + * Must z-index overlay everything it should. + * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. */ + overlayParentEl?: HTMLElement + /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ + onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; -non-topbar functionality from rUIfeatures - - elem screenshots (add css var and install overlay) - - adding lh-narrow ? - - third-party-filter - - open tab with data (viewer, treemap) - - enable fireworks - - dark mode - - add Button (for treemap or trace) - - show perf metric descriptions when there's a perf metric error + /** Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSINext don't want this.) + * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ + disableAutoDarkModeAndFireworks?: boolean; - topbar func form rUIfeatures - - setup stickyheader - - copy LHR to clipboard - - topbar dropdown, incl [data-i18n] - - save json/html - - print & collapse + /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ + onSaveGist?: (lhr: LH.Result) => string + /** If defined, when the 'Save/Copy as HTML' items are clicked, this fn will be used instead of `documentElement.outerHTML`. */ + getStandaloneReportHTML?: () => string + } -/** - * Full standalone report: - * .lh-root.lh-vars - *
- * .lh-topbar - * .lh-container - * (cats.length > 1) ? undefined : .lh-sticky-header - * (cats.length > 1) ?
: .lh-header--solo-category - * .lh-header-container - * .lh-report - * .lh-categories - * .lh-footer - * .lh-element-screenshot__overlay - */ - - -/** - * DevTools report: - * .lh-root.lh-vars - * .lh-topbar - * .lh-container - * (same container contents as standalone) - * .lh-element-screenshot__overlay (one level higher than CLI) - */ - -todo: -lh-root and lh-vars should probably merge -psi currently doesnt render toplabel warnings +} -/** - * PSI report - * .element-screenshots-container.lh-vars - * .result-tabs - * .result-container - * .goog-control.result.lh-vars.lh-root - * .psi-category-wrapper - * .report-summary - * .lh-score__gauge - * .inspected-url-text - * .lh-scorescale - * .result-body - * .report-body - * .field-data - * .psi-category-wrapper - * .lab-data - * .lh-category - * .goog-control.result.lh-vars.lh-root (the Desktop one) - * (all the same stuff) - */ // empty export to keep file a module export {} From f059abd21e269b81eb8d406b6c4cbcb3945f9d67 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 9 Jul 2021 17:45:24 -0700 Subject: [PATCH 03/41] 'proper' psi variant --- .../scripts/build-report-for-autodeployment.js | 10 ++++++++-- report/assets/psi-template.html | 3 ++- report/assets/standalone-template.html | 2 +- report/report-assets.js | 4 ++-- report/report-generator.js | 8 +++++--- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js index 56f885e6ad47..2d1b71bfa763 100644 --- a/lighthouse-core/scripts/build-report-for-autodeployment.js +++ b/lighthouse-core/scripts/build-report-for-autodeployment.js @@ -42,8 +42,14 @@ const DIST = path.join(LH_ROOT, `dist/now`); // Generate and write reports Object.entries(filenameToLhr).forEach(([filename, lhr]) => { - let html = ReportGenerator.generateReportHtml(lhr); - // TODO: PSI is another variant to consider + let reportTemplate; + let reportJs; + if (filename === 'psi') { + reportTemplate = fs.readFileSync(__dirname + '/../../report/assets/psi-template.html', 'utf8'); + reportJs = fs.readFileSync(__dirname + '/../../dist/report/psi-report.js', 'utf8'); + } + let html = ReportGenerator.generateReportHtml(lhr, reportTemplate, reportJs); + for (const variant of ['', '⌣.cdt.']) { if (variant.includes('cdt')) { // TODO: Make the DevTools Audits panel "emulation" more comprehensive diff --git a/report/assets/psi-template.html b/report/assets/psi-template.html index 6624247c5313..34a2516fdc0b 100644 --- a/report/assets/psi-template.html +++ b/report/assets/psi-template.html @@ -8,7 +8,7 @@ - PageSpeed Report + PageSpeed Faux Report - + diff --git a/report/report-assets.js b/report/report-assets.js index 8486ace28429..b54411100f1c 100644 --- a/report/report-assets.js +++ b/report/report-assets.js @@ -7,8 +7,8 @@ const fs = require('fs'); -const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/psi-template.html', 'utf8'); -const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/../dist/report/psi-report.js', 'utf8'); +const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/../dist/report/standalone.js', 'utf8'); const REPORT_CSS = fs.readFileSync(__dirname + '/assets/styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/assets/templates.html', 'utf8'); diff --git a/report/report-generator.js b/report/report-generator.js index 01a456c0e169..ec59d9b58e02 100644 --- a/report/report-generator.js +++ b/report/report-generator.js @@ -30,16 +30,18 @@ class ReportGenerator { /** * Returns the report HTML as a string with the report JSON and renderer JS inlined. * @param {LH.Result} lhr + * @param {string=} reportTemplate + * @param {string=} reportJs * @return {string} */ - static generateReportHtml(lhr) { + static generateReportHtml(lhr, reportTemplate = htmlReportAssets.REPORT_TEMPLATE, reportJs = htmlReportAssets.REPORT_JAVASCRIPT) { const sanitizedJson = JSON.stringify(lhr) .replace(/ Date: Tue, 20 Jul 2021 15:30:13 -0700 Subject: [PATCH 04/41] fixup psi dist --- .../scripts/build-report-for-autodeployment.js | 18 +++++++++++------- report/assets/psi-template.html | 3 ++- report/clients/faux-psi.js | 2 ++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js index 2d1b71bfa763..0915ba430ef7 100644 --- a/lighthouse-core/scripts/build-report-for-autodeployment.js +++ b/lighthouse-core/scripts/build-report-for-autodeployment.js @@ -15,7 +15,7 @@ const swapLocale = require('../lib/i18n/swap-locale.js'); // Must build before importing report-generator. const br = require('../../build/build-report.js'); br.buildStandaloneReport(); -br.buildPSIReport(); +br.buildPsiReport(); const ReportGenerator = require('../../report/report-generator.js'); const {defaultSettings} = require('../config/constants.js'); @@ -42,12 +42,7 @@ const DIST = path.join(LH_ROOT, `dist/now`); // Generate and write reports Object.entries(filenameToLhr).forEach(([filename, lhr]) => { - let reportTemplate; - let reportJs; - if (filename === 'psi') { - reportTemplate = fs.readFileSync(__dirname + '/../../report/assets/psi-template.html', 'utf8'); - reportJs = fs.readFileSync(__dirname + '/../../dist/report/psi-report.js', 'utf8'); - } + const [reportTemplate, reportJs] = filename === 'psi' ? readPsiAssets() : [,]; let html = ReportGenerator.generateReportHtml(lhr, reportTemplate, reportJs); for (const variant of ['', '⌣.cdt.']) { @@ -89,6 +84,15 @@ function generatePsiLHR(lhr) { return clone; } +function readPsiAssets() { + const reportTemplate = fs.readFileSync(__dirname + '/../../report/assets/psi-template.html', 'utf8'); + const reportJs = ` +${fs.readFileSync(__dirname + '/../../dist/report/psi.js', 'utf8')}; +${fs.readFileSync(__dirname + '/../../report/clients/faux-psi.js', 'utf8')}; + `; + return [reportTemplate, reportJs]; +} + /** * Generate an LHR with errors for the renderer to display. * We'll write an "empty" artifacts file to disk, only to use it in auditMode. diff --git a/report/assets/psi-template.html b/report/assets/psi-template.html index 34a2516fdc0b..01b86e0e7d6d 100644 --- a/report/assets/psi-template.html +++ b/report/assets/psi-template.html @@ -117,7 +117,8 @@

Desktop

- diff --git a/report/clients/faux-psi.js b/report/clients/faux-psi.js index 1f9872c4e5f7..39d1bb8843e2 100644 --- a/report/clients/faux-psi.js +++ b/report/clients/faux-psi.js @@ -5,6 +5,8 @@ */ 'use strict'; +/** @fileoverview This file is a glorified call of prepareLabData. */ + /* global document window prepareLabData */ (function __initLighthouseReport__() { From 349a6551c06aea3b791d16dd49496ef56c2a0224 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 20 Jul 2021 17:12:00 -0700 Subject: [PATCH 05/41] new tabs. distinguished screenshots --- .../build-report-for-autodeployment.js | 7 +- report/assets/faux-psi-template.html | 147 ++++++++++++++++++ report/assets/psi-template.html | 126 --------------- report/clients/faux-psi.js | 42 ++++- 4 files changed, 191 insertions(+), 131 deletions(-) create mode 100644 report/assets/faux-psi-template.html delete mode 100644 report/assets/psi-template.html diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js index 0915ba430ef7..94f9f707b555 100644 --- a/lighthouse-core/scripts/build-report-for-autodeployment.js +++ b/lighthouse-core/scripts/build-report-for-autodeployment.js @@ -81,11 +81,16 @@ function generatePsiLHR(lhr) { clone.categories = { 'performance': clone.categories.performance, }; + // no budgets in PSI + delete clone.audits['performance-budget']; + clone.categories.performance.auditRefs = clone.categories.performance.auditRefs.filter(audit => { + return !audit.id.endsWith('-budget'); + }); return clone; } function readPsiAssets() { - const reportTemplate = fs.readFileSync(__dirname + '/../../report/assets/psi-template.html', 'utf8'); + const reportTemplate = fs.readFileSync(__dirname + '/../../report/assets/faux-psi-template.html', 'utf8'); const reportJs = ` ${fs.readFileSync(__dirname + '/../../dist/report/psi.js', 'utf8')}; ${fs.readFileSync(__dirname + '/../../report/clients/faux-psi.js', 'utf8')}; diff --git a/report/assets/faux-psi-template.html b/report/assets/faux-psi-template.html new file mode 100644 index 000000000000..9975c7293deb --- /dev/null +++ b/report/assets/faux-psi-template.html @@ -0,0 +1,147 @@ + + + + + + + PageSpeed Faux Report + + + + + + + + +
+ + +
+ + + + + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + diff --git a/report/assets/psi-template.html b/report/assets/psi-template.html deleted file mode 100644 index 01b86e0e7d6d..000000000000 --- a/report/assets/psi-template.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - PageSpeed Faux Report - - - - - - - - -
- - -
-
-

Mobile

-
-

This content appears on tab 1.

-
- -
-

Desktop

-

This content appears on tab 2.

-
-
-
- - - - - - diff --git a/report/clients/faux-psi.js b/report/clients/faux-psi.js index 39d1bb8843e2..5679d5a196b3 100644 --- a/report/clients/faux-psi.js +++ b/report/clients/faux-psi.js @@ -9,21 +9,22 @@ /* global document window prepareLabData */ -(function __initLighthouseReport__() { +(async function __initLighthouseReport__() { const mobileLHR = window.__LIGHTHOUSE_JSON__; const desktopLHR = JSON.parse(JSON.stringify(mobileLHR)); desktopLHR.categories.performance.score = 0.81; const lhrs = { - 'tab1-mobile': mobileLHR, - 'tab2-desktop': desktopLHR, + 'mobile': mobileLHR, + 'desktop': desktopLHR, }; for (const [elId, lhr] of Object.entries(lhrs)) { + await distinguishLHR(lhr, elId); + const {scoreGaugeEl, perfCategoryEl, finalScreenshotDataUri, scoreScaleEl, installFeatures} = prepareLabData(lhr, document); - const container = document.querySelector(`#${elId}`).querySelector('main'); container.append(scoreGaugeEl); container.append(scoreScaleEl); @@ -34,3 +35,36 @@ installFeatures(container); } })(); + + +async function distinguishLHR(lhr, elId) { + lhr.categories.performance.title += ` ${elId}`; // for easier identification + + const finalSS = lhr.audits['final-screenshot'].details.data; + lhr.audits['final-screenshot'].details.data = await decorateScreenshot(finalSS, elId); + + const fullPageScreenshot = lhr.audits['full-page-screenshot'].details.screenshot.data; + lhr.audits['full-page-screenshot'].details.screenshot.data = await decorateScreenshot(fullPageScreenshot, elId); +} + +async function decorateScreenshot(datauri, elId) { + const img = document.createElement('img'); + + await new Promise((resolve, reject) => { + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', (err) => reject(err)); + img.src = datauri; + }); + const c = document.createElement('canvas'); + c.width = img.width; + c.height = img.height; + + const ctx = c.getContext('2d'); + ctx.drawImage(img, 0, 0); + console.log(img.width); + ctx.font = `${img.width / 2}px serif`; + ctx.textAlign = 'center'; + ctx.globalAlpha = 0.7; + ctx.fillText(elId === 'mobile' ? 'πŸ“±' : 'πŸ’»', img.width / 2, Math.min(img.height / 2, 700)); + return c.toDataURL(); +} From 4c2f18f469c4aa6f55f7d7fc5a0a1d41d426d44e Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 21 Jul 2021 09:52:17 -0700 Subject: [PATCH 06/41] cleanup psi more --- report/assets/faux-psi-template.html | 53 +++++++--------------------- report/clients/faux-psi.js | 33 +++++++++++------ 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/report/assets/faux-psi-template.html b/report/assets/faux-psi-template.html index 9975c7293deb..fc31f5f13771 100644 --- a/report/assets/faux-psi-template.html +++ b/report/assets/faux-psi-template.html @@ -8,14 +8,13 @@ - PageSpeed Faux Report + Faux PageSpeed Report - - - - - - - -
- - -
- - - - - - - -
-
-
-
- -
-
-
- -
-
- - - - - - diff --git a/report/clients/faux-psi.js b/report/clients/faux-psi.js deleted file mode 100644 index b900dc84561e..000000000000 --- a/report/clients/faux-psi.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. - * 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 - * 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. - */ -'use strict'; - -/** @fileoverview This file is a glorified call of prepareLabData. */ - -/* global document window prepareLabData */ - -(async function __initLighthouseReport__() { - const mobileLHR = window.__LIGHTHOUSE_JSON__; - const desktopLHR = JSON.parse(JSON.stringify(mobileLHR)); - - const lhrs = { - 'mobile': mobileLHR, - 'desktop': desktopLHR, - }; - - for (const [tabId, lhr] of Object.entries(lhrs)) { - await distinguishLHR(lhr, tabId); - - const {scoreGaugeEl, perfCategoryEl, - finalScreenshotDataUri, scoreScaleEl, installFeatures} = prepareLabData(lhr, document); - - const container = document.querySelector(`#${tabId}`).querySelector('main'); - container.append(scoreGaugeEl); - container.append(scoreScaleEl); - const imgEl = document.createElement('img'); - imgEl.src = finalScreenshotDataUri; - container.append(imgEl); - container.append(perfCategoryEl); - installFeatures(container); - } -})(); - - -/** - * Tweak the LHR to make the desktop and mobile reports easier to identify. - * Adjusted: Perf category name and score, and emoji placed on top of key screenshots. - * @param {LH.Report} lhr - * @param {string} tabId - */ -async function distinguishLHR(lhr, tabId) { - lhr.categories.performance.title += ` ${tabId}`; // for easier identification - if (tabId === 'desktop') { - lhr.categories.performance.score = 0.81; - } - - const finalSS = lhr.audits['final-screenshot'].details.data; - lhr.audits['final-screenshot'].details.data = await decorateScreenshot(finalSS, tabId); - - const fullPageScreenshot = lhr.audits['full-page-screenshot'].details.screenshot.data; - lhr.audits['full-page-screenshot'].details.screenshot.data = await decorateScreenshot(fullPageScreenshot, tabId); // eslint-disable-line max-len -} - -/** - * Add πŸ“± and πŸ’» emoji on top of screenshot - * @param {string} datauri - * @param {string} tabId - */ -async function decorateScreenshot(datauri, tabId) { - const img = document.createElement('img'); - - await new Promise((resolve, reject) => { - img.addEventListener('load', () => resolve(img)); - img.addEventListener('error', (err) => reject(err)); - img.src = datauri; - }); - const c = document.createElement('canvas'); - c.width = img.width; - c.height = img.height; - - const ctx = c.getContext('2d'); - ctx.drawImage(img, 0, 0); - console.log(img.width); - ctx.font = `${img.width / 2}px serif`; - ctx.textAlign = 'center'; - ctx.globalAlpha = 0.7; - ctx.fillText(tabId === 'mobile' ? 'πŸ“±' : 'πŸ’»', img.width / 2, Math.min(img.height / 2, 700)); - return c.toDataURL(); -} diff --git a/report/clients/psi.js b/report/clients/psi.js index bda86186d735..c243f580a7af 100644 --- a/report/clients/psi.js +++ b/report/clients/psi.js @@ -36,12 +36,7 @@ import {Util} from '../renderer/util.js'; * * @param {LH.Result | string} LHResult The stringified version of {LH.Result} * @param {Document} document The host page's window.document - * @return {{ - * scoreGaugeEl: Element, - * perfCategoryEl: Element, - * finalScreenshotDataUri: string|null, - * scoreScaleEl: Element, - * installFeatures: Function}} + * @return {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null, scoreScaleEl: Element, installFeatures: Function}} */ export function prepareLabData(LHResult, document) { const lhResult = (typeof LHResult === 'string') ? @@ -98,13 +93,9 @@ export function prepareLabData(LHResult, document) { /** @param {HTMLElement} reportEl */ const installFeatures = (reportEl) => { if (fullPageScreenshot) { - // 1) Add fpss css var to reportEl parent so any thumbnails will work ElementScreenshotRenderer.installFullPageScreenshot( reportEl, fullPageScreenshot.screenshot); - - // 2) Set up overlay DOM that must z-index overlap everything important - // Append the overlay element to a specific part of the DOM so that // the sticky tab group element renders correctly. If put in the reportEl // like normal, then the sticky header would bleed through the overlay diff --git a/report/report-generator.js b/report/report-generator.js index 89a8b3f9110c..a0f2fbecb535 100644 --- a/report/report-generator.js +++ b/report/report-generator.js @@ -30,21 +30,17 @@ class ReportGenerator { /** * Returns the report HTML as a string with the report JSON and renderer JS inlined. * @param {LH.Result} lhr - * @param {string=} reportTemplate - * @param {string=} reportJs * @return {string} */ - static generateReportHtml(lhr, - reportTemplate = htmlReportAssets.REPORT_TEMPLATE, - reportJs = htmlReportAssets.REPORT_JAVASCRIPT) { + static generateReportHtml(lhr) { const sanitizedJson = JSON.stringify(lhr) .replace(/ Date: Wed, 21 Jul 2021 11:10:02 -0700 Subject: [PATCH 08/41] one-shot functions --- types/report-renderer.d.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/types/report-renderer.d.ts b/types/report-renderer.d.ts index 0e1189b512d8..659e9535bd17 100644 --- a/types/report-renderer.d.ts +++ b/types/report-renderer.d.ts @@ -7,27 +7,30 @@ declare global { module LH.Renderer { - class ReportRenderer { - constructor(lhr: LH.Result, options?: ReportRendererOptions); + export function renderFullReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; + export function renderReportComponents(lhr: LH.Result, options?: ReportRendererOptions): ReportComponents - getContainerEl(): HTMLElement; - getTopBarEl(): HTMLElement; + export function getFinalScreenshot(): void | string; - - // No need for PSI mode. it'll just getContainerEl and get solo-cat mode and pull the scoregauge out, and remove the footer. - getFinalScreenshot(): void | string; - - // Convenience for folks: - // category handles a bunch of plugin, n/a, and error cases. groupDefs needed only for PWA - renderGaugeForCategory(category: LH.ReportCategory, groupDefinitions?: Object): HTMLElement; - renderGaugeForScore(num0to1: number): HTMLElement // maybe? - } + // Convenience for folks: + // category handles a bunch of plugin, n/a, and error cases. groupDefs needed only for PWA + export function renderGaugeForCategory(category: LH.ReportCategory, groupDefinitions?: Object): HTMLElement; + export function renderGaugeForScore(num0to1: number): HTMLElement // maybe? } + type ReportComponents = { + topbarEl: HTMLElement, + mainEl: HTMLElement, + headerEl: HTMLElement, + categoriesEl: HTMLElement, + footerEl: HTMLElement + }; + interface ReportRendererOptions { /** DOM element that will the overlay DOM should be a child of. - * Must z-index overlay everything it should. - * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. */ + * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. + * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. + * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ overlayParentEl?: HTMLElement /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ From 5e84263b499ad98838eebf180dcd65237f8f9ad6 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 21 Jul 2021 11:13:43 -0700 Subject: [PATCH 09/41] latest report api proposal --- types/report-renderer.d.ts | 40 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/types/report-renderer.d.ts b/types/report-renderer.d.ts index 659e9535bd17..0be55e801b22 100644 --- a/types/report-renderer.d.ts +++ b/types/report-renderer.d.ts @@ -4,26 +4,34 @@ * 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. */ - declare global { module LH.Renderer { export function renderFullReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; - export function renderReportComponents(lhr: LH.Result, options?: ReportRendererOptions): ReportComponents + export function renderReportComponents( + lhr: LH.Result, + options?: ReportRendererOptions + ): ReportComponents; export function getFinalScreenshot(): void | string; - // Convenience for folks: - // category handles a bunch of plugin, n/a, and error cases. groupDefs needed only for PWA - export function renderGaugeForCategory(category: LH.ReportCategory, groupDefinitions?: Object): HTMLElement; - export function renderGaugeForScore(num0to1: number): HTMLElement // maybe? + // Render gauge for a category + // category handles a bunch of plugin, n/a, and error cases. groupDefinitions only needed for PWA + export function renderGaugeForCategory( + category: LH.ReportCategory, + groupDefinitions?: Record + ): HTMLElement; + + // Extra convience if you have just a category score. + export function renderGaugeForScore(num0to1: number): HTMLElement; } + // TODO(paulirish): Refactor DOM to match. https://github.com/GoogleChrome/lighthouse/issues/12254#issuecomment-877520562 type ReportComponents = { - topbarEl: HTMLElement, - mainEl: HTMLElement, - headerEl: HTMLElement, - categoriesEl: HTMLElement, - footerEl: HTMLElement + topbarEl: HTMLElement; + mainEl: HTMLElement; + headerEl: HTMLElement; + categoriesEl: HTMLElement; + footerEl: HTMLElement; }; interface ReportRendererOptions { @@ -31,7 +39,7 @@ declare global { * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ - overlayParentEl?: HTMLElement + overlayParentEl?: HTMLElement; /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; @@ -41,14 +49,12 @@ declare global { disableAutoDarkModeAndFireworks?: boolean; /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ - onSaveGist?: (lhr: LH.Result) => string + onSaveGist?: (lhr: LH.Result) => string; /** If defined, when the 'Save/Copy as HTML' items are clicked, this fn will be used instead of `documentElement.outerHTML`. */ - getStandaloneReportHTML?: () => string + getStandaloneReportHTML?: () => string; } - } - // empty export to keep file a module -export {} +export {}; From e122e5d6ef3ef4256114ad1ad970b7fb58772199 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 23 Jul 2021 13:46:18 -0700 Subject: [PATCH 10/41] address all feedback --- types/report-renderer.d.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/types/report-renderer.d.ts b/types/report-renderer.d.ts index 0be55e801b22..d98d14924d26 100644 --- a/types/report-renderer.d.ts +++ b/types/report-renderer.d.ts @@ -6,13 +6,13 @@ declare global { module LH.Renderer { - export function renderFullReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; - export function renderReportComponents( + export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; + export function renderReportPartials( lhr: LH.Result, options?: ReportRendererOptions - ): ReportComponents; + ): ReportPartials; - export function getFinalScreenshot(): void | string; + export function getFinalScreenshotDataURL(): void | string; // Render gauge for a category // category handles a bunch of plugin, n/a, and error cases. groupDefinitions only needed for PWA @@ -26,7 +26,7 @@ declare global { } // TODO(paulirish): Refactor DOM to match. https://github.com/GoogleChrome/lighthouse/issues/12254#issuecomment-877520562 - type ReportComponents = { + type ReportPartials = { topbarEl: HTMLElement; mainEl: HTMLElement; headerEl: HTMLElement; @@ -35,7 +35,8 @@ declare global { }; interface ReportRendererOptions { - /** DOM element that will the overlay DOM should be a child of. + /** + * DOM element that will the overlay DOM should be a child of. * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ @@ -44,7 +45,8 @@ declare global { /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; - /** Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSINext don't want this.) + /** + * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.) * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ disableAutoDarkModeAndFireworks?: boolean; From d8aa5934d9c894fe9f8a412370f90a15c8bdb0a0 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 18 Oct 2021 15:37:28 -0700 Subject: [PATCH 11/41] update --- {types => report/types}/report-renderer.d.ts | 25 ++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) rename {types => report/types}/report-renderer.d.ts (75%) diff --git a/types/report-renderer.d.ts b/report/types/report-renderer.d.ts similarity index 75% rename from types/report-renderer.d.ts rename to report/types/report-renderer.d.ts index d98d14924d26..51d1526da7be 100644 --- a/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -7,17 +7,8 @@ declare global { module LH.Renderer { export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; - export function renderReportPartials( - lhr: LH.Result, - options?: ReportRendererOptions - ): ReportPartials; - - export function getFinalScreenshotDataURL(): void | string; - - // Render gauge for a category - // category handles a bunch of plugin, n/a, and error cases. groupDefinitions only needed for PWA export function renderGaugeForCategory( - category: LH.ReportCategory, + category: ReportResult.Category, groupDefinitions?: Record ): HTMLElement; @@ -25,15 +16,6 @@ declare global { export function renderGaugeForScore(num0to1: number): HTMLElement; } - // TODO(paulirish): Refactor DOM to match. https://github.com/GoogleChrome/lighthouse/issues/12254#issuecomment-877520562 - type ReportPartials = { - topbarEl: HTMLElement; - mainEl: HTMLElement; - headerEl: HTMLElement; - categoriesEl: HTMLElement; - footerEl: HTMLElement; - }; - interface ReportRendererOptions { /** * DOM element that will the overlay DOM should be a child of. @@ -43,13 +25,16 @@ declare global { overlayParentEl?: HTMLElement; /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ - onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; + onDetailsItemRendered?: (type: LH.Audit.Details['type'], el: HTMLElement, value: LH.Audit.Details) => void; /** * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.) * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ disableAutoDarkModeAndFireworks?: boolean; + /** Disable the topbar UI component */ + disableTopBar: boolean; + /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ onSaveGist?: (lhr: LH.Result) => string; From 399cd3cb6b04446e53508f64d0e02cdacb429979 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 18 Oct 2021 15:39:11 -0700 Subject: [PATCH 12/41] drop renderGaugeForCategory --- report/types/report-renderer.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index 51d1526da7be..7ff4ba86cd20 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -7,10 +7,6 @@ declare global { module LH.Renderer { export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; - export function renderGaugeForCategory( - category: ReportResult.Category, - groupDefinitions?: Record - ): HTMLElement; // Extra convience if you have just a category score. export function renderGaugeForScore(num0to1: number): HTMLElement; From 96cbb472ac3ae9588d78588b60bc4dabc1cb2943 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 15:13:52 -0700 Subject: [PATCH 13/41] report api impl --- report/renderer/api.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 report/renderer/api.js diff --git a/report/renderer/api.js b/report/renderer/api.js new file mode 100644 index 000000000000..51d3854bc5f5 --- /dev/null +++ b/report/renderer/api.js @@ -0,0 +1,25 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * 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 + * 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. + */ +'use strict'; + + +import {DOM} from '../renderer/dom.js'; +import {ReportRenderer} from '../renderer/report-renderer.js'; +import {ReportUIFeatures} from '../renderer/report-ui-features.js'; + +export function renderReport(lhr, opts) { + const rootEl = document.createElement('main'); + + const dom = new DOM(rootEl.ownerDocument); + const renderer = new ReportRenderer(dom); + + renderer.renderReport(lhr, rootEl); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(lhr); +} From 2b08fc21b4b5d40627246f3a08785b9197a4071e Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 15:21:51 -0700 Subject: [PATCH 14/41] omittopbar --- report/renderer/api.js | 4 +++ report/renderer/report-ui-features.js | 19 ++++++++----- report/types/report-renderer.d.ts | 39 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 report/types/report-renderer.d.ts diff --git a/report/renderer/api.js b/report/renderer/api.js index 51d3854bc5f5..4fcbb5480232 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -10,6 +10,10 @@ import {DOM} from '../renderer/dom.js'; import {ReportRenderer} from '../renderer/report-renderer.js'; import {ReportUIFeatures} from '../renderer/report-ui-features.js'; +/** + * @param {LH.Result} lhr + * @param {LH.ReportRendererOptions} opts + */ export function renderReport(lhr, opts) { const rootEl = document.createElement('main'); diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 0747a701d569..3e9bd71465bb 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -40,15 +40,16 @@ function getTableRows(tableEl) { export class ReportUIFeatures { /** * @param {DOM} dom + * @param {{omitTopbar?: Boolean}} opts */ - constructor(dom) { + constructor(dom, opts = {}) { /** @type {LH.Result} */ this.json; // eslint-disable-line no-unused-expressions /** @type {DOM} */ this._dom = dom; /** @type {Document} */ this._document = this._dom.document(); - this._topbar = new TopbarFeatures(this, dom); + this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom); this.onMediaQueryChange = this.onMediaQueryChange.bind(this); } @@ -61,8 +62,10 @@ export class ReportUIFeatures { initFeatures(lhr) { this.json = lhr; - this._topbar.enable(lhr); - this._topbar.resetUIState(); + if (this._topbar) { + this._topbar.enable(lhr); + this._topbar.resetUIState(); + } this._setupMediaQueryListeners(); this._setupThirdPartyFilter(); this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document)); @@ -150,7 +153,9 @@ export class ReportUIFeatures { * @return {string} */ getReportHtml() { - this._topbar.resetUIState(); + if (this._topbar) { + this._topbar.resetUIState(); + } return this._document.documentElement.outerHTML; } @@ -183,7 +188,9 @@ export class ReportUIFeatures { * be in their closed state (not opened) and the templates should be unstamped. */ _resetUIState() { - this._topbar.resetUIState(); + if (this._topbar) { + this._topbar.resetUIState(); + } } /** diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts new file mode 100644 index 000000000000..e934790e691f --- /dev/null +++ b/report/types/report-renderer.d.ts @@ -0,0 +1,39 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * 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 + * 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. + */ + + declare global { + module LH.Renderer { + export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; + + // Extra convience if you have just a category score. + export function renderGaugeForScore(num0to1: number): HTMLElement; + + export interface ReportRendererOptions { + /** + * DOM element that will the overlay DOM should be a child of. + * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. + * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. + * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ + overlayParentEl?: HTMLElement; + + /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ + onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; + + /** + * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.) + * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ + disableAutoDarkModeAndFireworks?: boolean; + + /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ + onSaveGist?: (lhr: LH.Result) => string; + + /** If defined, when the 'Save/Copy as HTML' items are clicked, this fn will be used instead of `documentElement.outerHTML`. */ + getStandaloneReportHTML?: () => string; + } +} + +// empty export to keep file a module +export {}; From 0c2f103299a54da0fa4934a2e619b4b88a39a284 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 15:32:32 -0700 Subject: [PATCH 15/41] broken because features is enabled before reportEl is added to dom --- build/build-report.js | 2 +- report/clients/bundle.js | 1 + report/renderer/api.js | 5 +++- report/test-assets/faux-psi.js | 45 +++++++++------------------------- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index d47b3a67ba22..7ddf24179cf4 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -125,7 +125,7 @@ async function buildUmdBundle() { if (require.main === module) { if (process.argv.length <= 2) { buildStandaloneReport(); - buildFlowReport(); + // buildFlowReport(); buildEsModulesBundle(); buildUmdBundle(); } diff --git a/report/clients/bundle.js b/report/clients/bundle.js index a25a45580eee..9f8af53456dd 100644 --- a/report/clients/bundle.js +++ b/report/clients/bundle.js @@ -15,3 +15,4 @@ export {DOM} from '../renderer/dom.js'; export {ReportRenderer} from '../renderer/report-renderer.js'; export {ReportUIFeatures} from '../renderer/report-ui-features.js'; +export {renderReport} from '../renderer/api.js'; diff --git a/report/renderer/api.js b/report/renderer/api.js index 4fcbb5480232..33ced84cf8cd 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -13,9 +13,11 @@ import {ReportUIFeatures} from '../renderer/report-ui-features.js'; /** * @param {LH.Result} lhr * @param {LH.ReportRendererOptions} opts + * @return {HTMLElement} */ export function renderReport(lhr, opts) { const rootEl = document.createElement('main'); + rootEl.classList.add('lh-root', 'lh-vars'); const dom = new DOM(rootEl.ownerDocument); const renderer = new ReportRenderer(dom); @@ -24,6 +26,7 @@ export function renderReport(lhr, opts) { // Hook in JS features and page-level event listeners after the report // is in the document. - const features = new ReportUIFeatures(dom); + const features = new ReportUIFeatures(dom, opts); features.initFeatures(lhr); + return rootEl; } diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index d9e4b7db0aed..633514ea5cb8 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -26,43 +26,22 @@ const lighthouseRenderer = window['report']; for (const [tabId, lhr] of Object.entries(lhrs)) { await distinguishLHR(lhr, tabId); - const container = document.querySelector(`#${tabId} main`); + const container = document.querySelector(`section#${tabId}`); if (!container) throw new Error('Unexpected DOM. Bailing.'); - renderLHReport(lhr, container); - } -})(); - -/** - * @param {LH.Result} lhrData - * @param {HTMLElement} reportContainer - */ -function renderLHReport(lhrData, reportContainer) { - /** - * @param {Document} doc - */ - function getRenderer(doc) { - const dom = new lighthouseRenderer.DOM(doc); - return new lighthouseRenderer.ReportRenderer(dom); - } - - const renderer = getRenderer(reportContainer.ownerDocument); - reportContainer.classList.add('lh-root', 'lh-vars'); - - try { - renderer.renderReport(lhrData, reportContainer); - // TODO: handle topbar removal better - // TODO: display warnings if appropriate. - for (const el of reportContainer.querySelectorAll('.lh-topbar, .lh-warnings')) { - el.setAttribute('hidden', 'true'); + try { + const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: true}); + // TODO: display warnings if appropriate. + for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { + el.setAttribute('hidden', 'true'); + } + container.append(reportRootEl); + } catch (e) { + console.error(e); + container.textContent = 'Error: LHR failed to render.'; } - const features = new lighthouseRenderer.ReportUIFeatures(renderer._dom); - features.initFeatures(lhrData); - } catch (e) { - console.error(e); - reportContainer.textContent = 'Error: LHR failed to render.'; } -} +})(); /** From bff39ee6dc089cb7e8daa5d2c752aa071da33fc9 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 15:49:09 -0700 Subject: [PATCH 16/41] rootEl in a good spot --- lighthouse-viewer/test/viewer-test-pptr.js | 2 +- report/renderer/api.js | 4 ++-- report/renderer/dom.js | 5 ++++- report/renderer/features-util.js | 2 +- report/renderer/report-renderer.js | 26 +++++++++++++++------- report/renderer/report-ui-features.js | 5 ++--- report/renderer/topbar-features.js | 6 ++--- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/lighthouse-viewer/test/viewer-test-pptr.js b/lighthouse-viewer/test/viewer-test-pptr.js index 7cb2e605a11f..ffa3ca0a0aa1 100644 --- a/lighthouse-viewer/test/viewer-test-pptr.js +++ b/lighthouse-viewer/test/viewer-test-pptr.js @@ -88,7 +88,7 @@ describe('Lighthouse Viewer', () => { await viewerPage.goto(viewerUrl, {waitUntil: 'networkidle2', timeout: 30000}); const fileInput = await viewerPage.$('#hidden-file-input'); await fileInput.uploadFile(sampleLhr); - await viewerPage.waitForSelector('.lh-container', {timeout: 30000}); + await viewerPage.waitForSelector('.lh-categories', {timeout: 30000}); }); it('should load with no errors', async () => { diff --git a/report/renderer/api.js b/report/renderer/api.js index 33ced84cf8cd..0ddb8f000637 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -19,10 +19,10 @@ export function renderReport(lhr, opts) { const rootEl = document.createElement('main'); rootEl.classList.add('lh-root', 'lh-vars'); - const dom = new DOM(rootEl.ownerDocument); + const dom = new DOM(rootEl.ownerDocument, rootEl); const renderer = new ReportRenderer(dom); - renderer.renderReport(lhr, rootEl); + renderer.renderReport(lhr, rootEl, {omitTopbar: true}); // Hook in JS features and page-level event listeners after the report // is in the document. diff --git a/report/renderer/dom.js b/report/renderer/dom.js index f81a415c167b..1b5f32bb9725 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -27,14 +27,17 @@ import {createComponent} from './components.js'; export class DOM { /** * @param {Document} document + * @param {HTMLElement} rootEl */ - constructor(document) { + constructor(document, rootEl) { /** @type {Document} */ this._document = document; /** @type {string} */ this._lighthouseChannel = 'unknown'; /** @type {Map} */ this._componentCache = new Map(); + /** @type {HTMLElement} */ + this.rootEl = rootEl; } /** diff --git a/report/renderer/features-util.js b/report/renderer/features-util.js index 890eca687880..f40a3528fcc8 100644 --- a/report/renderer/features-util.js +++ b/report/renderer/features-util.js @@ -14,7 +14,7 @@ * @param {boolean} [force] */ export function toggleDarkTheme(dom, force) { - const el = dom.find('.lh-vars', dom.document()); + const el = dom.rootEl; // This seems unnecessary, but in DevTools, passing "undefined" as the second // parameter acts like passing "false". // https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819 diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 0ac7cfcd9a0d..6b969433b4a6 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -40,18 +40,26 @@ export class ReportRenderer { /** * @param {LH.Result} lhr - * @param {Element} container Parent element to render the report into. + * @param {HTMLElement} rootEl Report root element containing the report + * @param {{omitTopbar?: Boolean}?} opts * @return {!Element} */ - renderReport(lhr, container) { + renderReport(lhr, rootEl, opts) { + if (!opts) { + console.warn('Please adopt the new report API in renderer/api.js.'); + this._dom.rootEl = rootEl; + opts = {}; + } + this._opts = opts; + this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown'); const report = Util.prepareReportResult(lhr); - container.textContent = ''; // Remove previous report. - container.appendChild(this._renderReport(report)); + rootEl.textContent = ''; // Remove previous report. + rootEl.appendChild(this._renderReport(report)); - return container; + return rootEl; } /** @@ -197,7 +205,7 @@ export class ReportRenderer { gaugeWrapperEl.addEventListener('click', e => { if (!gaugeWrapperEl.matches('[href^="#"]')) return; const selector = gaugeWrapperEl.getAttribute('href'); - const reportRoot = gaugeWrapperEl.closest('.lh-vars'); + const reportRoot = this._dom.rootEl; if (!selector || !reportRoot) return; const destEl = this._dom.find(selector, reportRoot); e.preventDefault(); @@ -300,9 +308,11 @@ export class ReportRenderer { const reportFragment = this._dom.createFragment(); reportFragment.append(this._dom.createComponent('styles')); - const topbarDocumentFragment = this._renderReportTopbar(report); - reportFragment.appendChild(topbarDocumentFragment); + if (!this._opts.omitTopbar) { + reportFragment.appendChild(this._renderReportTopbar(report)); + } + reportFragment.appendChild(reportContainer); reportContainer.appendChild(headerContainer); reportContainer.appendChild(reportSection); diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 3e9bd71465bb..17eb363d09ae 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -68,7 +68,7 @@ export class ReportUIFeatures { } this._setupMediaQueryListeners(); this._setupThirdPartyFilter(); - this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document)); + this._setupElementScreenshotOverlay(this._dom.rootEl); let turnOffTheLights = false; // Do not query the system preferences for DevTools - DevTools should only apply dark theme @@ -198,8 +198,7 @@ export class ReportUIFeatures { * @param {MediaQueryList|MediaQueryListEvent} mql */ onMediaQueryChange(mql) { - const root = this._dom.find('.lh-root', this._document); - root.classList.toggle('lh-narrow', mql.matches); + this._dom.rootEl.classList.toggle('lh-narrow', mql.matches); } _setupThirdPartyFilter() { diff --git a/report/renderer/topbar-features.js b/report/renderer/topbar-features.js index 39831a1512c5..88f5b2b6ce52 100644 --- a/report/renderer/topbar-features.js +++ b/report/renderer/topbar-features.js @@ -59,8 +59,8 @@ export class TopbarFeatures { // There is only a sticky header when at least 2 categories are present. if (Object.keys(this.lhr.categories).length >= 2) { this._setupStickyHeaderElements(); - const containerEl = this._dom.find('.lh-container', this._document); - const elToAddScrollListener = this._getScrollParent(containerEl); + const reportRootEl = this._dom.rootEl; + const elToAddScrollListener = this._getScrollParent(reportRootEl); elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll); // Use ResizeObserver where available. @@ -70,7 +70,7 @@ export class TopbarFeatures { // For now, limit to DevTools. if (this._dom.isDevTools()) { const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll); - resizeObserver.observe(containerEl); + resizeObserver.observe(reportRootEl); } else { window.addEventListener('resize', this._updateStickyHeaderOnScroll); } From 501a980b970523cd732fa3383b84e09b6248cd49 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 16:16:53 -0700 Subject: [PATCH 17/41] rootEl all the things --- build/build-report-components.js | 2 +- report/renderer/category-renderer.js | 2 +- report/renderer/components.js | 52 +++++++++---------- report/renderer/dom.js | 19 ++++--- report/renderer/drop-down-menu.js | 10 ++-- report/renderer/report-renderer.js | 2 +- report/renderer/report-ui-features.js | 22 ++++---- report/renderer/swap-locale-feature.js | 6 +-- report/renderer/topbar-features.js | 34 ++++++------ report/test-assets/faux-psi.js | 1 - report/test/renderer/report-renderer-test.js | 20 +++---- .../test/renderer/report-ui-features-test.js | 18 +++---- 12 files changed, 92 insertions(+), 96 deletions(-) diff --git a/build/build-report-components.js b/build/build-report-components.js index 9a1a0986f37b..598291d841ce 100644 --- a/build/build-report-components.js +++ b/build/build-report-components.js @@ -143,7 +143,7 @@ function compileTemplate(tmpEl) { } const fragmentVarName = makeOrGetVarName(tmpEl); - lines.push(`const ${fragmentVarName} = dom.document().createDocumentFragment();`); + lines.push(`const ${fragmentVarName} = dom.createFragment();`); for (const topLevelEl of tmpEl.content.children) { process(topLevelEl); diff --git a/report/renderer/category-renderer.js b/report/renderer/category-renderer.js index 10380f6c6934..6881f9b6c0ab 100644 --- a/report/renderer/category-renderer.js +++ b/report/renderer/category-renderer.js @@ -133,7 +133,7 @@ export class CategoryRenderer { const warningsEl = this.dom.createChildOf(summaryEl, 'div', 'lh-warnings'); this.dom.createChildOf(warningsEl, 'span').textContent = strings.warningHeader; if (warnings.length === 1) { - warningsEl.appendChild(this.dom.document().createTextNode(warnings.join(''))); + warningsEl.appendChild(this.dom.createTextNode(warnings.join(''))); } else { const warningsUl = this.dom.createChildOf(warningsEl, 'ul'); for (const warning of warnings) { diff --git a/report/renderer/components.js b/report/renderer/components.js index f7707a839bb3..cc299cfd9790 100644 --- a/report/renderer/components.js +++ b/report/renderer/components.js @@ -11,7 +11,7 @@ * @param {DOM} dom */ function create3pFilterComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); el1.append('\n .lh-3p-filter {\n background-color: var(--table-higlight-background-color);\n color: var(--color-gray-600);\n float: right;\n padding: 6px;\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 '); el0.append(el1); @@ -33,7 +33,7 @@ function create3pFilterComponent(dom) { * @param {DOM} dom */ function createAuditComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-audit'); const el2 = dom.createElement('details', 'lh-expandable-details'); const el3 = dom.createElement('summary'); @@ -58,7 +58,7 @@ function createAuditComponent(dom) { * @param {DOM} dom */ function createCategoryHeaderComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-category-header'); const el2 = dom.createElement('div', 'lh-score__gauge'); el2.setAttribute('role', 'heading'); @@ -73,7 +73,7 @@ function createCategoryHeaderComponent(dom) { * @param {DOM} dom */ function createChevronComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-chevron'); el1.setAttribute('viewBox', '0 0 100 100'); const el2 = dom.createElementNS('http://www.w3.org/2000/svg', 'g', 'lh-chevron__lines'); @@ -91,7 +91,7 @@ function createChevronComponent(dom) { * @param {DOM} dom */ function createClumpComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('details', 'lh-clump lh-audit-group'); const el2 = dom.createElement('summary'); const el3 = dom.createElement('div', 'lh-audit-group__summary'); @@ -110,7 +110,7 @@ function createClumpComponent(dom) { * @param {DOM} dom */ function createCrcComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-crc-container'); const el2 = dom.createElement('style'); 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,\');\n }\n .lh-crc .lh-right {\n background: url(\'data:image/svg+xml;utf8,\');\n }\n .lh-crc .lh-up-right {\n background: url(\'data:image/svg+xml;utf8,\');\n }\n .lh-crc .lh-vert-right {\n background: url(\'data:image/svg+xml;utf8,\');\n }\n .lh-crc .lh-vert {\n background: url(\'data:image/svg+xml;utf8,\');\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 '); @@ -132,7 +132,7 @@ function createCrcComponent(dom) { * @param {DOM} dom */ function createCrcChainComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-crc-node'); const el2 = dom.createElement('span', 'lh-crc-node__tree-marker'); const el3 = dom.createElement('span', 'lh-crc-node__tree-value'); @@ -145,7 +145,7 @@ function createCrcChainComponent(dom) { * @param {DOM} dom */ function createElementScreenshotComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-element-screenshot'); const el2 = dom.createElement('div', 'lh-element-screenshot__content'); const el3 = dom.createElement('div', 'lh-element-screenshot__mask'); @@ -170,7 +170,7 @@ function createElementScreenshotComponent(dom) { * @param {DOM} dom */ function createFooterComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); 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 '); el0.append(el1); @@ -196,7 +196,7 @@ function createFooterComponent(dom) { * @param {DOM} dom */ function createFractionComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('a', 'lh-fraction__wrapper'); const el2 = dom.createElement('div', 'lh-fraction__content-wrapper'); const el3 = dom.createElement('div', 'lh-fraction__content'); @@ -213,7 +213,7 @@ function createFractionComponent(dom) { * @param {DOM} dom */ function createGaugeComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('a', 'lh-gauge__wrapper'); const el2 = dom.createElement('div', 'lh-gauge__svg-wrapper'); const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-gauge'); @@ -241,7 +241,7 @@ function createGaugeComponent(dom) { * @param {DOM} dom */ function createGaugePwaComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); 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 '); el0.append(el1); @@ -341,7 +341,7 @@ function createGaugePwaComponent(dom) { * @param {DOM} dom */ function createHeadingComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); 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 '); el0.append(el1); @@ -356,7 +356,7 @@ function createHeadingComponent(dom) { * @param {DOM} dom */ function createMetricComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-metric'); const el2 = dom.createElement('div', 'lh-metric__innerwrap'); const el3 = dom.createElement('div', 'lh-metric__icon'); @@ -373,7 +373,7 @@ function createMetricComponent(dom) { * @param {DOM} dom */ function createMetricsToggleComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-metrics-toggle'); const el2 = dom.createElement('input', 'lh-metrics-toggle__input'); el2.setAttribute('type', 'checkbox'); @@ -411,7 +411,7 @@ function createMetricsToggleComponent(dom) { * @param {DOM} dom */ function createOpportunityComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-audit lh-audit--load-opportunity'); const el2 = dom.createElement('details', 'lh-expandable-details'); const el3 = dom.createElement('summary'); @@ -445,7 +445,7 @@ function createOpportunityComponent(dom) { * @param {DOM} dom */ function createOpportunityHeaderComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-load-opportunity__header lh-load-opportunity__cols'); const el2 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--one'); const el3 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--two'); @@ -458,7 +458,7 @@ function createOpportunityHeaderComponent(dom) { * @param {DOM} dom */ function createScorescaleComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-scorescale'); const el2 = dom.createElement('span', 'lh-scorescale-range lh-scorescale-range--fail'); el2.append('0–49'); @@ -475,7 +475,7 @@ function createScorescaleComponent(dom) { * @param {DOM} dom */ function createScoresWrapperComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--scores-container-padding);\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: 18px;\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: 700;\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 '); el0.append(el1); @@ -495,7 +495,7 @@ function createScoresWrapperComponent(dom) { * @param {DOM} dom */ function createSnippetComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-snippet'); const el2 = dom.createElement('style'); 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 '); @@ -508,7 +508,7 @@ function createSnippetComponent(dom) { * @param {DOM} dom */ function createSnippetContentComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-snippet__snippet'); const el2 = dom.createElement('div', 'lh-snippet__snippet-inner'); el1.append(' ', el2, ' '); @@ -520,7 +520,7 @@ function createSnippetContentComponent(dom) { * @param {DOM} dom */ function createSnippetHeaderComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-snippet__header'); const el2 = dom.createElement('div', 'lh-snippet__title'); const el3 = dom.createElement('div', 'lh-snippet__node'); @@ -537,7 +537,7 @@ function createSnippetHeaderComponent(dom) { * @param {DOM} dom */ function createSnippetLineComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-snippet__line'); const el2 = dom.createElement('div', 'lh-snippet__line-number'); const el3 = dom.createElement('div', 'lh-snippet__line-icon'); @@ -551,7 +551,7 @@ function createSnippetLineComponent(dom) { * @param {DOM} dom */ function createStylesComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); 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-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: #018642;\n --color-green: #0CCE6B;\n --color-lime-400: #D3E156;\n --color-orange-50: #FFF3E0;\n --color-orange-700: #D04900;\n --color-orange: #FFA400;\n --color-red-700: #EB0F00;\n --color-red: #FF4E42;\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: 40px;\n --audit-group-padding-vertical: 8px;\n --audit-margin-horizontal: 5px;\n --audit-padding-vertical: 8px;\n --category-padding: 40px;\n --chevron-line-stroke: var(--color-gray-600);\n --chevron-size: 12px;\n --default-padding: 12px;\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: 112px;\n --gauge-circle-size: 80px;\n --gauge-circle-size-sm: 36px;\n --gauge-label-font-size-big: 28px;\n --gauge-label-font-size: 20px;\n --gauge-label-line-height-big: 36px;\n --gauge-label-line-height: 26px;\n --gauge-percentage-font-size-big: 38px;\n --gauge-percentage-font-size: 28px;\n --gauge-wrapper-width: 148px;\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 --locale-selector-background-color: var(--color-white);\n --metric-toggle-lines-fill: #7F7F7F;\n --metric-value-font-size: 28px;\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 6px 0 -2px;\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: 16px;\n --report-icon-size: var(--score-icon-background-size);\n --report-line-height: 24px;\n --report-min-width: 400px;\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-width: calc(60 * var(--report-font-size));\n --score-container-padding: 8px;\n --score-icon-background-size: 24px;\n --score-icon-margin-left: 4px;\n --score-icon-margin-right: 12px;\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 --scores-container-padding: 20px 0 20px 0;\n --scorescale-height: 6px;\n --scorescale-width: 18px;\n --screenshot-overlay-background: rgba(0, 0, 0, 0.3);\n --section-padding-vertical: 12px;\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(0, 0%, 75%, 0.1);\n --tools-icon-color: var(--color-gray-600);\n --topbar-background-color: var(--color-gray-100);\n --topbar-height: 32px;\n --topbar-logo-size: 24px;\n --topbar-padding: 0 8px;\n --toplevel-warning-background-color: var(--color-orange-50);\n --toplevel-warning-message-text-color: #BD4200;\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,\');\n --plugin-icon-url: url(\'data:image/svg+xml;utf8,\');\n\n --pass-icon-url: url(\'data:image/svg+xml;utf8,check\');\n --average-icon-url: url(\'data:image/svg+xml;utf8,info\');\n --fail-icon-url: url(\'data:image/svg+xml;utf8,warn\');\n\n --pwa-installable-gray-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-color-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-color-url: url(\'data:image/svg+xml;utf8,\');\n\n --swap-locale-icon-url: url(\'data:image/svg+xml;utf8,\');\n}\n\n@media not print {\n .lh-vars.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 --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 \t--toplevel-warning-background-color: #544B40;\n \t--toplevel-warning-message-text-color: var(--color-orange-700);\n\t--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: 24px;\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 -webkit-font-smoothing: antialiased;\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(--color-informative);\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 font-size: var(--report-font-size);\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: 50px;\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-right: 5px;\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,\');\n}\n.lh-report-icon--copy::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--open::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--download::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--dark::before {\n background-image:url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--treemap::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--date::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--devices::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--world::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--stopwatch::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--networkspeed::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-one::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-many::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--chrome::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n\n\n\n.lh-buttons {\n display: flex;\n flex-wrap: wrap;\n}\n.lh-button {\n margin: 10px;\n height: 30px;\n border: 1px solid var(--color-gray-600);\n border-radius: 4px;\n font-weight: bold;\n color: rgb(26, 115, 232);\n background-color: var(--report-background-color);\n}\n.lh-button:first-of-type {\n margin-left: 0;\n}\n.lh-dark .lh-button {\n color: var(--color-blue-200);\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 font-size: var(--report-font-size);\n text-align: center;\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 font-weight: 500;\n padding: var(--audit-padding-vertical) 0;\n}\n\n.lh-audit--load-opportunity .lh-audit__header {\n display: block;\n}\n\n\n.lh-metricfilter {\n text-align: right;\n margin-top: var(--default-padding);\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 border: solid 1px var(--color-gray-400);\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n cursor: pointer;\n font-size: 90%;\n}\n\n.lh-metricfilter__label:first-of-type {\n border-top-left-radius: 5px;\n border-bottom-left-radius: 5px;\n margin-left: 5px;\n}\n.lh-metricfilter__label:last-of-type {\n border-top-right-radius: 5px;\n border-bottom-right-radius: 5px;\n}\n\n.lh-metricfilter__label--active {\n background: var(--color-blue-A700);\n color: var(--color-white);\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/* 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-audit-group > summary,\n.lh-expandable-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-audit-group > summary::-webkit-details-marker,\n.lh-expandable-details > summary::-webkit-details-marker {\n display: none;\n}\n\n/* Perf Metric */\n\n.lh-metrics-container {\n display: grid;\n grid-template-rows: 1fr 1fr 1fr;\n grid-template-columns: 1fr 1fr;\n grid-auto-flow: column;\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@media screen and (min-width: 640px) {\n .lh-metric:nth-child(3n+3) {\n border-bottom: 1px solid var(--report-border-color-secondary);\n }\n}\n\n@media screen and (max-width: 640px) {\n .lh-metrics-container {\n display: block;\n }\n\n .lh-metric:nth-last-child(-n+1) {\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: 10px 0;\n}\n\n.lh-metric__details {\n order: -1;\n}\n\n.lh-metric__title {\n flex: 1;\n font-weight: 500;\n}\n\n.lh-metrics__disclaimer {\n color: var(--color-gray-600);\n margin: var(--section-padding-vertical) 0;\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 font-weight: 500;\n grid-column-start: 2;\n}\n\n/* Change the grid to 3 columns for narrow viewport. */\n@media screen and (max-width: 535px) {\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.lh-metrics-toggle__input {\n cursor: pointer;\n opacity: 0;\n position: absolute;\n right: 0;\n width: 74px;\n height: 28px;\n top: -3px;\n}\n.lh-metrics-toggle__label {\n display: flex;\n background-color: #eee;\n border-radius: 20px;\n overflow: hidden;\n position: absolute;\n right: 0;\n top: -3px;\n pointer-events: none;\n}\n.lh-metrics-toggle__input:focus + label {\n outline: -webkit-focus-ring-color auto 3px;\n}\n.lh-metrics-toggle__icon {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n}\n.lh-metrics-toggle__input:not(:checked) + label .lh-metrics-toggle__icon--less,\n.lh-metrics-toggle__input:checked + label .lh-metrics-toggle__icon--more {\n background-color: var(--color-blue-A700);\n --metric-toggle-lines-fill: var(--color-white);\n}\n.lh-metrics-toggle__lines {\n fill: var(--metric-toggle-lines-fill);\n}\n\n.lh-metrics-toggle__label {\n background-color: var(--metrics-toggle-background-color);\n}\n\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--less {\n padding-left: 8px;\n}\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--more {\n padding-right: 8px;\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}\n.lh-audit-group--metrics .lh-audit-group__header span.lh-audit-group__title {\n flex: 1;\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: flex;\n flex-direction: row;\n justify-content: space-between;\n padding-bottom: var(--default-padding);\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@media screen and (max-width: 750px) {\n .lh-filmstrip {\n flex-wrap: wrap;\n }\n .lh-filmstrip__frame {\n width: 20%;\n margin-bottom: 5px;\n }\n .lh-filmstrip__thumbnail {\n display: block;\n margin: auto;\n }\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-table {\n width: 100%;\n margin: 16px 0px 16px 0px;\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 margin-top: calc(var(--category-padding) * 1.5);\n margin-bottom: var(--category-padding);\n}\n\n.lh-audit-group__itemcount {\n color: var(--color-gray-600);\n font-weight: bold;\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 font-size: var(--report-font-size);\n margin: 0 0 var(--audit-group-padding-vertical);\n /* When the header takes 100% width, the chevron becomes small. */\n max-width: calc(100% - var(--chevron-size));\n}\n/* max-width makes the metric toggle not flush. metrics doesn\'t have a chevron so unset. */\n.lh-audit-group--metrics .lh-audit-group__header {\n max-width: unset;\n}\n\n.lh-audit-group__header span.lh-audit-group__title {\n font-weight: bold;\n}\n\n.lh-audit-group__header span.lh-audit-group__itemcount {\n font-weight: bold;\n color: var(--color-gray-600);\n}\n\n.lh-audit-group__header span.lh-audit-group__description {\n font-weight: 500;\n color: var(--color-gray-600);\n}\n.lh-audit-group__header span.lh-audit-group__description::before {\n content: \'β€”\';\n margin: 0px var(--audit-margin-horizontal);\n}\n\n.lh-clump > .lh-audit-group__header,\n.lh-audit-group--diagnostics .lh-audit-group__header,\n.lh-audit-group--load-opportunities .lh-audit-group__header,\n.lh-audit-group--metrics .lh-audit-group__header,\n.lh-audit-group--pwa-installable .lh-audit-group__header,\n.lh-audit-group--pwa-optimized .lh-audit-group__header {\n margin-top: var(--audit-group-padding-vertical);\n}\n\n/* Our report design omits the header \'Metrics\', as they\'re clearly the metrics.\n It\'s more straightforward to hide w/ CSS vs change categoryRenderer.renderAuditGroup for this case. */\n.lh-audit-group--metrics > .lh-audit-group__header {\n /* Also, it\'s vis hidden (and not display:none) because of spacing regarding the pill. */\n visibility: hidden;\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);\n margin: var(--audit-padding-vertical) 0;\n padding: calc(var(--audit-padding-vertical) / 2) calc(var(--audit-description-padding-left));\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 max-width: calc(var(--report-width) - var(--category-padding) * 2);\n background-color: var(--toplevel-warning-background-color);\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);\n fill: var(--color-pass);\n stroke: var(--color-pass);\n}\n\n.lh-gauge__wrapper--average {\n color: var(--color-average);\n fill: var(--color-average);\n stroke: var(--color-average);\n}\n\n.lh-gauge__wrapper--fail {\n color: var(--color-fail);\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);\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);\n}\n.lh-fraction__wrapper--average .lh-fraction__background {\n background-color: var(--color-average);\n}\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 line-height: var(--gauge-label-line-height);\n margin-top: 10px;\n text-align: center;\n color: var(--report-text-color);\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 margin: 12px auto 0 auto;\n border: 1px solid var(--color-gray-200);\n border-radius: 20px;\n padding: 8px 8px;\n}\n\n.lh-scorescale-range {\n display: flex;\n align-items: center;\n margin: 0 12px;\n font-family: var(--report-font-family-monospace);\n white-space: nowrap;\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-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\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}\n\n.lh-category-header__finalscreenshot .lh-scorescale {\n border: 0;\n padding: 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-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}\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: 535px) {\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));\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--odd {\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: 8px 6px;\n}\n.lh-table th:first-child {\n padding-left: 0;\n}\n.lh-table th:last-child {\n padding-right: 0;\n}\n\n.lh-table tr {\n vertical-align: middle;\n}\n\n/* Looks unnecessary, but mostly for keeping the 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 padding: 0;\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-audit-group > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,\n.lh-audit > .lh-expandable-details .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {\n transform: rotate(var(--chevron-angle-right));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {\n transform: rotate(var(--chevron-angle));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__lines {\n transform: translateY(calc(var(--chevron-size) * -1));\n}\n\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 display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n background-color: var(--env-item-background-color);\n border-radius: 3px;\n padding: 0;\n font-size: 13px;\n padding-bottom: calc(var(--default-padding) / 2);\n}\n\n.lh-meta__item {\n display: block;\n list-style-type: none;\n position: relative;\n padding: calc(var(--default-padding) / 2) 0 0 calc(var(--default-padding) * 3);\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: calc(var(--report-icon-size) * 0.8);\n height: calc(var(--report-icon-size) * 0.8);\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'); el0.append(el1); @@ -562,7 +562,7 @@ function createStylesComponent(dom) { * @param {DOM} dom */ function createTopbarComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('style'); 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 padding: var(--topbar-padding);\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 12px;\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: unset;\n color: var(--tools-icon-color);\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 }\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 '); el0.append(el1); @@ -779,7 +779,7 @@ function createTopbarComponent(dom) { * @param {DOM} dom */ function createWarningsToplevelComponent(dom) { - const el0 = dom.document().createDocumentFragment(); + const el0 = dom.createFragment(); const el1 = dom.createElement('div', 'lh-warnings lh-warnings--toplevel'); const el2 = dom.createElement('p', 'lh-warnings__msg'); const el3 = dom.createElement('ul'); diff --git a/report/renderer/dom.js b/report/renderer/dom.js index 1b5f32bb9725..db850dfe2565 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -27,7 +27,7 @@ import {createComponent} from './components.js'; export class DOM { /** * @param {Document} document - * @param {HTMLElement} rootEl + * @param {HTMLElement=} rootEl */ constructor(document, rootEl) { /** @type {Document} */ @@ -37,6 +37,7 @@ export class DOM { /** @type {Map} */ this._componentCache = new Map(); /** @type {HTMLElement} */ + // @ts-expect-error For legacy Report API users, this'll be undefined, but set in renderReport this.rootEl = rootEl; } @@ -79,6 +80,15 @@ export class DOM { return this._document.createDocumentFragment(); } + /** + * @param {string} data + * @return {!Node} + */ + createTextNode(data) { + return this._document.createTextNode(data); + } + + /** * @template {string} T * @param {Element} parentElem @@ -218,13 +228,6 @@ export class DOM { this._lighthouseChannel = lighthouseChannel; } - /** - * @return {Document} - */ - document() { - return this._document; - } - /** * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this * @return {boolean} diff --git a/report/renderer/drop-down-menu.js b/report/renderer/drop-down-menu.js index 16231461dc5d..461238912485 100644 --- a/report/renderer/drop-down-menu.js +++ b/report/renderer/drop-down-menu.js @@ -36,11 +36,11 @@ export class DropDownMenu { * @param {function(MouseEvent): any} menuClickHandler */ setup(menuClickHandler) { - this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document()); + this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.rootEl); this._toggleEl.addEventListener('click', this.onToggleClick); this._toggleEl.addEventListener('keydown', this.onToggleKeydown); - this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.document()); + this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.rootEl); this._menuEl.addEventListener('keydown', this.onMenuKeydown); this._menuEl.addEventListener('click', menuClickHandler); } @@ -48,12 +48,12 @@ export class DropDownMenu { close() { this._toggleEl.classList.remove('lh-active'); this._toggleEl.setAttribute('aria-expanded', 'false'); - if (this._menuEl.contains(this._dom.document().activeElement)) { + if (this._menuEl.contains(this._dom._document.activeElement)) { // Refocus on the tools button if the drop down last had focus this._toggleEl.focus(); } this._menuEl.removeEventListener('focusout', this.onMenuFocusOut); - this._dom.document().removeEventListener('keydown', this.onDocumentKeyDown); + this._dom.rootEl.removeEventListener('keydown', this.onDocumentKeyDown); } /** @@ -73,7 +73,7 @@ export class DropDownMenu { this._toggleEl.classList.add('lh-active'); this._toggleEl.setAttribute('aria-expanded', 'true'); this._menuEl.addEventListener('focusout', this.onMenuFocusOut); - this._dom.document().addEventListener('keydown', this.onDocumentKeyDown); + this._dom.rootEl.addEventListener('keydown', this.onDocumentKeyDown); } /** diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 6b969433b4a6..563ba5db4e10 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -41,7 +41,7 @@ export class ReportRenderer { /** * @param {LH.Result} lhr * @param {HTMLElement} rootEl Report root element containing the report - * @param {{omitTopbar?: Boolean}?} opts + * @param {{omitTopbar?: Boolean}=} opts * @return {!Element} */ renderReport(lhr, rootEl, opts) { diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 17eb363d09ae..bc1b3bc86888 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -47,10 +47,8 @@ export class ReportUIFeatures { this.json; // eslint-disable-line no-unused-expressions /** @type {DOM} */ this._dom = dom; - /** @type {Document} */ - this._document = this._dom.document(); - this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom); + this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom); this.onMediaQueryChange = this.onMediaQueryChange.bind(this); } @@ -97,7 +95,7 @@ export class ReportUIFeatures { const hasMetricError = lhr.categories.performance && lhr.categories.performance.auditRefs .some(audit => Boolean(audit.group === 'metrics' && lhr.audits[audit.id].errorMessage)); if (hasMetricError) { - const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document); + const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._dom.rootEl); toggleInputEl.checked = true; } @@ -112,7 +110,7 @@ export class ReportUIFeatures { } // Fill in all i18n data. - for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) { + for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) { // These strings are guaranteed to (at least) have a default English string in Util.UIStrings, // so this cannot be undefined as long as `report-ui-features.data-i18n` test passes. const i18nKey = node.getAttribute('data-i18n'); @@ -122,18 +120,16 @@ export class ReportUIFeatures { } /** - * @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts + * @param {{text: string, icon?: string, onClick: () => void}} opts */ addButton(opts) { // report-ui-features doesn't have a reference to the root report el, and PSI has // 2 reports on the page (and not even attached to DOM when installFeatures is called..) // so we need a container option to specify where the element should go. - const metricsEl = this._document.querySelector('.lh-audit-group--metrics'); - const containerEl = opts.container || metricsEl; - if (!containerEl) return; + const metricsEl = this._dom.find('.lh-audit-group--metrics', this._dom.rootEl); - let buttonsEl = containerEl.querySelector('.lh-buttons'); - if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons'); + let buttonsEl = metricsEl.querySelector('.lh-buttons'); + if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons'); const classes = [ 'lh-button', @@ -168,7 +164,7 @@ export class ReportUIFeatures { } _enableFireworks() { - const scoresContainer = this._dom.find('.lh-scores-container', this._document); + const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl); scoresContainer.classList.add('lh-score100'); scoresContainer.addEventListener('click', _ => { scoresContainer.classList.toggle('lh-fireworks-paused'); @@ -215,7 +211,7 @@ export class ReportUIFeatures { ]; // Get all tables with a text url column. - const tables = Array.from(this._document.querySelectorAll('table.lh-table')); + const tables = Array.from(this._dom.rootEl.querySelectorAll('table.lh-table')); const tablesWithUrls = tables .filter(el => el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location')) diff --git a/report/renderer/swap-locale-feature.js b/report/renderer/swap-locale-feature.js index a37cabc05a02..6e66b716ad8a 100644 --- a/report/renderer/swap-locale-feature.js +++ b/report/renderer/swap-locale-feature.js @@ -33,17 +33,17 @@ export class SwapLocaleFeature { throw new Error('missing icuMessagePaths'); } - this._dom.find('.lh-tools-locale', this._dom.document()).classList.remove('lh-hidden'); + this._dom.find('.lh-tools-locale', this._dom.rootEl).classList.remove('lh-hidden'); const currentLocale = this._reportUIFeatures.json.configSettings.locale; - const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.document()); + const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.rootEl); containerEl.removeAttribute('aria-hidden'); const selectEl = this._dom.createChildOf(containerEl, 'select', 'lh-locale-selector'); selectEl.name = 'lh-locale-list'; selectEl.setAttribute('role', 'menuitem'); - const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.document()); + const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.rootEl); toggleEl.addEventListener('click', () => { toggleEl.classList.toggle('lh-active'); }); diff --git a/report/renderer/topbar-features.js b/report/renderer/topbar-features.js index 88f5b2b6ce52..02fbf4a19bb1 100644 --- a/report/renderer/topbar-features.js +++ b/report/renderer/topbar-features.js @@ -24,8 +24,6 @@ export class TopbarFeatures { this.lhr; // eslint-disable-line no-unused-expressions this._reportUIFeatures = reportUIFeatures; this._dom = dom; - /** @type {Document} */ - this._document = this._dom.document(); this._dropDownMenu = new DropDownMenu(this._dom); this._copyAttempt = false; /** @type {HTMLElement} */ @@ -48,12 +46,12 @@ export class TopbarFeatures { */ enable(lhr) { this.lhr = lhr; - this._document.addEventListener('keyup', this.onKeyUp); - this._document.addEventListener('copy', this.onCopy); + this._dom.rootEl.addEventListener('keyup', this.onKeyUp); + this._dom.rootEl.addEventListener('copy', this.onCopy); this._dropDownMenu.setup(this.onDropDownMenuClick); this._setUpCollapseDetailsAfterPrinting(); - const topbarLogo = this._dom.find('.lh-topbar__logo', this._document); + const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl); topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom)); // There is only a sticky header when at least 2 categories are present. @@ -112,7 +110,7 @@ export class TopbarFeatures { try { this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'})); } catch (e) { - this._dom.fireEventOn('lh-log', this._document, { + this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { cmd: 'error', msg: 'Could not export as HTML. ' + e.message, }); } @@ -152,7 +150,7 @@ export class TopbarFeatures { e.preventDefault(); e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2)); - this._dom.fireEventOn('lh-log', this._document, { + this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { cmd: 'log', msg: 'Report JSON copied to clipboard', }); } @@ -164,28 +162,28 @@ export class TopbarFeatures { * Copies the report JSON to the clipboard (if supported by the browser). */ onCopyButtonClick() { - this._dom.fireEventOn('lh-analytics', this._document, { + this._dom.fireEventOn('lh-analytics', this._dom.rootEl.ownerDocument, { cmd: 'send', fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'}, }); try { - if (this._document.queryCommandSupported('copy')) { + if (this._dom.rootEl.ownerDocument.queryCommandSupported('copy')) { this._copyAttempt = true; // Note: In Safari 10.0.1, execCommand('copy') returns true if there's // a valid text selection on the page. See http://caniuse.com/#feat=clipboard. - if (!this._document.execCommand('copy')) { + if (!this._dom.rootEl.ownerDocument.execCommand('copy')) { this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt. - this._dom.fireEventOn('lh-log', this._document, { + this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { cmd: 'warn', msg: 'Your browser does not support copy to clipboard.', }); } } } catch (e) { this._copyAttempt = false; - this._dom.fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message}); + this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, {cmd: 'log', msg: e.message}); } } @@ -206,7 +204,7 @@ export class TopbarFeatures { * open a `
` element. */ expandAllDetails() { - const details = this._dom.findAll('.lh-categories details', this._document); + const details = this._dom.findAll('.lh-categories details', this._dom.rootEl); details.map(detail => detail.open = true); } @@ -215,7 +213,7 @@ export class TopbarFeatures { * open a `
` element. */ collapseAllDetails() { - const details = this._dom.findAll('.lh-categories details', this._document); + const details = this._dom.findAll('.lh-categories details', this._dom.rootEl); details.map(detail => detail.open = false); } @@ -274,9 +272,9 @@ export class TopbarFeatures { } _setupStickyHeaderElements() { - this.topbarEl = this._dom.find('div.lh-topbar', this._document); - this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._document); - this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._document); + this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl); + this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._dom.rootEl); + this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl); // Highlighter will be absolutely positioned at first gauge, then transformed on scroll. this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter'); @@ -290,7 +288,7 @@ export class TopbarFeatures { // Highlight mini gauge when section is in view. // In view = the last category that starts above the middle of the window. - const categoryEls = Array.from(this._document.querySelectorAll('.lh-category')); + const categoryEls = Array.from(this._dom.rootEl.querySelectorAll('.lh-category')); const categoriesAboveTheMiddle = categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0); const highlightIndex = diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index 633514ea5cb8..58d72798e58b 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -51,7 +51,6 @@ const lighthouseRenderer = window['report']; * @param {string} tabId */ async function distinguishLHR(lhr, tabId) { - lhr.categories.performance.title += ` ${tabId}`; // for easier identification if (tabId === 'desktop') { lhr.categories.performance.score = 0.81; } diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 43f31bc957a4..6237632bd715 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -50,7 +50,7 @@ describe('ReportRenderer', () => { describe('renderReport', () => { it('should render a report', () => { - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(sampleResults, container); assert.ok(output.querySelector('.lh-header-container'), 'has a header'); assert.ok(output.querySelector('.lh-report'), 'has report body'); @@ -60,7 +60,7 @@ describe('ReportRenderer', () => { }); it('renders additional reports by replacing the existing one', () => { - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const oldReport = Array.from(renderer.renderReport(sampleResults, container).children); const newReport = Array.from(renderer.renderReport(sampleResults, container).children); assert.ok(!oldReport.find(node => container.contains(node)), 'old report was removed'); @@ -86,7 +86,7 @@ describe('ReportRenderer', () => { auditRefs: [], }; - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(sampleResultsCopy, container); function isPWAGauge(el) { @@ -126,7 +126,7 @@ describe('ReportRenderer', () => { title: 'Some Plugin', auditRefs: [], }; - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(sampleResultsCopy, container); const scoresHeaderElem = output.querySelector('.lh-scores-header'); @@ -140,7 +140,7 @@ describe('ReportRenderer', () => { }); it('should not mutate a report object', () => { - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const originalResults = JSON.parse(JSON.stringify(sampleResults)); renderer.renderReport(sampleResults, container); assert.deepStrictEqual(sampleResults, originalResults); @@ -148,13 +148,13 @@ describe('ReportRenderer', () => { it('renders no warning section when no lighthouseRunWarnings occur', () => { const warningResults = Object.assign({}, sampleResults, {runWarnings: []}); - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(warningResults, container); assert.strictEqual(output.querySelector('.lh-warnings--toplevel'), null); }); it('renders a warning section', () => { - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(sampleResults, container); const warningEls = output.querySelectorAll('.lh-warnings--toplevel > ul > li'); @@ -165,7 +165,7 @@ describe('ReportRenderer', () => { const warningResults = Object.assign({}, sampleResults, { runWarnings: ['[I am a link](https://example.com/)'], }); - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(warningResults, container); const warningEls = output.querySelectorAll('.lh-warnings--toplevel ul li a'); @@ -196,7 +196,7 @@ describe('ReportRenderer', () => { // Make sure we have a channel in the LHR. assert.ok(lhrChannel.length > 2); - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const output = renderer.renderReport(sampleResults, container); const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev']; @@ -225,7 +225,7 @@ describe('ReportRenderer', () => { assert.ok(notApplicableCount > 20); // Make sure something's being tested. - const container = renderer._dom._document.body; + const container = renderer._dom.rootEl; const reportElement = renderer.renderReport(sampleResults, container); const notApplicableElementCount = reportElement .querySelectorAll('.lh-audit--notapplicable').length; diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index befcfd6e8862..355c15ed3d63 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -33,7 +33,7 @@ describe('ReportUIFeatures', () => { const categoryRenderer = new CategoryRenderer(dom, detailsRenderer); const renderer = new ReportRenderer(dom, categoryRenderer); const reportUIFeatures = new ReportUIFeatures(dom); - const container = dom.find('main', dom._document); + const container = dom.find('main', dom.rootEl); renderer.renderReport(lhr, container); reportUIFeatures.initFeatures(lhr); return container; @@ -331,7 +331,7 @@ describe('ReportUIFeatures', () => { let dropDown; beforeEach(() => { - window = dom.document().defaultView; + window = dom._document.defaultView; const features = new ReportUIFeatures(dom); features.initFeatures(sampleResults); dropDown = features._topbar._dropDownMenu; @@ -351,7 +351,7 @@ describe('ReportUIFeatures', () => { assert.ok(dropDown._toggleEl.classList.contains('lh-active')); const escape = new window.KeyboardEvent('keydown', {keyCode: /* ESC */ 27}); - dom.document().dispatchEvent(escape); + dropDown._toggleEl.dispatchEvent(escape); assert.ok(!dropDown._toggleEl.classList.contains('lh-active')); }); @@ -369,7 +369,7 @@ describe('ReportUIFeatures', () => { const arrowUp = new window.KeyboardEvent('keydown', {bubbles: true, code: 'ArrowUp'}); dropDown._menuEl.firstElementChild.dispatchEvent(arrowUp); - assert.strictEqual(dom.document().activeElement, dropDown._menuEl.lastElementChild); + assert.strictEqual(dom._document.activeElement, dropDown._menuEl.lastElementChild); }); it('ArrowDown on the first menu element should focus the second element', () => { @@ -379,7 +379,7 @@ describe('ReportUIFeatures', () => { const arrowDown = new window.KeyboardEvent('keydown', {bubbles: true, code: 'ArrowDown'}); dropDown._menuEl.firstElementChild.dispatchEvent(arrowDown); - assert.strictEqual(dom.document().activeElement, nextElementSibling); + assert.strictEqual(dom._document.activeElement, nextElementSibling); }); it('Home on the last menu element should focus the first element', () => { @@ -389,7 +389,7 @@ describe('ReportUIFeatures', () => { const home = new window.KeyboardEvent('keydown', {bubbles: true, code: 'Home'}); dropDown._menuEl.lastElementChild.dispatchEvent(home); - assert.strictEqual(dom.document().activeElement, firstElementChild); + assert.strictEqual(dom._document.activeElement, firstElementChild); }); it('End on the first menu element should focus the last element', () => { @@ -399,14 +399,14 @@ describe('ReportUIFeatures', () => { const end = new window.KeyboardEvent('keydown', {bubbles: true, code: 'End'}); dropDown._menuEl.firstElementChild.dispatchEvent(end); - assert.strictEqual(dom.document().activeElement, lastElementChild); + assert.strictEqual(dom._document.activeElement, lastElementChild); }); describe('_getNextSelectableNode', () => { let createDiv; beforeAll(() => { - createDiv = () => dom.document().createElement('div'); + createDiv = () => dom._document.createElement('div'); }); it('should return undefined when nodes is empty', () => { @@ -482,7 +482,7 @@ describe('ReportUIFeatures', () => { }); it('should toggle active class when focus relatedTarget is document.body', () => { - const relatedTarget = dom.document().body; + const relatedTarget = dom._document.body; const event = new window.FocusEvent('focusout', {relatedTarget}); dropDown.onMenuFocusOut(event); From 37bf128fcd9c972215a1dad3aaa8eae1a5bc75c2 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 16:26:54 -0700 Subject: [PATCH 18/41] root for fallback scenario --- build/build-report.js | 2 +- report/assets/styles.css | 2 +- report/renderer/components.js | 2 +- report/renderer/drop-down-menu.js | 4 ++-- report/renderer/report-renderer.js | 6 +++++- report/test-assets/faux-psi.js | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 7ddf24179cf4..25b0856d9d0c 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -45,7 +45,7 @@ async function buildStandaloneReport() { input: 'report/clients/standalone.js', plugins: [ rollupPlugins.commonjs(), - rollupPlugins.terser(), + // rollupPlugins.terser(), ], }); diff --git a/report/assets/styles.css b/report/assets/styles.css index 35456da656cd..2e76c2be588d 100644 --- a/report/assets/styles.css +++ b/report/assets/styles.css @@ -174,7 +174,7 @@ } @media not print { - .lh-vars.lh-dark { + .lh-dark { /* Pallete */ --color-gray-200: var(--color-gray-800); --color-gray-300: #616161; diff --git a/report/renderer/components.js b/report/renderer/components.js index cc299cfd9790..2b1ffd034c1e 100644 --- a/report/renderer/components.js +++ b/report/renderer/components.js @@ -553,7 +553,7 @@ function createSnippetLineComponent(dom) { function createStylesComponent(dom) { const el0 = dom.createFragment(); const el1 = dom.createElement('style'); - 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-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: #018642;\n --color-green: #0CCE6B;\n --color-lime-400: #D3E156;\n --color-orange-50: #FFF3E0;\n --color-orange-700: #D04900;\n --color-orange: #FFA400;\n --color-red-700: #EB0F00;\n --color-red: #FF4E42;\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: 40px;\n --audit-group-padding-vertical: 8px;\n --audit-margin-horizontal: 5px;\n --audit-padding-vertical: 8px;\n --category-padding: 40px;\n --chevron-line-stroke: var(--color-gray-600);\n --chevron-size: 12px;\n --default-padding: 12px;\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: 112px;\n --gauge-circle-size: 80px;\n --gauge-circle-size-sm: 36px;\n --gauge-label-font-size-big: 28px;\n --gauge-label-font-size: 20px;\n --gauge-label-line-height-big: 36px;\n --gauge-label-line-height: 26px;\n --gauge-percentage-font-size-big: 38px;\n --gauge-percentage-font-size: 28px;\n --gauge-wrapper-width: 148px;\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 --locale-selector-background-color: var(--color-white);\n --metric-toggle-lines-fill: #7F7F7F;\n --metric-value-font-size: 28px;\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 6px 0 -2px;\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: 16px;\n --report-icon-size: var(--score-icon-background-size);\n --report-line-height: 24px;\n --report-min-width: 400px;\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-width: calc(60 * var(--report-font-size));\n --score-container-padding: 8px;\n --score-icon-background-size: 24px;\n --score-icon-margin-left: 4px;\n --score-icon-margin-right: 12px;\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 --scores-container-padding: 20px 0 20px 0;\n --scorescale-height: 6px;\n --scorescale-width: 18px;\n --screenshot-overlay-background: rgba(0, 0, 0, 0.3);\n --section-padding-vertical: 12px;\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(0, 0%, 75%, 0.1);\n --tools-icon-color: var(--color-gray-600);\n --topbar-background-color: var(--color-gray-100);\n --topbar-height: 32px;\n --topbar-logo-size: 24px;\n --topbar-padding: 0 8px;\n --toplevel-warning-background-color: var(--color-orange-50);\n --toplevel-warning-message-text-color: #BD4200;\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,\');\n --plugin-icon-url: url(\'data:image/svg+xml;utf8,\');\n\n --pass-icon-url: url(\'data:image/svg+xml;utf8,check\');\n --average-icon-url: url(\'data:image/svg+xml;utf8,info\');\n --fail-icon-url: url(\'data:image/svg+xml;utf8,warn\');\n\n --pwa-installable-gray-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-color-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-color-url: url(\'data:image/svg+xml;utf8,\');\n\n --swap-locale-icon-url: url(\'data:image/svg+xml;utf8,\');\n}\n\n@media not print {\n .lh-vars.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 --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 \t--toplevel-warning-background-color: #544B40;\n \t--toplevel-warning-message-text-color: var(--color-orange-700);\n\t--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: 24px;\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 -webkit-font-smoothing: antialiased;\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(--color-informative);\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 font-size: var(--report-font-size);\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: 50px;\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-right: 5px;\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,\');\n}\n.lh-report-icon--copy::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--open::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--download::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--dark::before {\n background-image:url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--treemap::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--date::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--devices::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--world::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--stopwatch::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--networkspeed::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-one::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-many::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--chrome::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n\n\n\n.lh-buttons {\n display: flex;\n flex-wrap: wrap;\n}\n.lh-button {\n margin: 10px;\n height: 30px;\n border: 1px solid var(--color-gray-600);\n border-radius: 4px;\n font-weight: bold;\n color: rgb(26, 115, 232);\n background-color: var(--report-background-color);\n}\n.lh-button:first-of-type {\n margin-left: 0;\n}\n.lh-dark .lh-button {\n color: var(--color-blue-200);\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 font-size: var(--report-font-size);\n text-align: center;\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 font-weight: 500;\n padding: var(--audit-padding-vertical) 0;\n}\n\n.lh-audit--load-opportunity .lh-audit__header {\n display: block;\n}\n\n\n.lh-metricfilter {\n text-align: right;\n margin-top: var(--default-padding);\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 border: solid 1px var(--color-gray-400);\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n cursor: pointer;\n font-size: 90%;\n}\n\n.lh-metricfilter__label:first-of-type {\n border-top-left-radius: 5px;\n border-bottom-left-radius: 5px;\n margin-left: 5px;\n}\n.lh-metricfilter__label:last-of-type {\n border-top-right-radius: 5px;\n border-bottom-right-radius: 5px;\n}\n\n.lh-metricfilter__label--active {\n background: var(--color-blue-A700);\n color: var(--color-white);\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/* 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-audit-group > summary,\n.lh-expandable-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-audit-group > summary::-webkit-details-marker,\n.lh-expandable-details > summary::-webkit-details-marker {\n display: none;\n}\n\n/* Perf Metric */\n\n.lh-metrics-container {\n display: grid;\n grid-template-rows: 1fr 1fr 1fr;\n grid-template-columns: 1fr 1fr;\n grid-auto-flow: column;\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@media screen and (min-width: 640px) {\n .lh-metric:nth-child(3n+3) {\n border-bottom: 1px solid var(--report-border-color-secondary);\n }\n}\n\n@media screen and (max-width: 640px) {\n .lh-metrics-container {\n display: block;\n }\n\n .lh-metric:nth-last-child(-n+1) {\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: 10px 0;\n}\n\n.lh-metric__details {\n order: -1;\n}\n\n.lh-metric__title {\n flex: 1;\n font-weight: 500;\n}\n\n.lh-metrics__disclaimer {\n color: var(--color-gray-600);\n margin: var(--section-padding-vertical) 0;\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 font-weight: 500;\n grid-column-start: 2;\n}\n\n/* Change the grid to 3 columns for narrow viewport. */\n@media screen and (max-width: 535px) {\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.lh-metrics-toggle__input {\n cursor: pointer;\n opacity: 0;\n position: absolute;\n right: 0;\n width: 74px;\n height: 28px;\n top: -3px;\n}\n.lh-metrics-toggle__label {\n display: flex;\n background-color: #eee;\n border-radius: 20px;\n overflow: hidden;\n position: absolute;\n right: 0;\n top: -3px;\n pointer-events: none;\n}\n.lh-metrics-toggle__input:focus + label {\n outline: -webkit-focus-ring-color auto 3px;\n}\n.lh-metrics-toggle__icon {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n}\n.lh-metrics-toggle__input:not(:checked) + label .lh-metrics-toggle__icon--less,\n.lh-metrics-toggle__input:checked + label .lh-metrics-toggle__icon--more {\n background-color: var(--color-blue-A700);\n --metric-toggle-lines-fill: var(--color-white);\n}\n.lh-metrics-toggle__lines {\n fill: var(--metric-toggle-lines-fill);\n}\n\n.lh-metrics-toggle__label {\n background-color: var(--metrics-toggle-background-color);\n}\n\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--less {\n padding-left: 8px;\n}\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--more {\n padding-right: 8px;\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}\n.lh-audit-group--metrics .lh-audit-group__header span.lh-audit-group__title {\n flex: 1;\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: flex;\n flex-direction: row;\n justify-content: space-between;\n padding-bottom: var(--default-padding);\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@media screen and (max-width: 750px) {\n .lh-filmstrip {\n flex-wrap: wrap;\n }\n .lh-filmstrip__frame {\n width: 20%;\n margin-bottom: 5px;\n }\n .lh-filmstrip__thumbnail {\n display: block;\n margin: auto;\n }\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-table {\n width: 100%;\n margin: 16px 0px 16px 0px;\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 margin-top: calc(var(--category-padding) * 1.5);\n margin-bottom: var(--category-padding);\n}\n\n.lh-audit-group__itemcount {\n color: var(--color-gray-600);\n font-weight: bold;\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 font-size: var(--report-font-size);\n margin: 0 0 var(--audit-group-padding-vertical);\n /* When the header takes 100% width, the chevron becomes small. */\n max-width: calc(100% - var(--chevron-size));\n}\n/* max-width makes the metric toggle not flush. metrics doesn\'t have a chevron so unset. */\n.lh-audit-group--metrics .lh-audit-group__header {\n max-width: unset;\n}\n\n.lh-audit-group__header span.lh-audit-group__title {\n font-weight: bold;\n}\n\n.lh-audit-group__header span.lh-audit-group__itemcount {\n font-weight: bold;\n color: var(--color-gray-600);\n}\n\n.lh-audit-group__header span.lh-audit-group__description {\n font-weight: 500;\n color: var(--color-gray-600);\n}\n.lh-audit-group__header span.lh-audit-group__description::before {\n content: \'β€”\';\n margin: 0px var(--audit-margin-horizontal);\n}\n\n.lh-clump > .lh-audit-group__header,\n.lh-audit-group--diagnostics .lh-audit-group__header,\n.lh-audit-group--load-opportunities .lh-audit-group__header,\n.lh-audit-group--metrics .lh-audit-group__header,\n.lh-audit-group--pwa-installable .lh-audit-group__header,\n.lh-audit-group--pwa-optimized .lh-audit-group__header {\n margin-top: var(--audit-group-padding-vertical);\n}\n\n/* Our report design omits the header \'Metrics\', as they\'re clearly the metrics.\n It\'s more straightforward to hide w/ CSS vs change categoryRenderer.renderAuditGroup for this case. */\n.lh-audit-group--metrics > .lh-audit-group__header {\n /* Also, it\'s vis hidden (and not display:none) because of spacing regarding the pill. */\n visibility: hidden;\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);\n margin: var(--audit-padding-vertical) 0;\n padding: calc(var(--audit-padding-vertical) / 2) calc(var(--audit-description-padding-left));\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 max-width: calc(var(--report-width) - var(--category-padding) * 2);\n background-color: var(--toplevel-warning-background-color);\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);\n fill: var(--color-pass);\n stroke: var(--color-pass);\n}\n\n.lh-gauge__wrapper--average {\n color: var(--color-average);\n fill: var(--color-average);\n stroke: var(--color-average);\n}\n\n.lh-gauge__wrapper--fail {\n color: var(--color-fail);\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);\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);\n}\n.lh-fraction__wrapper--average .lh-fraction__background {\n background-color: var(--color-average);\n}\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 line-height: var(--gauge-label-line-height);\n margin-top: 10px;\n text-align: center;\n color: var(--report-text-color);\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 margin: 12px auto 0 auto;\n border: 1px solid var(--color-gray-200);\n border-radius: 20px;\n padding: 8px 8px;\n}\n\n.lh-scorescale-range {\n display: flex;\n align-items: center;\n margin: 0 12px;\n font-family: var(--report-font-family-monospace);\n white-space: nowrap;\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-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\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}\n\n.lh-category-header__finalscreenshot .lh-scorescale {\n border: 0;\n padding: 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-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}\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: 535px) {\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));\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--odd {\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: 8px 6px;\n}\n.lh-table th:first-child {\n padding-left: 0;\n}\n.lh-table th:last-child {\n padding-right: 0;\n}\n\n.lh-table tr {\n vertical-align: middle;\n}\n\n/* Looks unnecessary, but mostly for keeping the 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 padding: 0;\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-audit-group > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,\n.lh-audit > .lh-expandable-details .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {\n transform: rotate(var(--chevron-angle-right));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {\n transform: rotate(var(--chevron-angle));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__lines {\n transform: translateY(calc(var(--chevron-size) * -1));\n}\n\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 display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n background-color: var(--env-item-background-color);\n border-radius: 3px;\n padding: 0;\n font-size: 13px;\n padding-bottom: calc(var(--default-padding) / 2);\n}\n\n.lh-meta__item {\n display: block;\n list-style-type: none;\n position: relative;\n padding: calc(var(--default-padding) / 2) 0 0 calc(var(--default-padding) * 3);\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: calc(var(--report-icon-size) * 0.8);\n height: calc(var(--report-icon-size) * 0.8);\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'); + 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-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: #018642;\n --color-green: #0CCE6B;\n --color-lime-400: #D3E156;\n --color-orange-50: #FFF3E0;\n --color-orange-700: #D04900;\n --color-orange: #FFA400;\n --color-red-700: #EB0F00;\n --color-red: #FF4E42;\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: 40px;\n --audit-group-padding-vertical: 8px;\n --audit-margin-horizontal: 5px;\n --audit-padding-vertical: 8px;\n --category-padding: 40px;\n --chevron-line-stroke: var(--color-gray-600);\n --chevron-size: 12px;\n --default-padding: 12px;\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: 112px;\n --gauge-circle-size: 80px;\n --gauge-circle-size-sm: 36px;\n --gauge-label-font-size-big: 28px;\n --gauge-label-font-size: 20px;\n --gauge-label-line-height-big: 36px;\n --gauge-label-line-height: 26px;\n --gauge-percentage-font-size-big: 38px;\n --gauge-percentage-font-size: 28px;\n --gauge-wrapper-width: 148px;\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 --locale-selector-background-color: var(--color-white);\n --metric-toggle-lines-fill: #7F7F7F;\n --metric-value-font-size: 28px;\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 6px 0 -2px;\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: 16px;\n --report-icon-size: var(--score-icon-background-size);\n --report-line-height: 24px;\n --report-min-width: 400px;\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-width: calc(60 * var(--report-font-size));\n --score-container-padding: 8px;\n --score-icon-background-size: 24px;\n --score-icon-margin-left: 4px;\n --score-icon-margin-right: 12px;\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 --scores-container-padding: 20px 0 20px 0;\n --scorescale-height: 6px;\n --scorescale-width: 18px;\n --screenshot-overlay-background: rgba(0, 0, 0, 0.3);\n --section-padding-vertical: 12px;\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(0, 0%, 75%, 0.1);\n --tools-icon-color: var(--color-gray-600);\n --topbar-background-color: var(--color-gray-100);\n --topbar-height: 32px;\n --topbar-logo-size: 24px;\n --topbar-padding: 0 8px;\n --toplevel-warning-background-color: var(--color-orange-50);\n --toplevel-warning-message-text-color: #BD4200;\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,\');\n --plugin-icon-url: url(\'data:image/svg+xml;utf8,\');\n\n --pass-icon-url: url(\'data:image/svg+xml;utf8,check\');\n --average-icon-url: url(\'data:image/svg+xml;utf8,info\');\n --fail-icon-url: url(\'data:image/svg+xml;utf8,warn\');\n\n --pwa-installable-gray-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-gray-url-dark: url(\'data:image/svg+xml;utf8,\');\n\n --pwa-installable-color-url: url(\'data:image/svg+xml;utf8,\');\n --pwa-optimized-color-url: url(\'data:image/svg+xml;utf8,\');\n\n --swap-locale-icon-url: url(\'data:image/svg+xml;utf8,\');\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 --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 \t--toplevel-warning-background-color: #544B40;\n \t--toplevel-warning-message-text-color: var(--color-orange-700);\n\t--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: 24px;\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 -webkit-font-smoothing: antialiased;\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(--color-informative);\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 font-size: var(--report-font-size);\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: 50px;\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-right: 5px;\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,\');\n}\n.lh-report-icon--copy::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--open::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--download::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--dark::before {\n background-image:url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--treemap::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--date::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--devices::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--world::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--stopwatch::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--networkspeed::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-one::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--samples-many::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n.lh-report-icon--chrome::before {\n background-image: url(\'data:image/svg+xml;utf8,\');\n}\n\n\n\n.lh-buttons {\n display: flex;\n flex-wrap: wrap;\n}\n.lh-button {\n margin: 10px;\n height: 30px;\n border: 1px solid var(--color-gray-600);\n border-radius: 4px;\n font-weight: bold;\n color: rgb(26, 115, 232);\n background-color: var(--report-background-color);\n}\n.lh-button:first-of-type {\n margin-left: 0;\n}\n.lh-dark .lh-button {\n color: var(--color-blue-200);\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 font-size: var(--report-font-size);\n text-align: center;\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 font-weight: 500;\n padding: var(--audit-padding-vertical) 0;\n}\n\n.lh-audit--load-opportunity .lh-audit__header {\n display: block;\n}\n\n\n.lh-metricfilter {\n text-align: right;\n margin-top: var(--default-padding);\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 border: solid 1px var(--color-gray-400);\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n cursor: pointer;\n font-size: 90%;\n}\n\n.lh-metricfilter__label:first-of-type {\n border-top-left-radius: 5px;\n border-bottom-left-radius: 5px;\n margin-left: 5px;\n}\n.lh-metricfilter__label:last-of-type {\n border-top-right-radius: 5px;\n border-bottom-right-radius: 5px;\n}\n\n.lh-metricfilter__label--active {\n background: var(--color-blue-A700);\n color: var(--color-white);\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/* 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-audit-group > summary,\n.lh-expandable-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-audit-group > summary::-webkit-details-marker,\n.lh-expandable-details > summary::-webkit-details-marker {\n display: none;\n}\n\n/* Perf Metric */\n\n.lh-metrics-container {\n display: grid;\n grid-template-rows: 1fr 1fr 1fr;\n grid-template-columns: 1fr 1fr;\n grid-auto-flow: column;\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@media screen and (min-width: 640px) {\n .lh-metric:nth-child(3n+3) {\n border-bottom: 1px solid var(--report-border-color-secondary);\n }\n}\n\n@media screen and (max-width: 640px) {\n .lh-metrics-container {\n display: block;\n }\n\n .lh-metric:nth-last-child(-n+1) {\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: 10px 0;\n}\n\n.lh-metric__details {\n order: -1;\n}\n\n.lh-metric__title {\n flex: 1;\n font-weight: 500;\n}\n\n.lh-metrics__disclaimer {\n color: var(--color-gray-600);\n margin: var(--section-padding-vertical) 0;\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 font-weight: 500;\n grid-column-start: 2;\n}\n\n/* Change the grid to 3 columns for narrow viewport. */\n@media screen and (max-width: 535px) {\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.lh-metrics-toggle__input {\n cursor: pointer;\n opacity: 0;\n position: absolute;\n right: 0;\n width: 74px;\n height: 28px;\n top: -3px;\n}\n.lh-metrics-toggle__label {\n display: flex;\n background-color: #eee;\n border-radius: 20px;\n overflow: hidden;\n position: absolute;\n right: 0;\n top: -3px;\n pointer-events: none;\n}\n.lh-metrics-toggle__input:focus + label {\n outline: -webkit-focus-ring-color auto 3px;\n}\n.lh-metrics-toggle__icon {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n width: 50%;\n height: 28px;\n}\n.lh-metrics-toggle__input:not(:checked) + label .lh-metrics-toggle__icon--less,\n.lh-metrics-toggle__input:checked + label .lh-metrics-toggle__icon--more {\n background-color: var(--color-blue-A700);\n --metric-toggle-lines-fill: var(--color-white);\n}\n.lh-metrics-toggle__lines {\n fill: var(--metric-toggle-lines-fill);\n}\n\n.lh-metrics-toggle__label {\n background-color: var(--metrics-toggle-background-color);\n}\n\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--less {\n padding-left: 8px;\n}\n.lh-metrics-toggle__label .lh-metrics-toggle__icon--more {\n padding-right: 8px;\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}\n.lh-audit-group--metrics .lh-audit-group__header span.lh-audit-group__title {\n flex: 1;\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: flex;\n flex-direction: row;\n justify-content: space-between;\n padding-bottom: var(--default-padding);\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@media screen and (max-width: 750px) {\n .lh-filmstrip {\n flex-wrap: wrap;\n }\n .lh-filmstrip__frame {\n width: 20%;\n margin-bottom: 5px;\n }\n .lh-filmstrip__thumbnail {\n display: block;\n margin: auto;\n }\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-table {\n width: 100%;\n margin: 16px 0px 16px 0px;\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 margin-top: calc(var(--category-padding) * 1.5);\n margin-bottom: var(--category-padding);\n}\n\n.lh-audit-group__itemcount {\n color: var(--color-gray-600);\n font-weight: bold;\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 font-size: var(--report-font-size);\n margin: 0 0 var(--audit-group-padding-vertical);\n /* When the header takes 100% width, the chevron becomes small. */\n max-width: calc(100% - var(--chevron-size));\n}\n/* max-width makes the metric toggle not flush. metrics doesn\'t have a chevron so unset. */\n.lh-audit-group--metrics .lh-audit-group__header {\n max-width: unset;\n}\n\n.lh-audit-group__header span.lh-audit-group__title {\n font-weight: bold;\n}\n\n.lh-audit-group__header span.lh-audit-group__itemcount {\n font-weight: bold;\n color: var(--color-gray-600);\n}\n\n.lh-audit-group__header span.lh-audit-group__description {\n font-weight: 500;\n color: var(--color-gray-600);\n}\n.lh-audit-group__header span.lh-audit-group__description::before {\n content: \'β€”\';\n margin: 0px var(--audit-margin-horizontal);\n}\n\n.lh-clump > .lh-audit-group__header,\n.lh-audit-group--diagnostics .lh-audit-group__header,\n.lh-audit-group--load-opportunities .lh-audit-group__header,\n.lh-audit-group--metrics .lh-audit-group__header,\n.lh-audit-group--pwa-installable .lh-audit-group__header,\n.lh-audit-group--pwa-optimized .lh-audit-group__header {\n margin-top: var(--audit-group-padding-vertical);\n}\n\n/* Our report design omits the header \'Metrics\', as they\'re clearly the metrics.\n It\'s more straightforward to hide w/ CSS vs change categoryRenderer.renderAuditGroup for this case. */\n.lh-audit-group--metrics > .lh-audit-group__header {\n /* Also, it\'s vis hidden (and not display:none) because of spacing regarding the pill. */\n visibility: hidden;\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);\n margin: var(--audit-padding-vertical) 0;\n padding: calc(var(--audit-padding-vertical) / 2) calc(var(--audit-description-padding-left));\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 max-width: calc(var(--report-width) - var(--category-padding) * 2);\n background-color: var(--toplevel-warning-background-color);\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);\n fill: var(--color-pass);\n stroke: var(--color-pass);\n}\n\n.lh-gauge__wrapper--average {\n color: var(--color-average);\n fill: var(--color-average);\n stroke: var(--color-average);\n}\n\n.lh-gauge__wrapper--fail {\n color: var(--color-fail);\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);\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);\n}\n.lh-fraction__wrapper--average .lh-fraction__background {\n background-color: var(--color-average);\n}\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 line-height: var(--gauge-label-line-height);\n margin-top: 10px;\n text-align: center;\n color: var(--report-text-color);\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 margin: 12px auto 0 auto;\n border: 1px solid var(--color-gray-200);\n border-radius: 20px;\n padding: 8px 8px;\n}\n\n.lh-scorescale-range {\n display: flex;\n align-items: center;\n margin: 0 12px;\n font-family: var(--report-font-family-monospace);\n white-space: nowrap;\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-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\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}\n\n.lh-category-header__finalscreenshot .lh-scorescale {\n border: 0;\n padding: 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-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}\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: 535px) {\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));\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--odd {\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: 8px 6px;\n}\n.lh-table th:first-child {\n padding-left: 0;\n}\n.lh-table th:last-child {\n padding-right: 0;\n}\n\n.lh-table tr {\n vertical-align: middle;\n}\n\n/* Looks unnecessary, but mostly for keeping the 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 padding: 0;\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-audit-group > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,\n.lh-audit > .lh-expandable-details .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {\n transform: rotate(var(--chevron-angle-right));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {\n transform: rotate(var(--chevron-angle));\n}\n\n.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,\n.lh-audit > .lh-expandable-details[open] .lh-chevron__lines {\n transform: translateY(calc(var(--chevron-size) * -1));\n}\n\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 display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n background-color: var(--env-item-background-color);\n border-radius: 3px;\n padding: 0;\n font-size: 13px;\n padding-bottom: calc(var(--default-padding) / 2);\n}\n\n.lh-meta__item {\n display: block;\n list-style-type: none;\n position: relative;\n padding: calc(var(--default-padding) / 2) 0 0 calc(var(--default-padding) * 3);\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: calc(var(--report-icon-size) * 0.8);\n height: calc(var(--report-icon-size) * 0.8);\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'); el0.append(el1); return el0; } diff --git a/report/renderer/drop-down-menu.js b/report/renderer/drop-down-menu.js index 461238912485..a60852c68c46 100644 --- a/report/renderer/drop-down-menu.js +++ b/report/renderer/drop-down-menu.js @@ -36,11 +36,11 @@ export class DropDownMenu { * @param {function(MouseEvent): any} menuClickHandler */ setup(menuClickHandler) { - this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.rootEl); + this._toggleEl = this._dom.find('.lh-topbar button.lh-tools__button', this._dom.rootEl); this._toggleEl.addEventListener('click', this.onToggleClick); this._toggleEl.addEventListener('keydown', this.onToggleKeydown); - this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.rootEl); + this._menuEl = this._dom.find('.lh-topbar div.lh-tools__dropdown', this._dom.rootEl); this._menuEl.addEventListener('keydown', this.onMenuKeydown); this._menuEl.addEventListener('click', menuClickHandler); } diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 563ba5db4e10..45f228dd60c6 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -47,7 +47,11 @@ export class ReportRenderer { renderReport(lhr, rootEl, opts) { if (!opts) { console.warn('Please adopt the new report API in renderer/api.js.'); - this._dom.rootEl = rootEl; + const closestRoot = rootEl.closest('.lh-vars'); + if (!closestRoot || !(closestRoot instanceof HTMLElement)) { + throw new Error('Invalid DOM. Please adopt the new report API'); + } + this._dom.rootEl = closestRoot; opts = {}; } this._opts = opts; diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index 58d72798e58b..a604b106952f 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -30,7 +30,7 @@ const lighthouseRenderer = window['report']; if (!container) throw new Error('Unexpected DOM. Bailing.'); try { - const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: true}); + const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: false}); // TODO: display warnings if appropriate. for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { el.setAttribute('hidden', 'true'); From 69a7feda378e8c113ccc8b5b45a2b358e20dfa83 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 25 Oct 2021 17:55:49 -0700 Subject: [PATCH 19/41] happy tests --- build/build-report.js | 2 +- report/clients/standalone.js | 6 +++-- report/renderer/api.js | 4 ++-- report/renderer/dom.js | 9 ++++++++ report/renderer/drop-down-menu.js | 6 ++--- report/renderer/report-renderer.js | 23 +++++++++++++------ report/renderer/report-ui-features.js | 9 ++++---- report/renderer/topbar-features.js | 16 ++++++------- .../test/generator/report-generator-test.js | 1 + .../test/renderer/report-renderer-axe-test.js | 4 ++-- report/test/renderer/report-renderer-test.js | 20 ++++++++-------- .../test/renderer/report-ui-features-test.js | 4 ++-- 12 files changed, 62 insertions(+), 42 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 25b0856d9d0c..7ddf24179cf4 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -45,7 +45,7 @@ async function buildStandaloneReport() { input: 'report/clients/standalone.js', plugins: [ rollupPlugins.commonjs(), - // rollupPlugins.terser(), + rollupPlugins.terser(), ], }); diff --git a/report/clients/standalone.js b/report/clients/standalone.js index 91ca556ba8c3..25f90d2d24ea 100644 --- a/report/clients/standalone.js +++ b/report/clients/standalone.js @@ -27,11 +27,13 @@ function __initLighthouseReport__() { /** @type {LH.ReportResult} */ // @ts-expect-error const lhr = window.__LIGHTHOUSE_JSON__; - renderer.renderReport(lhr, container); + + const opts = {omitTopbar: false}; + renderer.renderReport(lhr, container, opts); // Hook in JS features and page-level event listeners after the report // is in the document. - const features = new ReportUIFeatures(dom); + const features = new ReportUIFeatures(dom, opts); features.initFeatures(lhr); document.addEventListener('lh-analytics', /** @param {Event} e */ e => { diff --git a/report/renderer/api.js b/report/renderer/api.js index 0ddb8f000637..71c0a2c74d8c 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -15,14 +15,14 @@ import {ReportUIFeatures} from '../renderer/report-ui-features.js'; * @param {LH.ReportRendererOptions} opts * @return {HTMLElement} */ -export function renderReport(lhr, opts) { +export function renderReport(lhr, opts = {}) { const rootEl = document.createElement('main'); rootEl.classList.add('lh-root', 'lh-vars'); const dom = new DOM(rootEl.ownerDocument, rootEl); const renderer = new ReportRenderer(dom); - renderer.renderReport(lhr, rootEl, {omitTopbar: true}); + renderer.renderReport(lhr, rootEl, opts); // Hook in JS features and page-level event listeners after the report // is in the document. diff --git a/report/renderer/dom.js b/report/renderer/dom.js index db850dfe2565..6957fe3bec1a 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -228,6 +228,15 @@ export class DOM { this._lighthouseChannel = lighthouseChannel; } + /** + * ONLY use if `dom.rootEl` isn't sufficient for your needs. It is preferred for all scoping, + * as a document can have multiple reports within it. + * @return {Document} + */ + document() { + return this._document; + } + /** * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this * @return {boolean} diff --git a/report/renderer/drop-down-menu.js b/report/renderer/drop-down-menu.js index a60852c68c46..67204f522ca8 100644 --- a/report/renderer/drop-down-menu.js +++ b/report/renderer/drop-down-menu.js @@ -48,12 +48,12 @@ export class DropDownMenu { close() { this._toggleEl.classList.remove('lh-active'); this._toggleEl.setAttribute('aria-expanded', 'false'); - if (this._menuEl.contains(this._dom._document.activeElement)) { + if (this._menuEl.contains(this._dom.document().activeElement)) { // Refocus on the tools button if the drop down last had focus this._toggleEl.focus(); } this._menuEl.removeEventListener('focusout', this.onMenuFocusOut); - this._dom.rootEl.removeEventListener('keydown', this.onDocumentKeyDown); + this._dom.document().removeEventListener('keydown', this.onDocumentKeyDown); } /** @@ -73,7 +73,7 @@ export class DropDownMenu { this._toggleEl.classList.add('lh-active'); this._toggleEl.setAttribute('aria-expanded', 'true'); this._menuEl.addEventListener('focusout', this.onMenuFocusOut); - this._dom.rootEl.addEventListener('keydown', this.onDocumentKeyDown); + this._dom.document().addEventListener('keydown', this.onDocumentKeyDown); } /** diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 45f228dd60c6..6244a240034e 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -36,6 +36,8 @@ export class ReportRenderer { constructor(dom) { /** @type {DOM} */ this._dom = dom; + /** @type {{omitTopbar?: Boolean}} */ + this._opts = {}; } /** @@ -45,16 +47,23 @@ export class ReportRenderer { * @return {!Element} */ renderReport(lhr, rootEl, opts) { - if (!opts) { - console.warn('Please adopt the new report API in renderer/api.js.'); + // Allow old report rendering API + if (!this._dom.rootEl) { + // @ts-expect-error + const isUnderTest = () => !!process.env.CI || process.env.NODE_ENV === 'test'; + + if (!isUnderTest()) { + console.warn('Please adopt the new report API in renderer/api.js.'); + } const closestRoot = rootEl.closest('.lh-vars'); - if (!closestRoot || !(closestRoot instanceof HTMLElement)) { - throw new Error('Invalid DOM. Please adopt the new report API'); + if (!closestRoot) { + rootEl.classList.add('lh-root', 'lh-vars'); } - this._dom.rootEl = closestRoot; - opts = {}; + this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot); + } + if (opts) { + this._opts = opts; } - this._opts = opts; this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown'); diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index bc1b3bc86888..bdd53fe55ed3 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -123,10 +123,9 @@ export class ReportUIFeatures { * @param {{text: string, icon?: string, onClick: () => void}} opts */ addButton(opts) { - // report-ui-features doesn't have a reference to the root report el, and PSI has - // 2 reports on the page (and not even attached to DOM when installFeatures is called..) - // so we need a container option to specify where the element should go. - const metricsEl = this._dom.find('.lh-audit-group--metrics', this._dom.rootEl); + // Use qSA directly to as we don't want to throw (if this element is missing). + const metricsEl = this._dom.rootEl.querySelector('.lh-audit-group--metrics'); + if (!metricsEl) return; let buttonsEl = metricsEl.querySelector('.lh-buttons'); if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons'); @@ -152,7 +151,7 @@ export class ReportUIFeatures { if (this._topbar) { this._topbar.resetUIState(); } - return this._document.documentElement.outerHTML; + return `${this._dom.rootEl.outerHTML}`; } /** diff --git a/report/renderer/topbar-features.js b/report/renderer/topbar-features.js index 02fbf4a19bb1..71c733862392 100644 --- a/report/renderer/topbar-features.js +++ b/report/renderer/topbar-features.js @@ -47,7 +47,7 @@ export class TopbarFeatures { enable(lhr) { this.lhr = lhr; this._dom.rootEl.addEventListener('keyup', this.onKeyUp); - this._dom.rootEl.addEventListener('copy', this.onCopy); + this._dom.document().addEventListener('copy', this.onCopy); this._dropDownMenu.setup(this.onDropDownMenuClick); this._setUpCollapseDetailsAfterPrinting(); @@ -110,7 +110,7 @@ export class TopbarFeatures { try { this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'})); } catch (e) { - this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { + this._dom.fireEventOn('lh-log', this._dom.document(), { cmd: 'error', msg: 'Could not export as HTML. ' + e.message, }); } @@ -150,7 +150,7 @@ export class TopbarFeatures { e.preventDefault(); e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2)); - this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { + this._dom.fireEventOn('lh-log', this._dom.document(), { cmd: 'log', msg: 'Report JSON copied to clipboard', }); } @@ -162,28 +162,28 @@ export class TopbarFeatures { * Copies the report JSON to the clipboard (if supported by the browser). */ onCopyButtonClick() { - this._dom.fireEventOn('lh-analytics', this._dom.rootEl.ownerDocument, { + this._dom.fireEventOn('lh-analytics', this._dom.document(), { cmd: 'send', fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'}, }); try { - if (this._dom.rootEl.ownerDocument.queryCommandSupported('copy')) { + if (this._dom.document().queryCommandSupported('copy')) { this._copyAttempt = true; // Note: In Safari 10.0.1, execCommand('copy') returns true if there's // a valid text selection on the page. See http://caniuse.com/#feat=clipboard. - if (!this._dom.rootEl.ownerDocument.execCommand('copy')) { + if (!this._dom.document().execCommand('copy')) { this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt. - this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, { + this._dom.fireEventOn('lh-log', this._dom.document(), { cmd: 'warn', msg: 'Your browser does not support copy to clipboard.', }); } } } catch (e) { this._copyAttempt = false; - this._dom.fireEventOn('lh-log', this._dom.rootEl.ownerDocument, {cmd: 'log', msg: e.message}); + this._dom.fireEventOn('lh-log', this._dom.document(), {cmd: 'log', msg: e.message}); } } diff --git a/report/test/generator/report-generator-test.js b/report/test/generator/report-generator-test.js index 373b09befd82..4ea34e13dbcd 100644 --- a/report/test/generator/report-generator-test.js +++ b/report/test/generator/report-generator-test.js @@ -60,6 +60,7 @@ describe('ReportGenerator', () => { it('should inject the report renderer javascript', () => { const result = ReportGenerator.generateReportHtml({}); + require('fs').writeFileSync('omg.txt', result, 'utf-8'); assert.ok(result.includes('configSettings.channel||"unknown"'), 'injects the script'); assert.ok(result.includes('robustness: <\\/script'), 'escapes HTML tags in javascript'); assert.ok(result.includes('pre$`post'), 'does not break from String.replace'); diff --git a/report/test/renderer/report-renderer-axe-test.js b/report/test/renderer/report-renderer-axe-test.js index 6c2957cd29c9..f351ae538e53 100644 --- a/report/test/renderer/report-renderer-axe-test.js +++ b/report/test/renderer/report-renderer-axe-test.js @@ -47,10 +47,10 @@ describe('ReportRendererAxe', () => { }); it('renders without axe violations', async () => { - const container = renderer._dom._document.createElement('main'); + const container = renderer._dom.document().createElement('main'); const output = renderer.renderReport(sampleResults, container); - renderer._dom._document.body.appendChild(container); + renderer._dom.document().body.appendChild(container); const config = { rules: { diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 6237632bd715..fa466aace40b 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -50,7 +50,7 @@ describe('ReportRenderer', () => { describe('renderReport', () => { it('should render a report', () => { - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(sampleResults, container); assert.ok(output.querySelector('.lh-header-container'), 'has a header'); assert.ok(output.querySelector('.lh-report'), 'has report body'); @@ -60,7 +60,7 @@ describe('ReportRenderer', () => { }); it('renders additional reports by replacing the existing one', () => { - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const oldReport = Array.from(renderer.renderReport(sampleResults, container).children); const newReport = Array.from(renderer.renderReport(sampleResults, container).children); assert.ok(!oldReport.find(node => container.contains(node)), 'old report was removed'); @@ -86,7 +86,7 @@ describe('ReportRenderer', () => { auditRefs: [], }; - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(sampleResultsCopy, container); function isPWAGauge(el) { @@ -126,7 +126,7 @@ describe('ReportRenderer', () => { title: 'Some Plugin', auditRefs: [], }; - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(sampleResultsCopy, container); const scoresHeaderElem = output.querySelector('.lh-scores-header'); @@ -140,7 +140,7 @@ describe('ReportRenderer', () => { }); it('should not mutate a report object', () => { - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const originalResults = JSON.parse(JSON.stringify(sampleResults)); renderer.renderReport(sampleResults, container); assert.deepStrictEqual(sampleResults, originalResults); @@ -148,13 +148,13 @@ describe('ReportRenderer', () => { it('renders no warning section when no lighthouseRunWarnings occur', () => { const warningResults = Object.assign({}, sampleResults, {runWarnings: []}); - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(warningResults, container); assert.strictEqual(output.querySelector('.lh-warnings--toplevel'), null); }); it('renders a warning section', () => { - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(sampleResults, container); const warningEls = output.querySelectorAll('.lh-warnings--toplevel > ul > li'); @@ -165,7 +165,7 @@ describe('ReportRenderer', () => { const warningResults = Object.assign({}, sampleResults, { runWarnings: ['[I am a link](https://example.com/)'], }); - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(warningResults, container); const warningEls = output.querySelectorAll('.lh-warnings--toplevel ul li a'); @@ -196,7 +196,7 @@ describe('ReportRenderer', () => { // Make sure we have a channel in the LHR. assert.ok(lhrChannel.length > 2); - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const output = renderer.renderReport(sampleResults, container); const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev']; @@ -225,7 +225,7 @@ describe('ReportRenderer', () => { assert.ok(notApplicableCount > 20); // Make sure something's being tested. - const container = renderer._dom.rootEl; + const container = renderer._dom.document().body; const reportElement = renderer.renderReport(sampleResults, container); const notApplicableElementCount = reportElement .querySelectorAll('.lh-audit--notapplicable').length; diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 355c15ed3d63..3f0a76458ba9 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -33,7 +33,7 @@ describe('ReportUIFeatures', () => { const categoryRenderer = new CategoryRenderer(dom, detailsRenderer); const renderer = new ReportRenderer(dom, categoryRenderer); const reportUIFeatures = new ReportUIFeatures(dom); - const container = dom.find('main', dom.rootEl); + const container = dom.find('main', dom._document); renderer.renderReport(lhr, container); reportUIFeatures.initFeatures(lhr); return container; @@ -351,7 +351,7 @@ describe('ReportUIFeatures', () => { assert.ok(dropDown._toggleEl.classList.contains('lh-active')); const escape = new window.KeyboardEvent('keydown', {keyCode: /* ESC */ 27}); - dropDown._toggleEl.dispatchEvent(escape); + dom.document().dispatchEvent(escape); assert.ok(!dropDown._toggleEl.classList.contains('lh-active')); }); From cc6f08ab7fca2612de6c43c937497a8a1f777386 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 27 Oct 2021 22:39:15 -0700 Subject: [PATCH 20/41] fix types --- report/renderer/api.js | 2 +- report/renderer/report-renderer.js | 11 +++------- report/renderer/report-ui-features.js | 2 +- report/test-assets/faux-psi.js | 2 +- report/test/clients/bundle-test.js | 3 +++ report/test/renderer/report-renderer-test.js | 5 +++++ .../test/renderer/report-ui-features-test.js | 3 +++ report/types/html-renderer.d.ts | 2 ++ report/types/report-renderer.d.ts | 21 ++++++++++--------- 9 files changed, 30 insertions(+), 21 deletions(-) diff --git a/report/renderer/api.js b/report/renderer/api.js index 71c0a2c74d8c..8fae0d0b43c1 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -12,7 +12,7 @@ import {ReportUIFeatures} from '../renderer/report-ui-features.js'; /** * @param {LH.Result} lhr - * @param {LH.ReportRendererOptions} opts + * @param {LH.Renderer.Options} opts * @return {HTMLElement} */ export function renderReport(lhr, opts = {}) { diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 19ef03684949..67b54eb06297 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -36,25 +36,20 @@ export class ReportRenderer { constructor(dom) { /** @type {DOM} */ this._dom = dom; - /** @type {{omitTopbar?: Boolean}} */ + /** @type {LH.Renderer.Options} */ this._opts = {}; } /** * @param {LH.Result} lhr * @param {HTMLElement} rootEl Report root element containing the report - * @param {{omitTopbar?: Boolean}=} opts + * @param {LH.Renderer.Options=} opts * @return {!Element} */ renderReport(lhr, rootEl, opts) { // Allow old report rendering API if (!this._dom.rootEl) { - // @ts-expect-error - const isUnderTest = () => !!process.env.CI || process.env.NODE_ENV === 'test'; - - if (!isUnderTest()) { - console.warn('Please adopt the new report API in renderer/api.js.'); - } + console.warn('Please adopt the new report API in renderer/api.js.'); const closestRoot = rootEl.closest('.lh-vars'); if (!closestRoot) { rootEl.classList.add('lh-root', 'lh-vars'); diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index bdd53fe55ed3..007e9a612b78 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -40,7 +40,7 @@ function getTableRows(tableEl) { export class ReportUIFeatures { /** * @param {DOM} dom - * @param {{omitTopbar?: Boolean}} opts + * @param {LH.Renderer.Options} opts */ constructor(dom, opts = {}) { /** @type {LH.Result} */ diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index a604b106952f..58d72798e58b 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -30,7 +30,7 @@ const lighthouseRenderer = window['report']; if (!container) throw new Error('Unexpected DOM. Bailing.'); try { - const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: false}); + const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: true}); // TODO: display warnings if appropriate. for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { el.setAttribute('hidden', 'true'); diff --git a/report/test/clients/bundle-test.js b/report/test/clients/bundle-test.js index 7f729bea9b1c..b61dd678e78f 100644 --- a/report/test/clients/bundle-test.js +++ b/report/test/clients/bundle-test.js @@ -9,6 +9,7 @@ import fs from 'fs'; import jsdom from 'jsdom'; +import {jest} from '@jest/globals'; import * as lighthouseRenderer from '../../clients/bundle.js'; import {LH_ROOT} from '../../../root.js'; @@ -22,6 +23,8 @@ const sampleResultsStr = describe('lighthouseRenderer bundle', () => { let document; beforeAll(() => { + global.console.warn = jest.fn(); + const {window} = new jsdom.JSDOM(); document = window.document; diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index fa466aace40b..5ddcb05735ba 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -10,6 +10,7 @@ import {strict as assert} from 'assert'; import jsdom from 'jsdom'; +import {jest} from '@jest/globals'; import {Util} from '../../renderer/util.js'; import URL from '../../../lighthouse-core/lib/url-shim.js'; @@ -26,6 +27,8 @@ describe('ReportRenderer', () => { let sampleResults; beforeAll(() => { + global.console.warn = jest.fn(); + // Stub out matchMedia for Node. global.matchMedia = function() { return { @@ -33,6 +36,8 @@ describe('ReportRenderer', () => { }; }; + // global.console.warn = jest.fn(); + const {window} = new jsdom.JSDOM(); global.self = window; diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 3f0a76458ba9..866c0aff2b56 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -10,6 +10,7 @@ import {strict as assert} from 'assert'; import jsdom from 'jsdom'; +import {jest} from '@jest/globals'; import reportAssets from '../../generator/report-assets.js'; import {Util} from '../../renderer/util.js'; @@ -40,6 +41,8 @@ describe('ReportUIFeatures', () => { } beforeAll(() => { + global.console.warn = jest.fn(); + // Stub out matchMedia for Node. global.matchMedia = function() { return { diff --git a/report/types/html-renderer.d.ts b/report/types/html-renderer.d.ts index 9d48de8937bb..f7665fd2d572 100644 --- a/report/types/html-renderer.d.ts +++ b/report/types/html-renderer.d.ts @@ -8,6 +8,7 @@ import AuditDetails from '../../types/lhr/audit-details'; import {FormattedIcu as FormattedIcu_, IcuMessage as IcuMessage_} from '../../types/lhr/i18n'; import LHResult from '../../types/lhr/lhr'; import ReportResult_ from './report-result'; +import Renderer_ from './report-renderer'; import * as Settings from '../../types/lhr/settings'; import Treemap_ from '../../types/lhr/treemap'; @@ -16,6 +17,7 @@ declare global { module LH { export import Result = LHResult; export import ReportResult = ReportResult_; + export import Renderer = Renderer_; export import Locale = Settings.Locale; export type IcuMessage = IcuMessage_; export type FormattedIcu = FormattedIcu_; diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index 7ff4ba86cd20..d4c601bacee7 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -4,15 +4,16 @@ * 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. */ -declare global { - module LH.Renderer { - export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; - // Extra convience if you have just a category score. - export function renderGaugeForScore(num0to1: number): HTMLElement; - } + import {Result as AuditResult} from '../../types/lhr/audit-result'; + +declare module Renderer { + function renderReport(lhr: AuditResult, options?: Options): HTMLElement; - interface ReportRendererOptions { + // Extra convience if you have just a category score. + function renderGaugeForScore(num0to1: number): HTMLElement; + + interface Options { /** * DOM element that will the overlay DOM should be a child of. * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. @@ -29,7 +30,7 @@ declare global { disableAutoDarkModeAndFireworks?: boolean; /** Disable the topbar UI component */ - disableTopBar: boolean; + omitTopbar?: boolean; /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ onSaveGist?: (lhr: LH.Result) => string; @@ -39,5 +40,5 @@ declare global { } } -// empty export to keep file a module -export {}; + +export default Renderer; From c1f94cba773cbf861ce1892bfae024ea8f21616f Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 27 Oct 2021 22:43:12 -0700 Subject: [PATCH 21/41] cleanup of remainign _document references --- viewer/app/src/viewer-ui-features.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/viewer/app/src/viewer-ui-features.js b/viewer/app/src/viewer-ui-features.js index fdc91574536a..db2dc3c52971 100644 --- a/viewer/app/src/viewer-ui-features.js +++ b/viewer/app/src/viewer-ui-features.js @@ -42,7 +42,7 @@ export class ViewerUIFeatures extends ReportUIFeatures { // Disable option to save as gist if no callback for saving. if (!this._saveGistCallback) { const saveGistItem = - this._dom.find('.lh-tools__dropdown a[data-action="save-gist"]', this._document); + this._dom.find('.lh-tools__dropdown a[data-action="save-gist"]', this._dom.rootEl); saveGistItem.setAttribute('disabled', 'true'); } @@ -75,7 +75,7 @@ export class ViewerUIFeatures extends ReportUIFeatures { // Disable save-gist option after saving. const saveGistItem = - this._dom.find('.lh-tools__dropdown a[data-action="save-gist"]', this._document); + this._dom.find('.lh-tools__dropdown a[data-action="save-gist"]', this._dom.rootEl); saveGistItem.setAttribute('disabled', 'true'); } From ce7dd3e5fee79a203a9735b0d3cb6a0ce8583919 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 27 Oct 2021 22:44:07 -0700 Subject: [PATCH 22/41] drop unimplemented options. --- report/types/report-renderer.d.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index d4c601bacee7..bfb13ac012d6 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -14,16 +14,6 @@ declare module Renderer { function renderGaugeForScore(num0to1: number): HTMLElement; interface Options { - /** - * DOM element that will the overlay DOM should be a child of. - * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. - * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. - * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ - overlayParentEl?: HTMLElement; - - /** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ - onDetailsItemRendered?: (type: LH.Audit.Details['type'], el: HTMLElement, value: LH.Audit.Details) => void; - /** * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.) * Also, the fireworks easter-egg will want to flip to dark, so this setting will also disable chance of fireworks. */ @@ -31,12 +21,6 @@ declare module Renderer { /** Disable the topbar UI component */ omitTopbar?: boolean; - - /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ - onSaveGist?: (lhr: LH.Result) => string; - - /** If defined, when the 'Save/Copy as HTML' items are clicked, this fn will be used instead of `documentElement.outerHTML`. */ - getStandaloneReportHTML?: () => string; } } From cc119a25361d725abaf269fa2369da40fcecfaa2 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 27 Oct 2021 22:48:32 -0700 Subject: [PATCH 23/41] disableAutoDarkModeAndFireworks --- report/renderer/report-ui-features.js | 5 ++++- report/test-assets/faux-psi.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 007e9a612b78..2e95697a597d 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -48,6 +48,8 @@ export class ReportUIFeatures { /** @type {DOM} */ this._dom = dom; + this._opts = opts; + this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom); this.onMediaQueryChange = this.onMediaQueryChange.bind(this); } @@ -71,7 +73,8 @@ export class ReportUIFeatures { let turnOffTheLights = false; // Do not query the system preferences for DevTools - DevTools should only apply dark theme // if dark is selected in the settings panel. - if (!this._dom.isDevTools() && window.matchMedia('(prefers-color-scheme: dark)').matches) { + const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks; + if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) { turnOffTheLights = true; } diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index 58d72798e58b..244a767192d4 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -30,7 +30,10 @@ const lighthouseRenderer = window['report']; if (!container) throw new Error('Unexpected DOM. Bailing.'); try { - const reportRootEl = lighthouseRenderer.renderReport(lhr, {omitTopbar: true}); + const reportRootEl = lighthouseRenderer.renderReport(lhr, { + omitTopbar: true, + disableAutoDarkModeAndFireworks: true, + }); // TODO: display warnings if appropriate. for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { el.setAttribute('hidden', 'true'); From 7779f253fdfcd136710f4a7131739b220bf90b9d Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Thu, 28 Oct 2021 11:16:10 -0700 Subject: [PATCH 24/41] add overlayParentEl option. but since the overlay is pos:fixed, it doesnt scope well already. about to revert --- report/renderer/element-screenshot-renderer.js | 2 +- report/renderer/report-ui-features.js | 4 ++-- report/test-assets/faux-psi.js | 1 + report/types/report-renderer.d.ts | 7 +++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/report/renderer/element-screenshot-renderer.js b/report/renderer/element-screenshot-renderer.js index 9b6f723536ab..84d467ebdb58 100644 --- a/report/renderer/element-screenshot-renderer.js +++ b/report/renderer/element-screenshot-renderer.js @@ -157,7 +157,7 @@ export class ElementScreenshotRenderer { const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot')); if (!el) return; - const overlay = dom.createElement('div', 'lh-element-screenshot__overlay'); + const overlay = dom.createElement('div', 'lh-element-screenshot__overlay lh-vars'); overlayContainerEl.append(overlay); // The newly-added overlay has the dimensions we need. diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 2e95697a597d..cbd8efa2c0ad 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -282,7 +282,7 @@ export class ReportUIFeatures { /** * @param {Element} el */ - _setupElementScreenshotOverlay(el) { + _setupElementScreenshotOverlay(el) { const fullPageScreenshot = this.json.audits['full-page-screenshot'] && this.json.audits['full-page-screenshot'].details && @@ -293,7 +293,7 @@ export class ReportUIFeatures { ElementScreenshotRenderer.installOverlayFeature({ dom: this._dom, reportEl: el, - overlayContainerEl: el, + overlayContainerEl: this._opts.overlayParentEl || el, fullPageScreenshot, }); } diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index 244a767192d4..ea28c1ccb0db 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -33,6 +33,7 @@ const lighthouseRenderer = window['report']; const reportRootEl = lighthouseRenderer.renderReport(lhr, { omitTopbar: true, disableAutoDarkModeAndFireworks: true, + overlayParentEl: container, // scope the overlay inside of the tabs. }); // TODO: display warnings if appropriate. for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index bfb13ac012d6..7040bba560ea 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -21,6 +21,13 @@ declare module Renderer { /** Disable the topbar UI component */ omitTopbar?: boolean; + + /** + * DOM element that will the overlay DOM should be a child of. + * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. + * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. + * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ + overlayParentEl?: HTMLElement; } } From 69e7a487a7de91e4776079b3221089e801cbb599 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Thu, 28 Oct 2021 11:16:12 -0700 Subject: [PATCH 25/41] Revert "add overlayParentEl option. but since the overlay is pos:fixed, it doesnt scope well already. about to revert" This reverts commit 7779f253fdfcd136710f4a7131739b220bf90b9d. --- build/build-report.js | 2 +- report/renderer/element-screenshot-renderer.js | 2 +- report/renderer/report-ui-features.js | 4 ++-- report/test-assets/faux-psi.js | 1 - report/test/generator/report-generator-test.js | 1 - report/test/renderer/report-renderer-test.js | 2 -- report/test/renderer/report-ui-features-test.js | 16 ++++++++-------- report/types/report-renderer.d.ts | 7 ------- 8 files changed, 12 insertions(+), 23 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 7ddf24179cf4..d47b3a67ba22 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -125,7 +125,7 @@ async function buildUmdBundle() { if (require.main === module) { if (process.argv.length <= 2) { buildStandaloneReport(); - // buildFlowReport(); + buildFlowReport(); buildEsModulesBundle(); buildUmdBundle(); } diff --git a/report/renderer/element-screenshot-renderer.js b/report/renderer/element-screenshot-renderer.js index 84d467ebdb58..9b6f723536ab 100644 --- a/report/renderer/element-screenshot-renderer.js +++ b/report/renderer/element-screenshot-renderer.js @@ -157,7 +157,7 @@ export class ElementScreenshotRenderer { const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot')); if (!el) return; - const overlay = dom.createElement('div', 'lh-element-screenshot__overlay lh-vars'); + const overlay = dom.createElement('div', 'lh-element-screenshot__overlay'); overlayContainerEl.append(overlay); // The newly-added overlay has the dimensions we need. diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index cbd8efa2c0ad..2e95697a597d 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -282,7 +282,7 @@ export class ReportUIFeatures { /** * @param {Element} el */ - _setupElementScreenshotOverlay(el) { + _setupElementScreenshotOverlay(el) { const fullPageScreenshot = this.json.audits['full-page-screenshot'] && this.json.audits['full-page-screenshot'].details && @@ -293,7 +293,7 @@ export class ReportUIFeatures { ElementScreenshotRenderer.installOverlayFeature({ dom: this._dom, reportEl: el, - overlayContainerEl: this._opts.overlayParentEl || el, + overlayContainerEl: el, fullPageScreenshot, }); } diff --git a/report/test-assets/faux-psi.js b/report/test-assets/faux-psi.js index ea28c1ccb0db..244a767192d4 100644 --- a/report/test-assets/faux-psi.js +++ b/report/test-assets/faux-psi.js @@ -33,7 +33,6 @@ const lighthouseRenderer = window['report']; const reportRootEl = lighthouseRenderer.renderReport(lhr, { omitTopbar: true, disableAutoDarkModeAndFireworks: true, - overlayParentEl: container, // scope the overlay inside of the tabs. }); // TODO: display warnings if appropriate. for (const el of reportRootEl.querySelectorAll('.lh-warnings')) { diff --git a/report/test/generator/report-generator-test.js b/report/test/generator/report-generator-test.js index 4ea34e13dbcd..373b09befd82 100644 --- a/report/test/generator/report-generator-test.js +++ b/report/test/generator/report-generator-test.js @@ -60,7 +60,6 @@ describe('ReportGenerator', () => { it('should inject the report renderer javascript', () => { const result = ReportGenerator.generateReportHtml({}); - require('fs').writeFileSync('omg.txt', result, 'utf-8'); assert.ok(result.includes('configSettings.channel||"unknown"'), 'injects the script'); assert.ok(result.includes('robustness: <\\/script'), 'escapes HTML tags in javascript'); assert.ok(result.includes('pre$`post'), 'does not break from String.replace'); diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 5ddcb05735ba..a79c1fb20ea7 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -36,8 +36,6 @@ describe('ReportRenderer', () => { }; }; - // global.console.warn = jest.fn(); - const {window} = new jsdom.JSDOM(); global.self = window; diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 866c0aff2b56..d6f0039eacf5 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -34,7 +34,7 @@ describe('ReportUIFeatures', () => { const categoryRenderer = new CategoryRenderer(dom, detailsRenderer); const renderer = new ReportRenderer(dom, categoryRenderer); const reportUIFeatures = new ReportUIFeatures(dom); - const container = dom.find('main', dom._document); + const container = dom.find('main', dom.document()); renderer.renderReport(lhr, container); reportUIFeatures.initFeatures(lhr); return container; @@ -334,7 +334,7 @@ describe('ReportUIFeatures', () => { let dropDown; beforeEach(() => { - window = dom._document.defaultView; + window = dom.document().defaultView; const features = new ReportUIFeatures(dom); features.initFeatures(sampleResults); dropDown = features._topbar._dropDownMenu; @@ -372,7 +372,7 @@ describe('ReportUIFeatures', () => { const arrowUp = new window.KeyboardEvent('keydown', {bubbles: true, code: 'ArrowUp'}); dropDown._menuEl.firstElementChild.dispatchEvent(arrowUp); - assert.strictEqual(dom._document.activeElement, dropDown._menuEl.lastElementChild); + assert.strictEqual(dom.document().activeElement, dropDown._menuEl.lastElementChild); }); it('ArrowDown on the first menu element should focus the second element', () => { @@ -382,7 +382,7 @@ describe('ReportUIFeatures', () => { const arrowDown = new window.KeyboardEvent('keydown', {bubbles: true, code: 'ArrowDown'}); dropDown._menuEl.firstElementChild.dispatchEvent(arrowDown); - assert.strictEqual(dom._document.activeElement, nextElementSibling); + assert.strictEqual(dom.document().activeElement, nextElementSibling); }); it('Home on the last menu element should focus the first element', () => { @@ -392,7 +392,7 @@ describe('ReportUIFeatures', () => { const home = new window.KeyboardEvent('keydown', {bubbles: true, code: 'Home'}); dropDown._menuEl.lastElementChild.dispatchEvent(home); - assert.strictEqual(dom._document.activeElement, firstElementChild); + assert.strictEqual(dom.document().activeElement, firstElementChild); }); it('End on the first menu element should focus the last element', () => { @@ -402,14 +402,14 @@ describe('ReportUIFeatures', () => { const end = new window.KeyboardEvent('keydown', {bubbles: true, code: 'End'}); dropDown._menuEl.firstElementChild.dispatchEvent(end); - assert.strictEqual(dom._document.activeElement, lastElementChild); + assert.strictEqual(dom.document().activeElement, lastElementChild); }); describe('_getNextSelectableNode', () => { let createDiv; beforeAll(() => { - createDiv = () => dom._document.createElement('div'); + createDiv = () => dom.document().createElement('div'); }); it('should return undefined when nodes is empty', () => { @@ -485,7 +485,7 @@ describe('ReportUIFeatures', () => { }); it('should toggle active class when focus relatedTarget is document.body', () => { - const relatedTarget = dom._document.body; + const relatedTarget = dom.document().body; const event = new window.FocusEvent('focusout', {relatedTarget}); dropDown.onMenuFocusOut(event); diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index 7040bba560ea..bfb13ac012d6 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -21,13 +21,6 @@ declare module Renderer { /** Disable the topbar UI component */ omitTopbar?: boolean; - - /** - * DOM element that will the overlay DOM should be a child of. - * Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above. - * Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header. - * @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */ - overlayParentEl?: HTMLElement; } } From a572b9ca5068b16c663c590054446182abec841e Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Thu, 28 Oct 2021 16:27:51 -0700 Subject: [PATCH 26/41] renderReport 2nd arg is now optional --- report/renderer/report-renderer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 67b54eb06297..7efbe093afbd 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -42,13 +42,13 @@ export class ReportRenderer { /** * @param {LH.Result} lhr - * @param {HTMLElement} rootEl Report root element containing the report + * @param {HTMLElement?} rootEl Report root element containing the report * @param {LH.Renderer.Options=} opts * @return {!Element} */ renderReport(lhr, rootEl, opts) { // Allow old report rendering API - if (!this._dom.rootEl) { + if (!this._dom.rootEl && rootEl) { console.warn('Please adopt the new report API in renderer/api.js.'); const closestRoot = rootEl.closest('.lh-vars'); if (!closestRoot) { @@ -64,10 +64,10 @@ export class ReportRenderer { const report = Util.prepareReportResult(lhr); - rootEl.textContent = ''; // Remove previous report. - rootEl.appendChild(this._renderReport(report)); + this._dom.rootEl.textContent = ''; // Remove previous report. + this._dom.rootEl.appendChild(this._renderReport(report)); - return rootEl; + return this._dom.rootEl; } /** From 66216a90365884599e8678bc52b3640c5904764b Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Thu, 28 Oct 2021 17:23:20 -0700 Subject: [PATCH 27/41] format types --- report/types/report-renderer.d.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/report/types/report-renderer.d.ts b/report/types/report-renderer.d.ts index bfb13ac012d6..a5f94830421f 100644 --- a/report/types/report-renderer.d.ts +++ b/report/types/report-renderer.d.ts @@ -4,15 +4,11 @@ * 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. */ - - import {Result as AuditResult} from '../../types/lhr/audit-result'; +import { Result as AuditResult } from "../../types/lhr/audit-result"; declare module Renderer { function renderReport(lhr: AuditResult, options?: Options): HTMLElement; - // Extra convience if you have just a category score. - function renderGaugeForScore(num0to1: number): HTMLElement; - interface Options { /** * Don't automatically apply dark-mode to dark based on (prefers-color-scheme: dark). (DevTools and PSI don't want this.) @@ -24,5 +20,4 @@ declare module Renderer { } } - export default Renderer; From 3058b570b385666ce4c48d00d429ad914ccaeac4 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 29 Oct 2021 18:15:04 -0700 Subject: [PATCH 28/41] standalone using new api. tsc errrs tho --- report/assets/standalone-template.html | 4 ++-- report/clients/standalone.js | 21 +++++---------------- report/renderer/dom.js | 4 ++-- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/report/assets/standalone-template.html b/report/assets/standalone-template.html index a6c9ae9bd3ce..4f368e745c8a 100644 --- a/report/assets/standalone-template.html +++ b/report/assets/standalone-template.html @@ -22,10 +22,10 @@ Lighthouse Report - + -
+
diff --git a/report/clients/standalone.js b/report/clients/standalone.js index 25f90d2d24ea..c32dd5fbde99 100644 --- a/report/clients/standalone.js +++ b/report/clients/standalone.js @@ -13,28 +13,17 @@ /* global ga */ -import {DOM} from '../renderer/dom.js'; +import {renderReport} from '../renderer/api.js'; import {Logger} from '../renderer/logger.js'; -import {ReportRenderer} from '../renderer/report-renderer.js'; -import {ReportUIFeatures} from '../renderer/report-ui-features.js'; // Used by standalone.html // eslint-disable-next-line no-unused-vars function __initLighthouseReport__() { - const dom = new DOM(document); - const renderer = new ReportRenderer(dom); - const container = dom.find('main', document); - /** @type {LH.ReportResult} */ + /** @type {LH.Result} */ // @ts-expect-error const lhr = window.__LIGHTHOUSE_JSON__; - - const opts = {omitTopbar: false}; - renderer.renderReport(lhr, container, opts); - - // Hook in JS features and page-level event listeners after the report - // is in the document. - const features = new ReportUIFeatures(dom, opts); - features.initFeatures(lhr); + const reportRootEl = renderReport(lhr); + document.querySelector('main').append(reportRootEl); document.addEventListener('lh-analytics', /** @param {Event} e */ e => { // @ts-expect-error @@ -42,7 +31,7 @@ function __initLighthouseReport__() { }); document.addEventListener('lh-log', /** @param {Event} e */ e => { - const el = dom.find('div#lh-log', document); + const el = document.querySelector('div#lh-log'); const logger = new Logger(el); // @ts-expect-error diff --git a/report/renderer/dom.js b/report/renderer/dom.js index 6957fe3bec1a..97a36925fd18 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -27,7 +27,7 @@ import {createComponent} from './components.js'; export class DOM { /** * @param {Document} document - * @param {HTMLElement=} rootEl + * @param {HTMLElement} rootEl */ constructor(document, rootEl) { /** @type {Document} */ @@ -37,7 +37,7 @@ export class DOM { /** @type {Map} */ this._componentCache = new Map(); /** @type {HTMLElement} */ - // @ts-expect-error For legacy Report API users, this'll be undefined, but set in renderReport + // For legacy Report API users, this'll be undefined, but set in renderReport this.rootEl = rootEl; } From e0bf5a5f2d9e6f24d10da606a4a0c9bdde2c2b69 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 2 Nov 2021 09:48:39 -0700 Subject: [PATCH 29/41] fixtest --- report/renderer/report-renderer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 7efbe093afbd..b41328792d29 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -51,10 +51,12 @@ export class ReportRenderer { if (!this._dom.rootEl && rootEl) { console.warn('Please adopt the new report API in renderer/api.js.'); const closestRoot = rootEl.closest('.lh-vars'); - if (!closestRoot) { + if (closestRoot) { + this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot); + } else { rootEl.classList.add('lh-root', 'lh-vars'); + this._dom.rootEl = rootEl; } - this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot); } if (opts) { this._opts = opts; From 6e19522db6dabebb474336cd8ef0d314655889bd Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 2 Nov 2021 10:06:30 -0700 Subject: [PATCH 30/41] move lh-vars classes to main rather than body --- flow-report/assets/standalone-flow-template.html | 4 ++-- viewer/app/index.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flow-report/assets/standalone-flow-template.html b/flow-report/assets/standalone-flow-template.html index 7b72f95fc9b1..c387bd0a61f2 100644 --- a/flow-report/assets/standalone-flow-template.html +++ b/flow-report/assets/standalone-flow-template.html @@ -24,10 +24,10 @@ - + -
+
diff --git a/report/clients/standalone.js b/report/clients/standalone.js index a912c3b86bc0..4e43b5efadf5 100644 --- a/report/clients/standalone.js +++ b/report/clients/standalone.js @@ -20,11 +20,9 @@ function __initLighthouseReport__() { /** @type {LH.Result} */ // @ts-expect-error const lhr = window.__LIGHTHOUSE_JSON__; - const mainEl = document.querySelector('main'); - if (!mainEl) return; const reportRootEl = renderReport(lhr); - mainEl.append(reportRootEl); + document.body.append(reportRootEl); document.addEventListener('lh-analytics', /** @param {Event} e */ e => { // @ts-expect-error diff --git a/viewer/app/index.html b/viewer/app/index.html index ce441cc52ee1..9ca5f92542c6 100644 --- a/viewer/app/index.html +++ b/viewer/app/index.html @@ -18,9 +18,9 @@ -
+
-
+
diff --git a/viewer/app/src/lighthouse-report-viewer.js b/viewer/app/src/lighthouse-report-viewer.js index 1c94f3eea1e6..417791d3951d 100644 --- a/viewer/app/src/lighthouse-report-viewer.js +++ b/viewer/app/src/lighthouse-report-viewer.js @@ -221,10 +221,11 @@ export class LighthouseReportViewer { return; } + // @ts-expect-error Legacy use of report renderer const dom = new DOM(document); const renderer = new ReportRenderer(dom); - const container = find('main', document); + const container = find('div.renderer-container', document); try { renderer.renderReport(json, container); From 871e8e50064b22c69273b399c646633b10568281 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 2 Nov 2021 14:56:29 -0700 Subject: [PATCH 33/41] reword --- report/renderer/dom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/report/renderer/dom.js b/report/renderer/dom.js index 97a36925fd18..9918359c2e7f 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -229,8 +229,8 @@ export class DOM { } /** - * ONLY use if `dom.rootEl` isn't sufficient for your needs. It is preferred for all scoping, - * as a document can have multiple reports within it. + * ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred + * for all scoping, because a document can have multiple reports within it. * @return {Document} */ document() { From 91390dc82f21ed53bb1811751835ce73bc93e295 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 2 Nov 2021 15:19:13 -0700 Subject: [PATCH 34/41] fix flow --- flow-report/src/wrappers/report.tsx | 5 ++--- flow-report/standalone-flow.tsx | 6 +++--- report/renderer/report-renderer.js | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/flow-report/src/wrappers/report.tsx b/flow-report/src/wrappers/report.tsx index eaa259b222dc..3d46ba6aba82 100644 --- a/flow-report/src/wrappers/report.tsx +++ b/flow-report/src/wrappers/report.tsx @@ -50,11 +50,10 @@ export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> = convertChildAnchors(ref.current, hashState.index); const fullPageScreenshot = getFullPageScreenshot(hashState.currentLhr); if (fullPageScreenshot) { - const container = dom.find('.lh-container', ref.current); ElementScreenshotRenderer.installOverlayFeature({ dom, - reportEl: container, - overlayContainerEl: container, + reportEl: ref.current, + overlayContainerEl: ref.current, fullPageScreenshot, }); } diff --git a/flow-report/standalone-flow.tsx b/flow-report/standalone-flow.tsx index 380f56aa990a..758d0430a4ff 100644 --- a/flow-report/standalone-flow.tsx +++ b/flow-report/standalone-flow.tsx @@ -15,9 +15,9 @@ import {App} from './src/app'; // Used by standalone-flow.html function __initLighthouseFlowReport__() { - const root = document.body.querySelector('main'); - if (!root) throw Error('Root element not found'); - render(, root); + const container = document.body.querySelector('main'); + if (!container) throw Error('Container element not found'); + render(, container); } window.__initLighthouseFlowReport__ = __initLighthouseFlowReport__; diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index b41328792d29..eebb2cf96811 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -50,7 +50,7 @@ export class ReportRenderer { // Allow old report rendering API if (!this._dom.rootEl && rootEl) { console.warn('Please adopt the new report API in renderer/api.js.'); - const closestRoot = rootEl.closest('.lh-vars'); + const closestRoot = rootEl.closest('.lh-root'); if (closestRoot) { this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot); } else { From 6c4d3df2d27ff1dd7145156971ad6626cca6fefe Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 2 Nov 2021 15:20:58 -0700 Subject: [PATCH 35/41] margin --- report/assets/standalone-template.html | 1 + 1 file changed, 1 insertion(+) diff --git a/report/assets/standalone-template.html b/report/assets/standalone-template.html index 65fd5485e59b..99f2921df862 100644 --- a/report/assets/standalone-template.html +++ b/report/assets/standalone-template.html @@ -21,6 +21,7 @@ Lighthouse Report + From b6aaf0159d28444f897465a45e98d2c02c736de2 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 10:32:22 -0700 Subject: [PATCH 36/41] latest feedback. new report creates an article. keep main as container --- flow-report/assets/standalone-flow-template.html | 1 + flow-report/standalone-flow.tsx | 1 + report/renderer/api.js | 2 +- report/test-assets/faux-psi-template.html | 9 ++------- viewer/app/index.html | 6 +++--- viewer/app/src/lighthouse-report-viewer.js | 2 +- viewer/app/styles/viewer.css | 1 - 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/flow-report/assets/standalone-flow-template.html b/flow-report/assets/standalone-flow-template.html index c387bd0a61f2..37afb0aa26d8 100644 --- a/flow-report/assets/standalone-flow-template.html +++ b/flow-report/assets/standalone-flow-template.html @@ -23,6 +23,7 @@ Lighthouse Flow Report + diff --git a/flow-report/standalone-flow.tsx b/flow-report/standalone-flow.tsx index 758d0430a4ff..69e8d22f7f33 100644 --- a/flow-report/standalone-flow.tsx +++ b/flow-report/standalone-flow.tsx @@ -15,6 +15,7 @@ import {App} from './src/app'; // Used by standalone-flow.html function __initLighthouseFlowReport__() { + // TODO(adamraine): add lh-vars, etc classes programmatically instead of in the HTML template const container = document.body.querySelector('main'); if (!container) throw Error('Container element not found'); render(, container); diff --git a/report/renderer/api.js b/report/renderer/api.js index 8fae0d0b43c1..e2b5e5006f22 100644 --- a/report/renderer/api.js +++ b/report/renderer/api.js @@ -16,7 +16,7 @@ import {ReportUIFeatures} from '../renderer/report-ui-features.js'; * @return {HTMLElement} */ export function renderReport(lhr, opts = {}) { - const rootEl = document.createElement('main'); + const rootEl = document.createElement('article'); rootEl.classList.add('lh-root', 'lh-vars'); const dom = new DOM(rootEl.ownerDocument, rootEl); diff --git a/report/test-assets/faux-psi-template.html b/report/test-assets/faux-psi-template.html index 2dcf747e297f..32be25b08239 100644 --- a/report/test-assets/faux-psi-template.html +++ b/report/test-assets/faux-psi-template.html @@ -66,7 +66,6 @@ } .tab-panel { - padding: 30px 0; border-top: 1px solid #ccc; } @@ -101,13 +100,9 @@
-
-
-
+
-
-
-
+
diff --git a/viewer/app/index.html b/viewer/app/index.html index 9ca5f92542c6..ed182617223d 100644 --- a/viewer/app/index.html +++ b/viewer/app/index.html @@ -16,12 +16,12 @@ - - -
+
+
+
diff --git a/viewer/app/src/lighthouse-report-viewer.js b/viewer/app/src/lighthouse-report-viewer.js index 417791d3951d..26d5326bdcfb 100644 --- a/viewer/app/src/lighthouse-report-viewer.js +++ b/viewer/app/src/lighthouse-report-viewer.js @@ -225,7 +225,7 @@ export class LighthouseReportViewer { const dom = new DOM(document); const renderer = new ReportRenderer(dom); - const container = find('div.renderer-container', document); + const container = find('main', document); try { renderer.renderReport(json, container); diff --git a/viewer/app/styles/viewer.css b/viewer/app/styles/viewer.css index e030a424adb5..9461d1aff62e 100644 --- a/viewer/app/styles/viewer.css +++ b/viewer/app/styles/viewer.css @@ -10,7 +10,6 @@ body { font-family: Roboto, Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; line-height: 24px; margin: 0; } From 41879926fa6cb7bd7b021a4d3c9ad4f4a26721bb Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 10:35:50 -0700 Subject: [PATCH 37/41] tsc --- flow-report/src/wrappers/report-renderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/flow-report/src/wrappers/report-renderer.tsx b/flow-report/src/wrappers/report-renderer.tsx index 8ad261a48064..158eced89ddc 100644 --- a/flow-report/src/wrappers/report-renderer.tsx +++ b/flow-report/src/wrappers/report-renderer.tsx @@ -29,6 +29,7 @@ function useReportRenderer() { const ReportRendererProvider: FunctionComponent = ({children}) => { const globals = useMemo(() => { + // @ts-expect-error Still using legacy const dom = new DOM(document); const detailsRenderer = new DetailsRenderer(dom); const categoryRenderer = new CategoryRenderer(dom, detailsRenderer); From 033a4532d1f87b38f0754de210021ff04314f7a2 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 10:59:18 -0700 Subject: [PATCH 38/41] fix report tests --- report/test/renderer/report-renderer-axe-test.js | 2 ++ report/test/renderer/report-ui-features-test.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/report/test/renderer/report-renderer-axe-test.js b/report/test/renderer/report-renderer-axe-test.js index f351ae538e53..dd6ee7a3ffc0 100644 --- a/report/test/renderer/report-renderer-axe-test.js +++ b/report/test/renderer/report-renderer-axe-test.js @@ -23,6 +23,8 @@ describe('ReportRendererAxe', () => { let sampleResults; beforeAll(async () => { + global.console.warn = jest.fn(); + const {window} = new jsdom.JSDOM(); const dom = new DOM(window.document); const detailsRenderer = new DetailsRenderer(dom); diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 8b9a0c463a70..1e2266a3c858 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -34,7 +34,7 @@ describe('ReportUIFeatures', () => { const categoryRenderer = new CategoryRenderer(dom, detailsRenderer); const renderer = new ReportRenderer(dom, categoryRenderer); const reportUIFeatures = new ReportUIFeatures(dom); - const container = dom.find('main', dom.document()); + const container = dom.find('body', dom.document()); renderer.renderReport(lhr, container); reportUIFeatures.initFeatures(lhr); return container; From 536f4cfaaab41ea2725b83a4d89fdd5f412116c8 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 11:26:30 -0700 Subject: [PATCH 39/41] handle flow legacy ussage --- report/renderer/report-renderer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index f1ded438b35a..3709f85adbd0 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -47,7 +47,7 @@ export class ReportRenderer { * @return {!Element} */ renderReport(lhr, rootEl, opts) { - // Allow old report rendering API + // Allow legacy report rendering API if (!this._dom.rootEl && rootEl) { console.warn('Please adopt the new report API in renderer/api.js.'); const closestRoot = rootEl.closest('.lh-root'); @@ -57,6 +57,9 @@ export class ReportRenderer { rootEl.classList.add('lh-root', 'lh-vars'); this._dom.rootEl = rootEl; } + } else if (this._dom.rootEl && rootEl) { + // Handle legacy flow-report case + this._dom.rootEl = rootEl; } if (opts) { this._opts = opts; From db94ac1c92de3174bf6d4945f9060aa24db3abc8 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 11:49:53 -0700 Subject: [PATCH 40/41] fix FPSS. css var was installed too low in the DOM --- report/renderer/report-renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 3709f85adbd0..181493daddb0 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -334,7 +334,7 @@ export class ReportRenderer { if (fullPageScreenshot) { ElementScreenshotRenderer.installFullPageScreenshot( - reportContainer, fullPageScreenshot.screenshot); + this._dom.rootEl, fullPageScreenshot.screenshot); } return reportFragment; From 7384079b7fefb2b4661396264af78c3359ae474c Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Wed, 3 Nov 2021 11:50:10 -0700 Subject: [PATCH 41/41] optional fpss cleanup and renaming --- flow-report/src/wrappers/report.tsx | 2 +- report/renderer/element-screenshot-renderer.js | 12 ++++++------ report/renderer/report-ui-features.js | 8 ++++---- report/test/renderer/report-renderer-axe-test.js | 1 + 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/flow-report/src/wrappers/report.tsx b/flow-report/src/wrappers/report.tsx index 418b7bd39426..19514bf99b6d 100644 --- a/flow-report/src/wrappers/report.tsx +++ b/flow-report/src/wrappers/report.tsx @@ -52,7 +52,7 @@ const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> = if (fullPageScreenshot) { ElementScreenshotRenderer.installOverlayFeature({ dom, - reportEl: ref.current, + rootEl: ref.current, overlayContainerEl: ref.current, fullPageScreenshot, }); diff --git a/report/renderer/element-screenshot-renderer.js b/report/renderer/element-screenshot-renderer.js index c55b3f2bb31c..08fbb2e02bc8 100644 --- a/report/renderer/element-screenshot-renderer.js +++ b/report/renderer/element-screenshot-renderer.js @@ -18,7 +18,7 @@ /** * @typedef InstallOverlayFeatureParams * @property {DOM} dom - * @property {Element} reportEl + * @property {Element} rootEl * @property {Element} overlayContainerEl * @property {LH.Audit.Details.FullPageScreenshot} fullPageScreenshot */ @@ -135,7 +135,7 @@ export class ElementScreenshotRenderer { * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot */ static installFullPageScreenshot(el, screenshot) { - el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`); + el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`); } /** @@ -143,14 +143,14 @@ export class ElementScreenshotRenderer { * @param {InstallOverlayFeatureParams} opts */ static installOverlayFeature(opts) { - const {dom, reportEl, overlayContainerEl, fullPageScreenshot} = opts; + const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts; const screenshotOverlayClass = 'lh-screenshot-overlay--enabled'; // Don't install the feature more than once. - if (reportEl.classList.contains(screenshotOverlayClass)) return; - reportEl.classList.add(screenshotOverlayClass); + if (rootEl.classList.contains(screenshotOverlayClass)) return; + rootEl.classList.add(screenshotOverlayClass); // Add a single listener to the provided element to handle all clicks within (event delegation). - reportEl.addEventListener('click', e => { + rootEl.addEventListener('click', e => { const target = /** @type {?HTMLElement} */ (e.target); if (!target) return; // Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too. diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js index 25e3b9070b0d..b873ddb30156 100644 --- a/report/renderer/report-ui-features.js +++ b/report/renderer/report-ui-features.js @@ -279,9 +279,9 @@ export class ReportUIFeatures { } /** - * @param {Element} el + * @param {Element} rootEl */ - _setupElementScreenshotOverlay(el) { + _setupElementScreenshotOverlay(rootEl) { const fullPageScreenshot = this.json.audits['full-page-screenshot'] && this.json.audits['full-page-screenshot'].details && @@ -291,8 +291,8 @@ export class ReportUIFeatures { ElementScreenshotRenderer.installOverlayFeature({ dom: this._dom, - reportEl: el, - overlayContainerEl: el, + rootEl: rootEl, + overlayContainerEl: rootEl, fullPageScreenshot, }); } diff --git a/report/test/renderer/report-renderer-axe-test.js b/report/test/renderer/report-renderer-axe-test.js index dd6ee7a3ffc0..5dd530f6f654 100644 --- a/report/test/renderer/report-renderer-axe-test.js +++ b/report/test/renderer/report-renderer-axe-test.js @@ -8,6 +8,7 @@ /* eslint-env jest */ import jsdom from 'jsdom'; +import {jest} from '@jest/globals'; import {Util} from '../../renderer/util.js'; import {DOM} from '../../renderer/dom.js';