From 2b9cec93aa8f5cb799ad47df1c5978366b62c777 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 4 Jun 2021 10:58:53 -0700 Subject: [PATCH 01/71] first pass --- build/build-dt-report-resources.js | 4 +- lighthouse-core/audits/audit.js | 54 +++++++- lighthouse-core/computed/resource-summary.js | 56 +++++++- lighthouse-core/lib/file-namer.js | 5 +- lighthouse-core/lib/url-shim.js | 126 +++++++++++++++++- .../report/html/html-report-assets.js | 55 +++++--- .../report/html/renderer/category-renderer.js | 19 +-- .../html/renderer/crc-details-renderer.js | 15 +-- .../report/html/renderer/details-renderer.js | 17 +-- lighthouse-core/report/html/renderer/dom.js | 12 +- .../renderer/element-screenshot-renderer.js | 14 +- .../report/html/renderer/file-namer.js | 8 ++ lighthouse-core/report/html/renderer/i18n.js | 12 +- .../report/html/renderer/logger.js | 6 +- lighthouse-core/report/html/renderer/main.js | 57 ++++++++ .../renderer/performance-category-renderer.js | 13 +- lighthouse-core/report/html/renderer/psi.js | 23 ++-- .../html/renderer/pwa-category-renderer.js | 11 +- .../report/html/renderer/report-renderer.js | 19 ++- .../html/renderer/report-ui-features.js | 15 +-- .../report/html/renderer/snippet-renderer.js | 15 +-- .../report/html/renderer/text-encoding.js | 8 +- lighthouse-core/report/html/renderer/util.js | 12 +- .../report/html/report-template.html | 81 +++++------ lighthouse-core/report/report-generator.js | 8 ++ lighthouse-core/scripts/roll-to-devtools.sh | 7 + types/html-renderer.d.ts | 47 ------- types/i18n.d.ts | 2 +- 28 files changed, 463 insertions(+), 258 deletions(-) create mode 100644 lighthouse-core/report/html/renderer/file-namer.js create mode 100644 lighthouse-core/report/html/renderer/main.js diff --git a/build/build-dt-report-resources.js b/build/build-dt-report-resources.js index 6a2b34b4d2db..60194567198d 100644 --- a/build/build-dt-report-resources.js +++ b/build/build-dt-report-resources.js @@ -28,11 +28,11 @@ function writeFile(name, content) { fs.rmdirSync(distDir, {recursive: true}); fs.mkdirSync(distDir); -writeFile('report.js', htmlReportAssets.REPORT_JAVASCRIPT); +writeFile('report.js', htmlReportAssets.REPORT_JAVASCRIPT); // TODO remove writeFile('report.css', htmlReportAssets.REPORT_CSS); writeFile('template.html', htmlReportAssets.REPORT_TEMPLATE); writeFile('templates.html', htmlReportAssets.REPORT_TEMPLATES); -writeFile('report.d.ts', 'export {}'); +writeFile('report.d.ts', 'export {}'); // TODO remove writeFile('report-generator.d.ts', 'export {}'); const pathToReportAssets = require.resolve('../clients/devtools-report-assets.js'); diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js index ec68bee6c307..cc2ea3987ae0 100644 --- a/lighthouse-core/audits/audit.js +++ b/lighthouse-core/audits/audit.js @@ -7,7 +7,59 @@ const {isUnderTest} = require('../lib/lh-env.js'); const statistics = require('../lib/statistics.js'); -const Util = require('../report/html/renderer/util.js'); +// const Util = require('../report/html/renderer/util.js'); +class Util { + static PASS_THRESHOLD = 0.9; + + /** + * Returns only lines that are near a message, or the first few lines if there are + * no line messages. + * @param {LH.Audit.Details.SnippetValue['lines']} lines + * @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages + * @param {number} surroundingLineCount Number of lines to include before and after + * the message. If this is e.g. 2 this function might return 5 lines. + */ + static filterRelevantLines(lines, lineMessages, surroundingLineCount) { + if (lineMessages.length === 0) { + // no lines with messages, just return the first bunch of lines + return lines.slice(0, surroundingLineCount * 2 + 1); + } + + const minGapSize = 3; + const lineNumbersToKeep = new Set(); + // Sort messages so we can check lineNumbersToKeep to see how big the gap to + // the previous line is. + lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0)); + lineMessages.forEach(({lineNumber}) => { + let firstSurroundingLineNumber = lineNumber - surroundingLineCount; + let lastSurroundingLineNumber = lineNumber + surroundingLineCount; + + while (firstSurroundingLineNumber < 1) { + // make sure we still show (surroundingLineCount * 2 + 1) lines in total + firstSurroundingLineNumber++; + lastSurroundingLineNumber++; + } + // If only a few lines would be omitted normally then we prefer to include + // extra lines to avoid the tiny gap + if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) { + firstSurroundingLineNumber -= minGapSize; + } + for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) { + const surroundingLineNumber = i; + lineNumbersToKeep.add(surroundingLineNumber); + } + }); + + return lines.filter(line => lineNumbersToKeep.has(line.lineNumber)); + } + + /** + * @param {string} categoryId + */ + static isPluginCategory(categoryId) { + return categoryId.startsWith('lighthouse-plugin-'); + } +} const DEFAULT_PASS = 'defaultPass'; diff --git a/lighthouse-core/computed/resource-summary.js b/lighthouse-core/computed/resource-summary.js index da9e0774eea1..6c1f57f9335b 100644 --- a/lighthouse-core/computed/resource-summary.js +++ b/lighthouse-core/computed/resource-summary.js @@ -11,7 +11,61 @@ const URL = require('../lib/url-shim.js'); const NetworkRequest = require('../lib/network-request.js'); const MainResource = require('./main-resource.js'); const Budget = require('../config/budget.js'); -const Util = require('../report/html/renderer/util.js'); +// const Util = require('../report/html/renderer/util.js'); + +// 25 most used tld plus one domains (aka public suffixes) from http archive. +// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 +// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes +const listOfTlds = [ + 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', + 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', +]; +class Util { + /** + * @param {string|URL} value + * @return {!URL} + */ + static createOrReturnURL(value) { + if (value instanceof URL) { + return value; + } + + return new URL(value); + } + + /** + * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). + * @param {string|URL} url hostname or URL object + * @returns {string} + */ + static getRootDomain(url) { + const hostname = Util.createOrReturnURL(url).hostname; + const tld = Util.getTld(hostname); + + // tld is .com or .co.uk which means we means that length is 1 to big + // .com => 2 & .co.uk => 3 + const splitTld = tld.split('.'); + + // get TLD + root domain + return hostname.split('.').slice(-splitTld.length).join('.'); + } + + /** + * Gets the tld of a domain + * + * @param {string} hostname + * @return {string} tld + */ + static getTld(hostname) { + const tlds = hostname.split('.').slice(-2); + + if (!listOfTlds.includes(tlds[0])) { + return `.${tlds[tlds.length - 1]}`; + } + + return `.${tlds.join('.')}`; + } +} /** @typedef {{count: number, resourceSize: number, transferSize: number}} ResourceEntry */ diff --git a/lighthouse-core/lib/file-namer.js b/lighthouse-core/lib/file-namer.js index 7d2f9c4c7b20..0b66d57ba0d7 100644 --- a/lighthouse-core/lib/file-namer.js +++ b/lighthouse-core/lib/file-namer.js @@ -34,7 +34,4 @@ function getFilenamePrefix(lhr) { return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-'); } -// don't attempt to export in the browser. -if (typeof module !== 'undefined' && module.exports) { - module.exports = {getFilenamePrefix}; -} +module.exports = {getFilenamePrefix}; diff --git a/lighthouse-core/lib/url-shim.js b/lighthouse-core/lib/url-shim.js index 8c6c3a148abd..e8bc30a6f116 100644 --- a/lighthouse-core/lib/url-shim.js +++ b/lighthouse-core/lib/url-shim.js @@ -9,7 +9,131 @@ * URL shim so we keep our code DRY */ -const Util = require('../report/html/renderer/util.js'); +// const Util = require('../report/html/renderer/util.js'); +const ELLIPSIS = '\u2026'; +// 25 most used tld plus one domains (aka public suffixes) from http archive. +// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 +// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes +const listOfTlds = [ + 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', + 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', +]; +class Util { + /** + * @param {string|URL} value + * @return {!URL} + */ + static createOrReturnURL(value) { + if (value instanceof URL) { + return value; + } + + return new URL(value); + } + + /** + * @param {URL} parsedUrl + * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options + * @return {string} + */ + static getURLDisplayName(parsedUrl, options) { + // Closure optional properties aren't optional in tsc, so fallback needs undefined values. + options = options || {numPathParts: undefined, preserveQuery: undefined, + preserveHost: undefined}; + const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2; + const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true; + const preserveHost = options.preserveHost || false; + + let name; + + if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') { + // Handle 'about:*' and 'data:*' URLs specially since they have no path. + name = parsedUrl.href; + } else { + name = parsedUrl.pathname; + const parts = name.split('/').filter(part => part.length); + if (numPathParts && parts.length > numPathParts) { + name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/'); + } + + if (preserveHost) { + name = `${parsedUrl.host}/${name.replace(/^\//, '')}`; + } + if (preserveQuery) { + name = `${name}${parsedUrl.search}`; + } + } + + const MAX_LENGTH = 64; + // Always elide hexadecimal hash + name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`); + // Also elide other hash-like mixed-case strings + name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g, + `$1${ELLIPSIS}`); + // Also elide long number sequences + name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`); + // Merge any adjacent ellipses + name = name.replace(/\u2026+/g, ELLIPSIS); + + // Elide query params first + if (name.length > MAX_LENGTH && name.includes('?')) { + // Try to leave the first query parameter intact + name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`); + + // Remove it all if it's still too long + if (name.length > MAX_LENGTH) { + name = name.replace(/\?.*/, `?${ELLIPSIS}`); + } + } + + // Elide too long names next + if (name.length > MAX_LENGTH) { + const dotIndex = name.lastIndexOf('.'); + if (dotIndex >= 0) { + name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + + // Show file extension + `${ELLIPSIS}${name.slice(dotIndex)}`; + } else { + name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS; + } + } + + return name; + } + + /** + * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). + * @param {string|URL} url hostname or URL object + * @returns {string} + */ + static getRootDomain(url) { + const hostname = Util.createOrReturnURL(url).hostname; + const tld = Util.getTld(hostname); + + // tld is .com or .co.uk which means we means that length is 1 to big + // .com => 2 & .co.uk => 3 + const splitTld = tld.split('.'); + + // get TLD + root domain + return hostname.split('.').slice(-splitTld.length).join('.'); + } + + /** + * Gets the tld of a domain + * + * @param {string} hostname + * @return {string} tld + */ + static getTld(hostname) { + const tlds = hostname.split('.').slice(-2); + + if (!listOfTlds.includes(tlds[0])) { + return `.${tlds[tlds.length - 1]}`; + } + + return `.${tlds.join('.')}`; + } +} /** @typedef {import('./network-request.js')} NetworkRequest */ diff --git a/lighthouse-core/report/html/html-report-assets.js b/lighthouse-core/report/html/html-report-assets.js index af2ef7c8afcb..746414d4b5fc 100644 --- a/lighthouse-core/report/html/html-report-assets.js +++ b/lighthouse-core/report/html/html-report-assets.js @@ -9,22 +9,46 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8'); const REPORT_JAVASCRIPT = [ - fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), + // fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), + // fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), ].join(';\n'); + +/* eslint-disable max-len */ +const REPORT_JAVASCRIPT_MODULES = { + './logger.js': fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), + './i18n.js': fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), + './text-encoding.js': fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), + './util.js': fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), + './dom.js': fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), + './crc-details-renderer.js': fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), + './snippet-renderer.js': fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), + './element-screenshot-renderer.js': fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), + './category-renderer.js': fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), + './performance-category-renderer.js': fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), + './pwa-category-renderer.js': fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), + './details-renderer.js': fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), + '../../../lib/file-namer.js': fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), + './file-namer.js': fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), + './report-ui-features.js': fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), + './report-renderer.js': fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), + './main.js': fs.readFileSync(__dirname + '/renderer/main.js', 'utf8'), +}; +/* eslint-enable max-len */ + const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8'); @@ -34,5 +58,6 @@ module.exports = { REPORT_TEMPLATE, REPORT_TEMPLATES, REPORT_JAVASCRIPT, + REPORT_JAVASCRIPT_MODULES, REPORT_CSS, }; diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js index b47aabf94088..4d684ab0a807 100644 --- a/lighthouse-core/report/html/renderer/category-renderer.js +++ b/lighthouse-core/report/html/renderer/category-renderer.js @@ -16,15 +16,14 @@ */ 'use strict'; -/* globals self, Util */ - -/** @typedef {import('./dom.js')} DOM */ -/** @typedef {import('./report-renderer.js')} ReportRenderer */ -/** @typedef {import('./details-renderer.js')} DetailsRenderer */ -/** @typedef {import('./util.js')} Util */ +/** @typedef {import('./dom.js').DOM} DOM */ +/** @typedef {import('./report-renderer.js').ReportRenderer} ReportRenderer */ +/** @typedef {import('./details-renderer.js').DetailsRenderer} DetailsRenderer */ /** @typedef {'failed'|'warning'|'manual'|'passed'|'notApplicable'} TopLevelClumpId */ -class CategoryRenderer { +import {Util} from './util.js'; + +export class CategoryRenderer { /** * @param {DOM} dom * @param {DetailsRenderer} detailsRenderer @@ -502,9 +501,3 @@ class CategoryRenderer { permalinkEl.id = id; } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = CategoryRenderer; -} else { - self.CategoryRenderer = CategoryRenderer; -} diff --git a/lighthouse-core/report/html/renderer/crc-details-renderer.js b/lighthouse-core/report/html/renderer/crc-details-renderer.js index 4800d4a32900..26ed81bdbacb 100644 --- a/lighthouse-core/report/html/renderer/crc-details-renderer.js +++ b/lighthouse-core/report/html/renderer/crc-details-renderer.js @@ -21,12 +21,12 @@ * critical request chains network tree. */ -/* globals self Util */ +/** @typedef {import('./dom.js').DOM} DOM */ +/** @typedef {import('./details-renderer.js').DetailsRenderer} DetailsRenderer */ -/** @typedef {import('./dom.js')} DOM */ -/** @typedef {import('./details-renderer.js')} DetailsRenderer */ +import {Util} from './util.js'; -class CriticalRequestChainRenderer { +export class CriticalRequestChainRenderer { /** * Create render context for critical-request-chain tree display. * @param {LH.Audit.SimpleCriticalRequestNode} tree @@ -193,13 +193,6 @@ class CriticalRequestChainRenderer { // Alias b/c the name is really long. const CRCRenderer = CriticalRequestChainRenderer; -// Allow Node require()'ing. -if (typeof module !== 'undefined' && module.exports) { - module.exports = CriticalRequestChainRenderer; -} else { - self.CriticalRequestChainRenderer = CriticalRequestChainRenderer; -} - /** @typedef {{ node: LH.Audit.SimpleCriticalRequestNode[string], isLastChild: boolean, diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js index a65fa8702702..da4e1e361f4d 100644 --- a/lighthouse-core/report/html/renderer/details-renderer.js +++ b/lighthouse-core/report/html/renderer/details-renderer.js @@ -16,9 +16,7 @@ */ 'use strict'; -/* globals self CriticalRequestChainRenderer SnippetRenderer ElementScreenshotRenderer Util */ - -/** @typedef {import('./dom.js')} DOM */ +/** @typedef {import('./dom.js').DOM} DOM */ // Convenience types for localized AuditDetails. /** @typedef {LH.FormattedIcu} AuditDetails */ @@ -27,9 +25,14 @@ /** @typedef {LH.FormattedIcu} TableItem */ /** @typedef {LH.FormattedIcu} TableItemValue */ +import {Util} from './util.js'; +import {CriticalRequestChainRenderer} from './crc-details-renderer.js'; +import {SnippetRenderer} from './snippet-renderer.js'; +import {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; + const URL_PREFIXES = ['http://', 'https://', 'data:']; -class DetailsRenderer { +export class DetailsRenderer { /** * @param {DOM} dom * @param {{fullPageScreenshot?: LH.Artifacts.FullPageScreenshot}} [options] @@ -614,9 +617,3 @@ class DetailsRenderer { return pre; } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = DetailsRenderer; -} else { - self.DetailsRenderer = DetailsRenderer; -} diff --git a/lighthouse-core/report/html/renderer/dom.js b/lighthouse-core/report/html/renderer/dom.js index d776d801e93c..695ba01194d3 100644 --- a/lighthouse-core/report/html/renderer/dom.js +++ b/lighthouse-core/report/html/renderer/dom.js @@ -16,12 +16,12 @@ */ 'use strict'; -/* globals self Util */ - /** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */ /** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector} ParseSelector */ -class DOM { +import {Util} from './util.js'; + +export class DOM { /** * @param {Document} document */ @@ -242,9 +242,3 @@ class DOM { return elements; } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = DOM; -} else { - self.DOM = DOM; -} diff --git a/lighthouse-core/report/html/renderer/element-screenshot-renderer.js b/lighthouse-core/report/html/renderer/element-screenshot-renderer.js index bd635accdeb5..cd1507af01a8 100644 --- a/lighthouse-core/report/html/renderer/element-screenshot-renderer.js +++ b/lighthouse-core/report/html/renderer/element-screenshot-renderer.js @@ -11,9 +11,7 @@ * 2. Display coords (DC suffix): that match the CSS pixel coordinate space of the LH report's page. */ -/* globals self Util */ - -/** @typedef {import('./dom.js')} DOM */ +/** @typedef {import('./dom.js').DOM} DOM */ /** @typedef {LH.Artifacts.Rect} Rect */ /** @typedef {{width: number, height: number}} Size */ @@ -26,6 +24,8 @@ * @property {LH.Artifacts.FullPageScreenshot} fullPageScreenshot */ +import {Util} from './util.js'; + /** * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot * @param {LH.Artifacts.Rect} rect @@ -59,7 +59,7 @@ function getRectCenterPoint(rect) { }; } -class ElementScreenshotRenderer { +export class ElementScreenshotRenderer { /** * Given the location of an element and the sizes of the preview and screenshot, * compute the absolute positions (in screenshot coordinate scale) of the screenshot content @@ -288,9 +288,3 @@ class ElementScreenshotRenderer { return containerEl; } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = ElementScreenshotRenderer; -} else { - self.ElementScreenshotRenderer = ElementScreenshotRenderer; -} diff --git a/lighthouse-core/report/html/renderer/file-namer.js b/lighthouse-core/report/html/renderer/file-namer.js new file mode 100644 index 000000000000..97082a1f3fb9 --- /dev/null +++ b/lighthouse-core/report/html/renderer/file-namer.js @@ -0,0 +1,8 @@ +/** + * @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'; + +export * from '../../../lib/file-namer.js'; diff --git a/lighthouse-core/report/html/renderer/i18n.js b/lighthouse-core/report/html/renderer/i18n.js index 4bff20e7db70..48b5e3e6f41b 100644 --- a/lighthouse-core/report/html/renderer/i18n.js +++ b/lighthouse-core/report/html/renderer/i18n.js @@ -5,8 +5,6 @@ */ 'use strict'; -/* globals self */ - // Not named `NBSP` because that creates a duplicate identifier (util.js). const NBSP2 = '\xa0'; const KiB = 1024; @@ -15,7 +13,7 @@ const MiB = KiB * KiB; /** * @template T */ -class I18n { +export class I18n { /** * @param {LH.Locale} locale * @param {T} strings @@ -72,7 +70,7 @@ class I18n { */ formatBytesToMiB(size, granularity = 0.1) { const formatter = this._byteFormatterForGranularity(granularity); - const kbs = formatter.format(Math.round(size / 1024 ** 2 / granularity) * granularity); + const kbs = formatter.format(Math.round(size / (1024 ** 2) / granularity) * granularity); return `${kbs}${NBSP2}MiB`; } @@ -198,9 +196,3 @@ class I18n { return parts.join(' '); } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = I18n; -} else { - self.I18n = I18n; -} diff --git a/lighthouse-core/report/html/renderer/logger.js b/lighthouse-core/report/html/renderer/logger.js index 9044dac93586..76b4aa6f063e 100644 --- a/lighthouse-core/report/html/renderer/logger.js +++ b/lighthouse-core/report/html/renderer/logger.js @@ -19,7 +19,7 @@ /** * Logs messages via a UI butter. */ -class Logger { +export class Logger { /** * @param {Element} element */ @@ -74,7 +74,3 @@ class Logger { this.el.classList.remove('show'); } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = Logger; -} diff --git a/lighthouse-core/report/html/renderer/main.js b/lighthouse-core/report/html/renderer/main.js new file mode 100644 index 000000000000..3a94fd794bea --- /dev/null +++ b/lighthouse-core/report/html/renderer/main.js @@ -0,0 +1,57 @@ +/** + * @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 */ + +import {DOM} from './dom.js'; +import {Logger} from './logger.js'; +import {ReportRenderer} from './report-renderer.js'; +import {ReportUIFeatures} from './report-ui-features.js'; + +function __initLighthouseReport__() { + const dom = new DOM(document); + const renderer = new ReportRenderer(dom); + + const container = document.querySelector('main'); + renderer.renderReport(window.__LIGHTHOUSE_JSON__, container); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(window.__LIGHTHOUSE_JSON__); +} + +if (document.readyState === 'loading') { + window.addEventListener('DOMContentLoaded', __initLighthouseReport__); +} else { + __initLighthouseReport__(); +} + +document.addEventListener('lh-analytics', e => { + if (window.ga) { + ga(e.detail.cmd, e.detail.fields); + } +}); + +document.addEventListener('lh-log', e => { + const logger = new Logger(document.querySelector('#lh-log')); + + switch (e.detail.cmd) { + case 'log': + logger.log(e.detail.msg); + break; + case 'warn': + logger.warn(e.detail.msg); + break; + case 'error': + logger.error(e.detail.msg); + break; + case 'hide': + logger.hide(); + break; + } +}); diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js index 32997aede04d..7130dfb2d47d 100644 --- a/lighthouse-core/report/html/renderer/performance-category-renderer.js +++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js @@ -16,11 +16,12 @@ */ 'use strict'; -/* globals self, Util, CategoryRenderer */ +/** @typedef {import('./dom.js').DOM} DOM */ -/** @typedef {import('./dom.js')} DOM */ +import {Util} from './util.js'; +import {CategoryRenderer} from './category-renderer.js'; -class PerformanceCategoryRenderer extends CategoryRenderer { +export class PerformanceCategoryRenderer extends CategoryRenderer { /** * @param {LH.ReportResult.AuditRef} audit * @return {!Element} @@ -361,9 +362,3 @@ class PerformanceCategoryRenderer extends CategoryRenderer { } } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = PerformanceCategoryRenderer; -} else { - self.PerformanceCategoryRenderer = PerformanceCategoryRenderer; -} diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js index de6248bcc5e2..17be28ad9565 100644 --- a/lighthouse-core/report/html/renderer/psi.js +++ b/lighthouse-core/report/html/renderer/psi.js @@ -16,7 +16,13 @@ */ 'use strict'; -/* globals self DOM PerformanceCategoryRenderer Util I18n DetailsRenderer ElementScreenshotRenderer ReportUIFeatures */ +import {DetailsRenderer} from './details-renderer.js'; +import {DOM} from './dom.js'; +import {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; +import {I18n} from './i18n.js'; +import {PerformanceCategoryRenderer} from './performance-category-renderer.js'; +import {ReportUIFeatures} from './report-ui-features.js'; +import {Util} from './util.js'; /** * Returns all the elements that PSI needs to render the report @@ -32,7 +38,7 @@ * @param {Document} document The host page's window.document * @return {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null, scoreScaleEl: Element, installFeatures: Function}} */ -function prepareLabData(LHResult, document) { +export function prepareLabData(LHResult, document) { const lhResult = (typeof LHResult === 'string') ? /** @type {LH.Result} */ (JSON.parse(LHResult)) : LHResult; @@ -138,16 +144,3 @@ function _getFinalScreenshot(perfCategory) { if (!details || details.type !== 'screenshot') return null; return details.data; } - -// Defined by lib/file-namer.js, but that file does not exist in PSI. PSI doesn't use it, but -// needs some basic definition so closure compiler accepts report-ui-features.js -// @ts-expect-error - unused by typescript, used by closure compiler -// eslint-disable-next-line no-unused-vars -function getFilenamePrefix(lhr) { -} - -if (typeof module !== 'undefined' && module.exports) { - module.exports = prepareLabData; -} else { - self.prepareLabData = prepareLabData; -} diff --git a/lighthouse-core/report/html/renderer/pwa-category-renderer.js b/lighthouse-core/report/html/renderer/pwa-category-renderer.js index fdb5b6906810..b66f0833db69 100644 --- a/lighthouse-core/report/html/renderer/pwa-category-renderer.js +++ b/lighthouse-core/report/html/renderer/pwa-category-renderer.js @@ -16,9 +16,10 @@ */ 'use strict'; -/* globals self, Util, CategoryRenderer */ +import {Util} from './util.js'; +import {CategoryRenderer} from './category-renderer.js'; -class PwaCategoryRenderer extends CategoryRenderer { +export class PwaCategoryRenderer extends CategoryRenderer { /** * @param {LH.ReportResult.Category} category * @param {Object} [groupDefinitions] @@ -184,9 +185,3 @@ class PwaCategoryRenderer extends CategoryRenderer { } } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = PwaCategoryRenderer; -} else { - self.PwaCategoryRenderer = PwaCategoryRenderer; -} diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js index a45b0cc2f8f0..653ad2e7645d 100644 --- a/lighthouse-core/report/html/renderer/report-renderer.js +++ b/lighthouse-core/report/html/renderer/report-renderer.js @@ -23,12 +23,17 @@ * Dummy text for ensuring report robustness: pre$`post %%LIGHTHOUSE_JSON%% */ -/** @typedef {import('./category-renderer')} CategoryRenderer */ -/** @typedef {import('./dom.js')} DOM */ +/** @typedef {import('./dom.js').DOM} DOM */ -/* globals self, Util, DetailsRenderer, CategoryRenderer, I18n, PerformanceCategoryRenderer, PwaCategoryRenderer, ElementScreenshotRenderer */ +import {CategoryRenderer} from './category-renderer.js'; +import {DetailsRenderer} from './details-renderer.js'; +import {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; +import {I18n} from './i18n.js'; +import {PerformanceCategoryRenderer} from './performance-category-renderer.js'; +import {PwaCategoryRenderer} from './pwa-category-renderer.js'; +import {Util} from './util.js'; -class ReportRenderer { +export class ReportRenderer { /** * @param {DOM} dom */ @@ -276,9 +281,3 @@ class ReportRenderer { return reportFragment; } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = ReportRenderer; -} else { - self.ReportRenderer = ReportRenderer; -} diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js index 88b6f8120efb..a33c74f611fa 100644 --- a/lighthouse-core/report/html/renderer/report-ui-features.js +++ b/lighthouse-core/report/html/renderer/report-ui-features.js @@ -23,9 +23,12 @@ * the report. */ -/* globals getFilenamePrefix Util TextEncoding ElementScreenshotRenderer */ +/** @typedef {import('./dom').DOM} DOM */ -/** @typedef {import('./dom')} DOM */ +import {getFilenamePrefix} from './file-namer.js'; +import {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; +import {TextEncoding} from './text-encoding.js'; +import {Util} from './util.js'; /** * @param {HTMLTableElement} tableEl @@ -44,7 +47,7 @@ function getAppsOrigin() { return 'https://googlechrome.github.io/lighthouse'; } -class ReportUIFeatures { +export class ReportUIFeatures { /** * @param {DOM} dom */ @@ -958,9 +961,3 @@ class DropDown { return this._getNextSelectableNode(nodes, startEl); } } - -if (typeof module !== 'undefined' && module.exports) { - module.exports = ReportUIFeatures; -} else { - self.ReportUIFeatures = ReportUIFeatures; -} diff --git a/lighthouse-core/report/html/renderer/snippet-renderer.js b/lighthouse-core/report/html/renderer/snippet-renderer.js index 72fce017c7cd..dfacbad42253 100644 --- a/lighthouse-core/report/html/renderer/snippet-renderer.js +++ b/lighthouse-core/report/html/renderer/snippet-renderer.js @@ -5,10 +5,10 @@ */ 'use strict'; -/* globals self, Util */ +/** @typedef {import('./details-renderer').DetailsRenderer} DetailsRenderer */ +/** @typedef {import('./dom').DOM} DOM */ -/** @typedef {import('./details-renderer')} DetailsRenderer */ -/** @typedef {import('./dom')} DOM */ +import {Util} from './util.js'; /** @enum {number} */ const LineVisibility = { @@ -87,7 +87,7 @@ function getLinesWhenCollapsed(details) { * can click "Expand snippet" to show more. * Content lines with annotations are highlighted. */ -class SnippetRenderer { +export class SnippetRenderer { /** * @param {DOM} dom * @param {DocumentFragment} tmpl @@ -356,10 +356,3 @@ class SnippetRenderer { return snippetEl; } } - -// Allow Node require()'ing. -if (typeof module !== 'undefined' && module.exports) { - module.exports = SnippetRenderer; -} else { - self.SnippetRenderer = SnippetRenderer; -} diff --git a/lighthouse-core/report/html/renderer/text-encoding.js b/lighthouse-core/report/html/renderer/text-encoding.js index 4386eff0fd55..7ed5d0bff3ea 100644 --- a/lighthouse-core/report/html/renderer/text-encoding.js +++ b/lighthouse-core/report/html/renderer/text-encoding.js @@ -5,7 +5,7 @@ */ 'use strict'; -/* global self btoa atob window CompressionStream Response */ +/* global btoa atob window CompressionStream Response */ const btoa_ = typeof btoa !== 'undefined' ? btoa : @@ -71,8 +71,4 @@ function fromBase64(encoded, options) { } } -if (typeof module !== 'undefined' && module.exports) { - module.exports = {toBase64, fromBase64}; -} else { - self.TextEncoding = {toBase64, fromBase64}; -} +export const TextEncoding = {toBase64, fromBase64}; diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js index a979531a1712..535c1bd02f91 100644 --- a/lighthouse-core/report/html/renderer/util.js +++ b/lighthouse-core/report/html/renderer/util.js @@ -16,9 +16,7 @@ */ 'use strict'; -/* globals self */ - -/** @template T @typedef {import('./i18n')} I18n */ +/** @template T @typedef {import('./i18n').I18n} I18n */ const ELLIPSIS = '\u2026'; const NBSP = '\xa0'; @@ -40,7 +38,7 @@ const listOfTlds = [ 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', ]; -class Util { +export class Util { static get PASS_THRESHOLD() { return PASS_THRESHOLD; } @@ -639,9 +637,3 @@ Util.UIStrings = { /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */ throttlingProvided: 'Provided by environment', }; - -if (typeof module !== 'undefined' && module.exports) { - module.exports = Util; -} else { - self.Util = Util; -} diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html index 96c481103efe..a586ca0bf6da 100644 --- a/lighthouse-core/report/html/report-template.html +++ b/lighthouse-core/report/html/report-template.html @@ -31,50 +31,51 @@
+ %%LIGHTHOUSE_JAVASCRIPT_MODULES%% + + + + + - diff --git a/lighthouse-core/report/report-generator.js b/lighthouse-core/report/report-generator.js index 9996be60f9ea..51c3ed7ea367 100644 --- a/lighthouse-core/report/report-generator.js +++ b/lighthouse-core/report/report-generator.js @@ -39,9 +39,17 @@ class ReportGenerator { .replace(/\u2029/g, '\\u2029'); // replaces paragraph separators const sanitizedJavascript = htmlReportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/'); + let sanitizedJavascriptModules = ''; + for (const [id, code] of Object.entries(htmlReportAssets.REPORT_JAVASCRIPT_MODULES)) { + const sanitizedCode = code.replace(/<\//g, '\\u003c/'); + sanitizedJavascriptModules += + ``; + } + return ReportGenerator.replaceStrings(htmlReportAssets.REPORT_TEMPLATE, [ {search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson}, {search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: sanitizedJavascript}, + {search: '%%LIGHTHOUSE_JAVASCRIPT_MODULES%%', replacement: sanitizedJavascriptModules}, {search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: htmlReportAssets.REPORT_CSS}, {search: '%%LIGHTHOUSE_TEMPLATES%%', replacement: htmlReportAssets.REPORT_TEMPLATES}, ]); diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index 170c7aa438f2..b31c5fab60da 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -43,6 +43,13 @@ lh_bg_js="dist/lighthouse-dt-bundle.js" cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." +# copy report code $fe_lh_dir +fe_lh_report_dir="$fe_lh_dir/report/" +rsync -avh lighthouse-core/report/html/renderer/ "$fe_lh_report_dir" --exclude="BUILD.gn" --delete +# file-namer.js is not used, but we should export something so it compiles. +echo 'export const getFilenamePrefix = () => {};' > "$fe_lh_report_dir/file-namer.js" +echo -e "$check Report code copied." + # copy report generator + cached resources into $fe_lh_dir fe_lh_report_assets_dir="$fe_lh_dir/report-assets/" rsync -avh dist/dt-report-resources/ "$fe_lh_report_assets_dir" --delete diff --git a/types/html-renderer.d.ts b/types/html-renderer.d.ts index 4d03fef5e601..a32f25065aa5 100644 --- a/types/html-renderer.d.ts +++ b/types/html-renderer.d.ts @@ -4,38 +4,7 @@ * 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 _CategoryRenderer = require('../lighthouse-core/report/html/renderer/category-renderer.js'); -import _CriticalRequestChainRenderer = require('../lighthouse-core/report/html/renderer/crc-details-renderer.js'); -import _SnippetRenderer = require('../lighthouse-core/report/html/renderer/snippet-renderer.js'); -import _ElementScreenshotRenderer = require('../lighthouse-core/report/html/renderer/element-screenshot-renderer.js'); -import _DetailsRenderer = require('../lighthouse-core/report/html/renderer/details-renderer.js'); -import _DOM = require('../lighthouse-core/report/html/renderer/dom.js'); -import _I18n = require('../lighthouse-core/report/html/renderer/i18n.js'); -import _PerformanceCategoryRenderer = require('../lighthouse-core/report/html/renderer/performance-category-renderer.js'); -import _PwaCategoryRenderer = require('../lighthouse-core/report/html/renderer/pwa-category-renderer.js'); -import _ReportRenderer = require('../lighthouse-core/report/html/renderer/report-renderer.js'); -import _ReportUIFeatures = require('../lighthouse-core/report/html/renderer/report-ui-features.js'); -import _Util = require('../lighthouse-core/report/html/renderer/util.js'); -import _TextEncoding = require('../lighthouse-core/report/html/renderer/text-encoding.js'); -import _prepareLabData = require('../lighthouse-core/report/html/renderer/psi.js'); -import _FileNamer = require('../lighthouse-core/lib/file-namer.js'); - declare global { - var CategoryRenderer: typeof _CategoryRenderer; - var CriticalRequestChainRenderer: typeof _CriticalRequestChainRenderer; - var SnippetRenderer: typeof _SnippetRenderer; - var ElementScreenshotRenderer: typeof _ElementScreenshotRenderer - var DetailsRenderer: typeof _DetailsRenderer; - var DOM: typeof _DOM; - var getFilenamePrefix: typeof _FileNamer.getFilenamePrefix; - var I18n: typeof _I18n; - var PerformanceCategoryRenderer: typeof _PerformanceCategoryRenderer; - var PwaCategoryRenderer: typeof _PwaCategoryRenderer; - var ReportRenderer: typeof _ReportRenderer; - var ReportUIFeatures: typeof _ReportUIFeatures; - var Util: typeof _Util; - var TextEncoding: typeof _TextEncoding; - var prepareLabData: typeof _prepareLabData; var CompressionStream: { prototype: CompressionStream, new (format: string): CompressionStream, @@ -45,22 +14,6 @@ declare global { readonly format: string; } - interface Window { - CategoryRenderer: typeof _CategoryRenderer; - CriticalRequestChainRenderer: typeof _CriticalRequestChainRenderer; - SnippetRenderer: typeof _SnippetRenderer; - ElementScreenshotRenderer: typeof _ElementScreenshotRenderer - DetailsRenderer: typeof _DetailsRenderer; - DOM: typeof _DOM; - I18n: typeof _I18n; - PerformanceCategoryRenderer: typeof _PerformanceCategoryRenderer; - PwaCategoryRenderer: typeof _PwaCategoryRenderer; - ReportRenderer: typeof _ReportRenderer; - ReportUIFeatures: typeof _ReportUIFeatures; - Util: typeof _Util; - prepareLabData: typeof _prepareLabData; - } - module LH { // During report generation, the LHR object is transformed a bit for convenience // Primarily, the auditResult is added as .result onto the auditRef. We're lazy sometimes. It'll be removed in due time. diff --git a/types/i18n.d.ts b/types/i18n.d.ts index 42e716e2400a..624c2aec087f 100644 --- a/types/i18n.d.ts +++ b/types/i18n.d.ts @@ -4,7 +4,7 @@ * 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 Util = require('../lighthouse-core/report/html/renderer/util.js'); +import {Util} from '../lighthouse-core/report/html/renderer/util.js'; declare global { module LH { From e89d095ac77ab6f999840b1761d8a2cf94bab795 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 4 Jun 2021 14:25:36 -0700 Subject: [PATCH 02/71] rollup yum --- build/build-report.js | 53 + .../report/html/html-report-assets.js | 53 +- .../{ => common}/category-renderer.js | 0 .../{ => common}/crc-details-renderer.js | 0 .../renderer/{ => common}/details-renderer.js | 0 .../report/html/renderer/{ => common}/dom.js | 0 .../element-screenshot-renderer.js | 0 .../html/renderer/{ => common}/file-namer.js | 4 +- .../report/html/renderer/{ => common}/i18n.js | 0 .../report/html/renderer/common/index.js | 22 + .../html/renderer/{ => common}/logger.js | 0 .../performance-category-renderer.js | 0 .../{ => common}/pwa-category-renderer.js | 0 .../renderer/{ => common}/report-renderer.js | 0 .../{ => common}/report-ui-features.js | 0 .../renderer/{ => common}/snippet-renderer.js | 0 .../renderer/{ => common}/text-encoding.js | 0 .../report/html/renderer/{ => common}/util.js | 0 .../html/renderer/generated/standalone.js | 4991 +++++++++++++++++ lighthouse-core/report/html/renderer/psi.js | 19 +- .../html/renderer/{main.js => standalone.js} | 8 +- .../report/html/report-template.html | 40 - lighthouse-core/report/report-generator.js | 14 +- lighthouse-core/runner.js | 6 + package.json | 2 + types/i18n.d.ts | 2 +- yarn.lock | 50 +- 27 files changed, 5166 insertions(+), 98 deletions(-) create mode 100644 build/build-report.js rename lighthouse-core/report/html/renderer/{ => common}/category-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/crc-details-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/details-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/dom.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/element-screenshot-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/file-namer.js (84%) rename lighthouse-core/report/html/renderer/{ => common}/i18n.js (100%) create mode 100644 lighthouse-core/report/html/renderer/common/index.js rename lighthouse-core/report/html/renderer/{ => common}/logger.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/performance-category-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/pwa-category-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/report-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/report-ui-features.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/snippet-renderer.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/text-encoding.js (100%) rename lighthouse-core/report/html/renderer/{ => common}/util.js (100%) create mode 100644 lighthouse-core/report/html/renderer/generated/standalone.js rename lighthouse-core/report/html/renderer/{main.js => standalone.js} (89%) diff --git a/build/build-report.js b/build/build-report.js new file mode 100644 index 000000000000..5b9e0c7f373b --- /dev/null +++ b/build/build-report.js @@ -0,0 +1,53 @@ +/** + * @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'; + +// TODO: where to output? +// standalone: lighthouse-core/report/html/renderer/generated/standalone.js + checking into source seems simplest for publishing. +// esmodules bundle (for devtools/whatever): dist/report.mjs seems good. don't check in cuz dont need it for publishing. + +const rollup = require('rollup'); +const commonjs = + // @ts-expect-error types are wrong. + /** @type {import('rollup-plugin-commonjs').default} */ (require('rollup-plugin-commonjs')); + +async function buildStandaloneReport() { + const bundle = await rollup.rollup({ + input: 'lighthouse-core/report/html/renderer/standalone.js', + plugins: [ + commonjs(), + ], + }); + + await bundle.write({ + file: 'lighthouse-core/report/html/renderer/generated/standalone.js', + format: 'iife', + }); + + // TODO: run thru terser. +} + +async function buildEsModulesBundle() { + const bundle = await rollup.rollup({ + input: 'lighthouse-core/report/html/renderer/common/index.js', + plugins: [ + commonjs(), + ], + }); + + await bundle.write({ + file: 'dist/report.mjs', + format: 'esm', + }); +} + +buildStandaloneReport(); +// TODO buildPsiReport(); ? +buildEsModulesBundle(); + +module.exports = { + buildStandaloneReport, +}; diff --git a/lighthouse-core/report/html/html-report-assets.js b/lighthouse-core/report/html/html-report-assets.js index 746414d4b5fc..c0b387c4b87a 100644 --- a/lighthouse-core/report/html/html-report-assets.js +++ b/lighthouse-core/report/html/html-report-assets.js @@ -8,44 +8,27 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8'); -const REPORT_JAVASCRIPT = [ - // fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), - // fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - // fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), -].join(';\n'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/renderer/generated/standalone.js', 'utf8'); /* eslint-disable max-len */ const REPORT_JAVASCRIPT_MODULES = { - './logger.js': fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - './i18n.js': fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - './text-encoding.js': fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), - './util.js': fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - './dom.js': fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - './crc-details-renderer.js': fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - './snippet-renderer.js': fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - './element-screenshot-renderer.js': fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - './category-renderer.js': fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - './performance-category-renderer.js': fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - './pwa-category-renderer.js': fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - './details-renderer.js': fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - '../../../lib/file-namer.js': fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), - './file-namer.js': fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), - './report-ui-features.js': fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - './report-renderer.js': fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - './main.js': fs.readFileSync(__dirname + '/renderer/main.js', 'utf8'), + // './logger.js': fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), + // './i18n.js': fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), + // './text-encoding.js': fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), + // './util.js': fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), + // './dom.js': fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), + // './crc-details-renderer.js': fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), + // './snippet-renderer.js': fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), + // './element-screenshot-renderer.js': fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), + // './category-renderer.js': fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), + // './performance-category-renderer.js': fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), + // './pwa-category-renderer.js': fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), + // './details-renderer.js': fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), + // '../../../lib/file-namer.js': fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), + // './file-namer.js': fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), + // './report-ui-features.js': fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), + // './report-renderer.js': fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), + // './main.js': fs.readFileSync(__dirname + '/renderer/main.js', 'utf8'), }; /* eslint-enable max-len */ diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/common/category-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/category-renderer.js rename to lighthouse-core/report/html/renderer/common/category-renderer.js diff --git a/lighthouse-core/report/html/renderer/crc-details-renderer.js b/lighthouse-core/report/html/renderer/common/crc-details-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/crc-details-renderer.js rename to lighthouse-core/report/html/renderer/common/crc-details-renderer.js diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/common/details-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/details-renderer.js rename to lighthouse-core/report/html/renderer/common/details-renderer.js diff --git a/lighthouse-core/report/html/renderer/dom.js b/lighthouse-core/report/html/renderer/common/dom.js similarity index 100% rename from lighthouse-core/report/html/renderer/dom.js rename to lighthouse-core/report/html/renderer/common/dom.js diff --git a/lighthouse-core/report/html/renderer/element-screenshot-renderer.js b/lighthouse-core/report/html/renderer/common/element-screenshot-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/element-screenshot-renderer.js rename to lighthouse-core/report/html/renderer/common/element-screenshot-renderer.js diff --git a/lighthouse-core/report/html/renderer/file-namer.js b/lighthouse-core/report/html/renderer/common/file-namer.js similarity index 84% rename from lighthouse-core/report/html/renderer/file-namer.js rename to lighthouse-core/report/html/renderer/common/file-namer.js index 97082a1f3fb9..09a92a22a797 100644 --- a/lighthouse-core/report/html/renderer/file-namer.js +++ b/lighthouse-core/report/html/renderer/common/file-namer.js @@ -5,4 +5,6 @@ */ 'use strict'; -export * from '../../../lib/file-namer.js'; +// export * from '../../../lib/file-namer.js'; + +export {getFilenamePrefix} from '../../../../lib/file-namer.js'; diff --git a/lighthouse-core/report/html/renderer/i18n.js b/lighthouse-core/report/html/renderer/common/i18n.js similarity index 100% rename from lighthouse-core/report/html/renderer/i18n.js rename to lighthouse-core/report/html/renderer/common/i18n.js diff --git a/lighthouse-core/report/html/renderer/common/index.js b/lighthouse-core/report/html/renderer/common/index.js new file mode 100644 index 000000000000..dedf2058365c --- /dev/null +++ b/lighthouse-core/report/html/renderer/common/index.js @@ -0,0 +1,22 @@ +/** + * @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'; + +export {CategoryRenderer} from './category-renderer.js'; +export {CriticalRequestChainRenderer} from './crc-details-renderer.js'; +export {DetailsRenderer} from './details-renderer.js'; +export {DOM} from './dom.js'; +export {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; +export {getFilenamePrefix} from './file-namer.js'; +export {I18n} from './i18n.js'; +export {Logger} from './logger.js'; +export {PerformanceCategoryRenderer} from './performance-category-renderer.js'; +export {PwaCategoryRenderer} from './pwa-category-renderer.js'; +export {ReportRenderer} from './report-renderer.js'; +export {ReportUIFeatures} from './report-ui-features.js'; +export {SnippetRenderer} from './snippet-renderer.js'; +export {TextEncoding} from './text-encoding.js'; +export {Util} from './util.js'; diff --git a/lighthouse-core/report/html/renderer/logger.js b/lighthouse-core/report/html/renderer/common/logger.js similarity index 100% rename from lighthouse-core/report/html/renderer/logger.js rename to lighthouse-core/report/html/renderer/common/logger.js diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/common/performance-category-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/performance-category-renderer.js rename to lighthouse-core/report/html/renderer/common/performance-category-renderer.js diff --git a/lighthouse-core/report/html/renderer/pwa-category-renderer.js b/lighthouse-core/report/html/renderer/common/pwa-category-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/pwa-category-renderer.js rename to lighthouse-core/report/html/renderer/common/pwa-category-renderer.js diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/common/report-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/report-renderer.js rename to lighthouse-core/report/html/renderer/common/report-renderer.js diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/common/report-ui-features.js similarity index 100% rename from lighthouse-core/report/html/renderer/report-ui-features.js rename to lighthouse-core/report/html/renderer/common/report-ui-features.js diff --git a/lighthouse-core/report/html/renderer/snippet-renderer.js b/lighthouse-core/report/html/renderer/common/snippet-renderer.js similarity index 100% rename from lighthouse-core/report/html/renderer/snippet-renderer.js rename to lighthouse-core/report/html/renderer/common/snippet-renderer.js diff --git a/lighthouse-core/report/html/renderer/text-encoding.js b/lighthouse-core/report/html/renderer/common/text-encoding.js similarity index 100% rename from lighthouse-core/report/html/renderer/text-encoding.js rename to lighthouse-core/report/html/renderer/common/text-encoding.js diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/common/util.js similarity index 100% rename from lighthouse-core/report/html/renderer/util.js rename to lighthouse-core/report/html/renderer/common/util.js diff --git a/lighthouse-core/report/html/renderer/generated/standalone.js b/lighthouse-core/report/html/renderer/generated/standalone.js new file mode 100644 index 000000000000..d9c35ebe3373 --- /dev/null +++ b/lighthouse-core/report/html/renderer/generated/standalone.js @@ -0,0 +1,4991 @@ +(function () { + 'use strict'; + + /** + * @license + * Copyright 2017 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. + */ + + /** @template T @typedef {import('./i18n').I18n} I18n */ + + const ELLIPSIS = '\u2026'; + const NBSP = '\xa0'; + const PASS_THRESHOLD = 0.9; + const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,'; + + const RATINGS = { + PASS: {label: 'pass', minScore: PASS_THRESHOLD}, + AVERAGE: {label: 'average', minScore: 0.5}, + FAIL: {label: 'fail'}, + ERROR: {label: 'error'}, + }; + + // 25 most used tld plus one domains (aka public suffixes) from http archive. + // @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 + // The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes + const listOfTlds = [ + 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', + 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', + ]; + + class Util { + static get PASS_THRESHOLD() { + return PASS_THRESHOLD; + } + + static get MS_DISPLAY_VALUE() { + return `%10d${NBSP}ms`; + } + + /** + * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer. + * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be + * compatible with current renderer. + * The LHR passed in is not mutated. + * TODO(team): we all agree the LHR shape change is technical debt we should fix + * @param {LH.Result} result + * @return {LH.ReportResult} + */ + static prepareReportResult(result) { + // If any mutations happen to the report within the renderers, we want the original object untouched + const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result))); + + // If LHR is older (≤3.0.3), it has no locale setting. Set default. + if (!clone.configSettings.locale) { + clone.configSettings.locale = 'en'; + } + if (!clone.configSettings.formFactor) { + // @ts-expect-error fallback handling for emulatedFormFactor + clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor; + } + + for (const audit of Object.values(clone.audits)) { + // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions) + // into 'notApplicable' (LHR ≥4.0). + // @ts-expect-error tsc rightly flags that these values shouldn't occur. + // eslint-disable-next-line max-len + if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') { + audit.scoreDisplayMode = 'notApplicable'; + } + + if (audit.details) { + // Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0) + // into 'debugdata' (LHR ≥5.0). + // @ts-expect-error tsc rightly flags that these values shouldn't occur. + if (audit.details.type === undefined || audit.details.type === 'diagnostic') { + // @ts-expect-error details is of type never. + audit.details.type = 'debugdata'; + } + + // Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0). + if (audit.details.type === 'filmstrip') { + for (const screenshot of audit.details.items) { + if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) { + screenshot.data = SCREENSHOT_PREFIX + screenshot.data; + } + } + } + } + } + + // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group) + if (typeof clone.categories !== 'object') throw new Error('No categories provided.'); + + /** @type {Map>} */ + const relevantAuditToMetricsMap = new Map(); + + for (const category of Object.values(clone.categories)) { + // Make basic lookup table for relevantAudits + category.auditRefs.forEach(metricRef => { + if (!metricRef.relevantAudits) return; + metricRef.relevantAudits.forEach(auditId => { + const arr = relevantAuditToMetricsMap.get(auditId) || []; + arr.push(metricRef); + relevantAuditToMetricsMap.set(auditId, arr); + }); + }); + + category.auditRefs.forEach(auditRef => { + const result = clone.audits[auditRef.id]; + auditRef.result = result; + + // Attach any relevantMetric auditRefs + if (relevantAuditToMetricsMap.has(auditRef.id)) { + auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id); + } + + // attach the stackpacks to the auditRef object + if (clone.stackPacks) { + clone.stackPacks.forEach(pack => { + if (pack.descriptions[auditRef.id]) { + auditRef.stackPacks = auditRef.stackPacks || []; + auditRef.stackPacks.push({ + title: pack.title, + iconDataURL: pack.iconDataURL, + description: pack.descriptions[auditRef.id], + }); + } + }); + } + }); + } + + return clone; + } + + /** + * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed" + * sections of the report. + * + * @param {{score: (number|null), scoreDisplayMode: string}} audit + * @return {boolean} + */ + static showAsPassed(audit) { + switch (audit.scoreDisplayMode) { + case 'manual': + case 'notApplicable': + return true; + case 'error': + case 'informative': + return false; + case 'numeric': + case 'binary': + default: + return Number(audit.score) >= RATINGS.PASS.minScore; + } + } + + /** + * Convert a score to a rating label. + * @param {number|null} score + * @param {string=} scoreDisplayMode + * @return {string} + */ + static calculateRating(score, scoreDisplayMode) { + // Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error' + if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') { + return RATINGS.PASS.label; + } else if (scoreDisplayMode === 'error') { + return RATINGS.ERROR.label; + } else if (score === null) { + return RATINGS.FAIL.label; + } + + // At this point, we're rating a standard binary/numeric audit + let rating = RATINGS.FAIL.label; + if (score >= RATINGS.PASS.minScore) { + rating = RATINGS.PASS.label; + } else if (score >= RATINGS.AVERAGE.minScore) { + rating = RATINGS.AVERAGE.label; + } + return rating; + } + + /** + * Split a string by markdown code spans (enclosed in `backticks`), splitting + * into segments that were enclosed in backticks (marked as `isCode === true`) + * and those that outside the backticks (`isCode === false`). + * @param {string} text + * @return {Array<{isCode: true, text: string}|{isCode: false, text: string}>} + */ + static splitMarkdownCodeSpans(text) { + /** @type {Array<{isCode: true, text: string}|{isCode: false, text: string}>} */ + const segments = []; + + // Split on backticked code spans. + const parts = text.split(/`(.*?)`/g); + for (let i = 0; i < parts.length; i ++) { + const text = parts[i]; + + // Empty strings are an artifact of splitting, not meaningful. + if (!text) continue; + + // Alternates between plain text and code segments. + const isCode = i % 2 !== 0; + segments.push({ + isCode, + text, + }); + } + + return segments; + } + + /** + * Split a string on markdown links (e.g. [some link](https://...)) into + * segments of plain text that weren't part of a link (marked as + * `isLink === false`), and segments with text content and a URL that did make + * up a link (marked as `isLink === true`). + * @param {string} text + * @return {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} + */ + static splitMarkdownLink(text) { + /** @type {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} */ + const segments = []; + + const parts = text.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g); + while (parts.length) { + // Shift off the same number of elements as the pre-split and capture groups. + const [preambleText, linkText, linkHref] = parts.splice(0, 3); + + if (preambleText) { // Skip empty text as it's an artifact of splitting, not meaningful. + segments.push({ + isLink: false, + text: preambleText, + }); + } + + // Append link if there are any. + if (linkText && linkHref) { + segments.push({ + isLink: true, + text: linkText, + linkHref, + }); + } + } + + return segments; + } + + /** + * @param {URL} parsedUrl + * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options + * @return {string} + */ + static getURLDisplayName(parsedUrl, options) { + // Closure optional properties aren't optional in tsc, so fallback needs undefined values. + options = options || {numPathParts: undefined, preserveQuery: undefined, + preserveHost: undefined}; + const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2; + const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true; + const preserveHost = options.preserveHost || false; + + let name; + + if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') { + // Handle 'about:*' and 'data:*' URLs specially since they have no path. + name = parsedUrl.href; + } else { + name = parsedUrl.pathname; + const parts = name.split('/').filter(part => part.length); + if (numPathParts && parts.length > numPathParts) { + name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/'); + } + + if (preserveHost) { + name = `${parsedUrl.host}/${name.replace(/^\//, '')}`; + } + if (preserveQuery) { + name = `${name}${parsedUrl.search}`; + } + } + + const MAX_LENGTH = 64; + // Always elide hexadecimal hash + name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`); + // Also elide other hash-like mixed-case strings + name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g, + `$1${ELLIPSIS}`); + // Also elide long number sequences + name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`); + // Merge any adjacent ellipses + name = name.replace(/\u2026+/g, ELLIPSIS); + + // Elide query params first + if (name.length > MAX_LENGTH && name.includes('?')) { + // Try to leave the first query parameter intact + name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`); + + // Remove it all if it's still too long + if (name.length > MAX_LENGTH) { + name = name.replace(/\?.*/, `?${ELLIPSIS}`); + } + } + + // Elide too long names next + if (name.length > MAX_LENGTH) { + const dotIndex = name.lastIndexOf('.'); + if (dotIndex >= 0) { + name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + + // Show file extension + `${ELLIPSIS}${name.slice(dotIndex)}`; + } else { + name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS; + } + } + + return name; + } + + /** + * Split a URL into a file, hostname and origin for easy display. + * @param {string} url + * @return {{file: string, hostname: string, origin: string}} + */ + static parseURL(url) { + const parsedUrl = new URL(url); + return { + file: Util.getURLDisplayName(parsedUrl), + hostname: parsedUrl.hostname, + origin: parsedUrl.origin, + }; + } + + /** + * @param {string|URL} value + * @return {!URL} + */ + static createOrReturnURL(value) { + if (value instanceof URL) { + return value; + } + + return new URL(value); + } + + /** + * Gets the tld of a domain + * + * @param {string} hostname + * @return {string} tld + */ + static getTld(hostname) { + const tlds = hostname.split('.').slice(-2); + + if (!listOfTlds.includes(tlds[0])) { + return `.${tlds[tlds.length - 1]}`; + } + + return `.${tlds.join('.')}`; + } + + /** + * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). + * @param {string|URL} url hostname or URL object + * @returns {string} + */ + static getRootDomain(url) { + const hostname = Util.createOrReturnURL(url).hostname; + const tld = Util.getTld(hostname); + + // tld is .com or .co.uk which means we means that length is 1 to big + // .com => 2 & .co.uk => 3 + const splitTld = tld.split('.'); + + // get TLD + root domain + return hostname.split('.').slice(-splitTld.length).join('.'); + } + + /** + * @param {LH.Config.Settings} settings + * @return {!Array<{name: string, description: string}>} + */ + static getEnvironmentDisplayValues(settings) { + const emulationDesc = Util.getEmulationDescriptions(settings); + + return [ + { + name: Util.i18n.strings.runtimeSettingsDevice, + description: emulationDesc.deviceEmulation, + }, + { + name: Util.i18n.strings.runtimeSettingsNetworkThrottling, + description: emulationDesc.networkThrottling, + }, + { + name: Util.i18n.strings.runtimeSettingsCPUThrottling, + description: emulationDesc.cpuThrottling, + }, + ]; + } + + /** + * @param {LH.Config.Settings} settings + * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}} + */ + static getEmulationDescriptions(settings) { + let cpuThrottling; + let networkThrottling; + + const throttling = settings.throttling; + + switch (settings.throttlingMethod) { + case 'provided': + cpuThrottling = Util.i18n.strings.throttlingProvided; + networkThrottling = Util.i18n.strings.throttlingProvided; + break; + case 'devtools': { + const {cpuSlowdownMultiplier, requestLatencyMs} = throttling; + cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`; + networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` + + `${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` + + `${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`; + break; + } + case 'simulate': { + const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling; + cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`; + networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` + + `${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`; + break; + } + default: + cpuThrottling = Util.i18n.strings.runtimeUnknown; + networkThrottling = Util.i18n.strings.runtimeUnknown; + } + + // TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796 + const deviceEmulation = { + mobile: Util.i18n.strings.runtimeMobileEmulation, + desktop: Util.i18n.strings.runtimeDesktopEmulation, + }[settings.formFactor] || Util.i18n.strings.runtimeNoEmulation; + + return { + deviceEmulation, + cpuThrottling, + networkThrottling, + }; + } + + /** + * Returns only lines that are near a message, or the first few lines if there are + * no line messages. + * @param {LH.Audit.Details.SnippetValue['lines']} lines + * @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages + * @param {number} surroundingLineCount Number of lines to include before and after + * the message. If this is e.g. 2 this function might return 5 lines. + */ + static filterRelevantLines(lines, lineMessages, surroundingLineCount) { + if (lineMessages.length === 0) { + // no lines with messages, just return the first bunch of lines + return lines.slice(0, surroundingLineCount * 2 + 1); + } + + const minGapSize = 3; + const lineNumbersToKeep = new Set(); + // Sort messages so we can check lineNumbersToKeep to see how big the gap to + // the previous line is. + lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0)); + lineMessages.forEach(({lineNumber}) => { + let firstSurroundingLineNumber = lineNumber - surroundingLineCount; + let lastSurroundingLineNumber = lineNumber + surroundingLineCount; + + while (firstSurroundingLineNumber < 1) { + // make sure we still show (surroundingLineCount * 2 + 1) lines in total + firstSurroundingLineNumber++; + lastSurroundingLineNumber++; + } + // If only a few lines would be omitted normally then we prefer to include + // extra lines to avoid the tiny gap + if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) { + firstSurroundingLineNumber -= minGapSize; + } + for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) { + const surroundingLineNumber = i; + lineNumbersToKeep.add(surroundingLineNumber); + } + }); + + return lines.filter(line => lineNumbersToKeep.has(line.lineNumber)); + } + + /** + * @param {string} categoryId + */ + static isPluginCategory(categoryId) { + return categoryId.startsWith('lighthouse-plugin-'); + } + } + + /** + * Some parts of the report renderer require data found on the LHR. Instead of wiring it + * through, we have this global. + * @type {LH.ReportResult | null} + */ + Util.reportJson = null; + + /** + * An always-increasing counter for making unique SVG ID suffixes. + */ + Util.getUniqueSuffix = (() => { + let svgSuffix = 0; + return function() { + return svgSuffix++; + }; + })(); + + /** @type {I18n} */ + // @ts-expect-error: Is set in report renderer. + Util.i18n = null; + + /** + * Report-renderer-specific strings. + */ + Util.UIStrings = { + /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */ + varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.', + /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */ + calculatorLink: 'See calculator.', + /** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */ + showRelevantAudits: 'Show audits relevant to:', + /** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */ + opportunityResourceColumnLabel: 'Opportunity', + /** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */ + opportunitySavingsColumnLabel: 'Estimated Savings', + + /** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */ + errorMissingAuditInfo: 'Report error: no audit information', + /** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */ + errorLabel: 'Error!', + /** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */ + warningHeader: 'Warnings: ', + /** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */ + warningAuditsGroupTitle: 'Passed audits but with warnings', + /** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + passedAuditsGroupTitle: 'Passed audits', + /** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + notApplicableAuditsGroupTitle: 'Not applicable', + /** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + manualAuditsGroupTitle: 'Additional items to manually check', + + /** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */ + toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:', + + /** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */ + crcInitialNavigation: 'Initial Navigation', + /** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */ + crcLongestDurationLabel: 'Maximum critical path latency:', + + /** Label for button that shows all lines of the snippet when clicked */ + snippetExpandButtonLabel: 'Expand snippet', + /** Label for button that only shows a few lines of the snippet when clicked */ + snippetCollapseButtonLabel: 'Collapse snippet', + + /** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */ + lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.', + /** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */ + labDataTitle: 'Lab Data', + + /** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */ + thirdPartyResourcesLabel: 'Show 3rd-party resources', + /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */ + viewTreemapLabel: 'View Treemap', + + /** Option in a dropdown menu that opens a small, summary report in a print dialog. */ + dropdownPrintSummary: 'Print Summary', + /** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */ + dropdownPrintExpanded: 'Print Expanded', + /** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */ + dropdownCopyJSON: 'Copy JSON', + /** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */ + dropdownSaveHTML: 'Save as HTML', + /** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */ + dropdownSaveJSON: 'Save as JSON', + /** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */ + dropdownViewer: 'Open in Viewer', + /** Option in a dropdown menu that saves the current report as a new GitHub Gist. */ + dropdownSaveGist: 'Save as Gist', + /** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */ + dropdownDarkTheme: 'Toggle Dark Theme', + + /** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */ + runtimeSettingsTitle: 'Runtime Settings', + /** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */ + runtimeSettingsUrl: 'URL', + /** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */ + runtimeSettingsFetchTime: 'Fetch Time', + /** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */ + runtimeSettingsDevice: 'Device', + /** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */ + runtimeSettingsNetworkThrottling: 'Network throttling', + /** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/ + runtimeSettingsCPUThrottling: 'CPU throttling', + /** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */ + runtimeSettingsChannel: 'Channel', + /** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */ + runtimeSettingsUA: 'User agent (host)', + /** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */ + runtimeSettingsUANetwork: 'User agent (network)', + /** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */ + runtimeSettingsBenchmark: 'CPU/Memory Power', + /** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */ + runtimeSettingsAxeVersion: 'Axe version', + + /** Label for button to create an issue against the Lighthouse GitHub project. */ + footerIssue: 'File an issue', + + /** Descriptive explanation for emulation setting when no device emulation is set. */ + runtimeNoEmulation: 'No emulation', + /** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */ + runtimeMobileEmulation: 'Emulated Moto G4', + /** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */ + runtimeDesktopEmulation: 'Emulated Desktop', + /** Descriptive explanation for a runtime setting that is set to an unknown value. */ + runtimeUnknown: 'Unknown', + + /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */ + throttlingProvided: 'Provided by environment', + }; + + /** + * @license + * Copyright 2017 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. + */ + + class DOM { + /** + * @param {Document} document + */ + constructor(document) { + /** @type {Document} */ + this._document = document; + /** @type {string} */ + this._lighthouseChannel = 'unknown'; + } + + /** + * @template {string} T + * @param {T} name + * @param {string=} className + * @param {Object=} attrs Attribute key/val pairs. + * Note: if an attribute key has an undefined value, this method does not + * set the attribute on the node. + * @return {HTMLElementByTagName[T]} + */ + createElement(name, className, attrs = {}) { + const element = this._document.createElement(name); + if (className) { + element.className = className; + } + Object.keys(attrs).forEach(key => { + const value = attrs[key]; + if (typeof value !== 'undefined') { + element.setAttribute(key, value); + } + }); + return element; + } + + /** + * @param {string} namespaceURI + * @param {string} name + * @param {string=} className + * @param {Object=} attrs Attribute key/val pairs. + * Note: if an attribute key has an undefined value, this method does not + * set the attribute on the node. + * @return {Element} + */ + createElementNS(namespaceURI, name, className, attrs = {}) { + const element = this._document.createElementNS(namespaceURI, name); + if (className) { + element.className = className; + } + Object.keys(attrs).forEach(key => { + const value = attrs[key]; + if (typeof value !== 'undefined') { + element.setAttribute(key, value); + } + }); + return element; + } + + /** + * @return {!DocumentFragment} + */ + createFragment() { + return this._document.createDocumentFragment(); + } + + /** + * @template {string} T + * @param {Element} parentElem + * @param {T} elementName + * @param {string=} className + * @param {Object=} attrs Attribute key/val pairs. + * Note: if an attribute key has an undefined value, this method does not + * set the attribute on the node. + * @return {HTMLElementByTagName[T]} + */ + createChildOf(parentElem, elementName, className, attrs) { + const element = this.createElement(elementName, className, attrs); + parentElem.appendChild(element); + return element; + } + + /** + * @param {string} selector + * @param {ParentNode} context + * @return {!DocumentFragment} A clone of the template content. + * @throws {Error} + */ + cloneTemplate(selector, context) { + const template = /** @type {?HTMLTemplateElement} */ (context.querySelector(selector)); + if (!template) { + throw new Error(`Template not found: template${selector}`); + } + + const clone = this._document.importNode(template.content, true); + + // Prevent duplicate styles in the DOM. After a template has been stamped + // for the first time, remove the clone's styles so they're not re-added. + if (template.hasAttribute('data-stamped')) { + this.findAll('style', clone).forEach(style => style.remove()); + } + template.setAttribute('data-stamped', 'true'); + + return clone; + } + + /** + * Resets the "stamped" state of the templates. + */ + resetTemplates() { + this.findAll('template[data-stamped]', this._document).forEach(t => { + t.removeAttribute('data-stamped'); + }); + } + + /** + * @param {string} text + * @return {Element} + */ + convertMarkdownLinkSnippets(text) { + const element = this.createElement('span'); + + for (const segment of Util.splitMarkdownLink(text)) { + if (!segment.isLink) { + // Plain text segment. + element.appendChild(this._document.createTextNode(segment.text)); + continue; + } + + // Otherwise, append any links found. + const url = new URL(segment.linkHref); + + const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev']; + if (DOCS_ORIGINS.includes(url.origin)) { + url.searchParams.set('utm_source', 'lighthouse'); + url.searchParams.set('utm_medium', this._lighthouseChannel); + } + + const a = this.createElement('a'); + a.rel = 'noopener'; + a.target = '_blank'; + a.textContent = segment.text; + a.href = url.href; + element.appendChild(a); + } + + return element; + } + + /** + * @param {string} markdownText + * @return {Element} + */ + convertMarkdownCodeSnippets(markdownText) { + const element = this.createElement('span'); + + for (const segment of Util.splitMarkdownCodeSpans(markdownText)) { + if (segment.isCode) { + const pre = this.createElement('code'); + pre.textContent = segment.text; + element.appendChild(pre); + } else { + element.appendChild(this._document.createTextNode(segment.text)); + } + } + + return element; + } + + /** + * The channel to use for UTM data when rendering links to the documentation. + * @param {string} lighthouseChannel + */ + setLighthouseChannel(lighthouseChannel) { + this._lighthouseChannel = lighthouseChannel; + } + + /** + * @return {Document} + */ + document() { + return this._document; + } + + /** + * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this + * @return {boolean} + */ + isDevTools() { + return !!this._document.querySelector('.lh-devtools'); + } + + /** + * Guaranteed context.querySelector. Always returns an element or throws if + * nothing matches query. + * @template {string} T + * @param {T} query + * @param {ParentNode} context + * @return {ParseSelector} + */ + find(query, context) { + const result = context.querySelector(query); + if (result === null) { + throw new Error(`query ${query} not found`); + } + + // Because we control the report layout and templates, use the simpler + // `typed-query-selector` types that don't require differentiating between + // e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011 + return /** @type {ParseSelector} */ (result); + } + + /** + * Helper for context.querySelectorAll. Returns an Array instead of a NodeList. + * @template {string} T + * @param {T} query + * @param {ParentNode} context + */ + findAll(query, context) { + const elements = Array.from(context.querySelectorAll(query)); + return elements; + } + } + + /** + * @license + * Copyright 2017 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. + */ + + /** + * Logs messages via a UI butter. + */ + class Logger { + /** + * @param {Element} element + */ + constructor(element) { + this.el = element; + this._id = undefined; + } + + /** + * Shows a butter bar. + * @param {string} msg The message to show. + * @param {boolean=} autoHide True to hide the message after a duration. + * Default is true. + */ + log(msg, autoHide = true) { + this._id && clearTimeout(this._id); + + this.el.textContent = msg; + this.el.classList.add('show'); + if (autoHide) { + this._id = setTimeout(_ => { + this.el.classList.remove('show'); + }, 7000); + } + } + + /** + * @param {string} msg + */ + warn(msg) { + this.log('Warning: ' + msg); + } + + /** + * @param {string} msg + */ + error(msg) { + this.log(msg); + + // Rethrow to make sure it's auditable as an error, but in a setTimeout so page + // recovers gracefully and user can try loading a report again. + setTimeout(_ => { + throw new Error(msg); + }, 0); + } + + /** + * Explicitly hides the butter bar. + */ + hide() { + this._id && clearTimeout(this._id); + this.el.classList.remove('show'); + } + } + + /** + * @license + * Copyright 2017 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. + */ + + class CategoryRenderer { + /** + * @param {DOM} dom + * @param {DetailsRenderer} detailsRenderer + */ + constructor(dom, detailsRenderer) { + /** @type {DOM} */ + this.dom = dom; + /** @type {DetailsRenderer} */ + this.detailsRenderer = detailsRenderer; + /** @type {ParentNode} */ + this.templateContext = this.dom.document(); + + this.detailsRenderer.setTemplateContext(this.templateContext); + } + + /** + * Display info per top-level clump. Define on class to avoid race with Util init. + */ + get _clumpTitles() { + return { + warning: Util.i18n.strings.warningAuditsGroupTitle, + manual: Util.i18n.strings.manualAuditsGroupTitle, + passed: Util.i18n.strings.passedAuditsGroupTitle, + notApplicable: Util.i18n.strings.notApplicableAuditsGroupTitle, + }; + } + + /** + * @param {LH.ReportResult.AuditRef} audit + * @return {Element} + */ + renderAudit(audit) { + const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext); + return this.populateAuditValues(audit, tmpl); + } + + /** + * Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity + * @param {LH.ReportResult.AuditRef} audit + * @param {DocumentFragment} tmpl + * @return {!Element} + */ + populateAuditValues(audit, tmpl) { + const strings = Util.i18n.strings; + const auditEl = this.dom.find('.lh-audit', tmpl); + auditEl.id = audit.result.id; + const scoreDisplayMode = audit.result.scoreDisplayMode; + + if (audit.result.displayValue) { + this.dom.find('.lh-audit__display-text', auditEl).textContent = audit.result.displayValue; + } + + const titleEl = this.dom.find('.lh-audit__title', auditEl); + titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title)); + const descEl = this.dom.find('.lh-audit__description', auditEl); + descEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description)); + + for (const relevantMetric of audit.relevantMetrics || []) { + const adornEl = this.dom.createChildOf(descEl, 'span', 'lh-audit__adorn', { + title: `Relevant to ${relevantMetric.result.title}`, + }); + adornEl.textContent = relevantMetric.acronym || relevantMetric.id; + } + + if (audit.stackPacks) { + audit.stackPacks.forEach(pack => { + const packElm = this.dom.createElement('div'); + packElm.classList.add('lh-audit__stackpack'); + + const packElmImg = this.dom.createElement('img'); + packElmImg.classList.add('lh-audit__stackpack__img'); + packElmImg.src = pack.iconDataURL; + packElmImg.alt = pack.title; + packElm.appendChild(packElmImg); + + packElm.appendChild(this.dom.convertMarkdownLinkSnippets(pack.description)); + + this.dom.find('.lh-audit__stackpacks', auditEl) + .appendChild(packElm); + }); + } + + const header = this.dom.find('details', auditEl); + if (audit.result.details) { + const elem = this.detailsRenderer.render(audit.result.details); + if (elem) { + elem.classList.add('lh-details'); + header.appendChild(elem); + } + } + + // Add chevron SVG to the end of the summary + this.dom.find('.lh-chevron-container', auditEl).appendChild(this._createChevron()); + this._setRatingClass(auditEl, audit.result.score, scoreDisplayMode); + + if (audit.result.scoreDisplayMode === 'error') { + auditEl.classList.add(`lh-audit--error`); + const textEl = this.dom.find('.lh-audit__display-text', auditEl); + textEl.textContent = strings.errorLabel; + textEl.classList.add('tooltip-boundary'); + const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error'); + tooltip.textContent = audit.result.errorMessage || strings.errorMissingAuditInfo; + } else if (audit.result.explanation) { + const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation'); + explEl.textContent = audit.result.explanation; + } + const warnings = audit.result.warnings; + if (!warnings || warnings.length === 0) return auditEl; + + // Add list of warnings or singular warning + const summaryEl = this.dom.find('summary', header); + 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(''))); + } else { + const warningsUl = this.dom.createChildOf(warningsEl, 'ul'); + for (const warning of warnings) { + const item = this.dom.createChildOf(warningsUl, 'li'); + item.textContent = warning; + } + } + return auditEl; + } + + /** + * @return {Element} + */ + _createChevron() { + const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext); + const chevronEl = this.dom.find('svg.lh-chevron', chevronTmpl); + return chevronEl; + } + + /** + * @param {Element} element DOM node to populate with values. + * @param {number|null} score + * @param {string} scoreDisplayMode + * @return {!Element} + */ + _setRatingClass(element, score, scoreDisplayMode) { + const rating = Util.calculateRating(score, scoreDisplayMode); + element.classList.add(`lh-audit--${scoreDisplayMode.toLowerCase()}`); + if (scoreDisplayMode !== 'informative') { + element.classList.add(`lh-audit--${rating}`); + } + return element; + } + + /** + * @param {LH.ReportResult.Category} category + * @param {Record} groupDefinitions + * @return {DocumentFragment} + */ + renderCategoryHeader(category, groupDefinitions) { + const tmpl = this.dom.cloneTemplate('#tmpl-lh-category-header', this.templateContext); + + const gaugeContainerEl = this.dom.find('.lh-score__gauge', tmpl); + const gaugeEl = this.renderScoreGauge(category, groupDefinitions); + gaugeContainerEl.appendChild(gaugeEl); + + if (category.description) { + const descEl = this.dom.convertMarkdownLinkSnippets(category.description); + this.dom.find('.lh-category-header__description', tmpl).appendChild(descEl); + } + + return tmpl; + } + + /** + * Renders the group container for a group of audits. Individual audit elements can be added + * directly to the returned element. + * @param {LH.Result.ReportGroup} group + * @return {Element} + */ + renderAuditGroup(group) { + const groupEl = this.dom.createElement('div', 'lh-audit-group'); + + const auditGroupHeader = this.dom.createElement('div', 'lh-audit-group__header'); + + this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title') + .textContent = group.title; + if (group.description) { + const descriptionEl = this.dom.convertMarkdownLinkSnippets(group.description); + descriptionEl.classList.add('lh-audit-group__description'); + auditGroupHeader.appendChild(descriptionEl); + } + groupEl.appendChild(auditGroupHeader); + + return groupEl; + } + + /** + * Takes an array of auditRefs, groups them if requested, then returns an + * array of audit and audit-group elements. + * @param {Array} auditRefs + * @param {Object} groupDefinitions + * @return {Array} + */ + _renderGroupedAudits(auditRefs, groupDefinitions) { + // Audits grouped by their group (or under notAGroup). + /** @type {Map>} */ + const grouped = new Map(); + + // Add audits without a group first so they will appear first. + const notAGroup = 'NotAGroup'; + grouped.set(notAGroup, []); + + for (const auditRef of auditRefs) { + const groupId = auditRef.group || notAGroup; + const groupAuditRefs = grouped.get(groupId) || []; + groupAuditRefs.push(auditRef); + grouped.set(groupId, groupAuditRefs); + } + + /** @type {Array} */ + const auditElements = []; + + for (const [groupId, groupAuditRefs] of grouped) { + if (groupId === notAGroup) { + // Push not-grouped audits individually. + for (const auditRef of groupAuditRefs) { + auditElements.push(this.renderAudit(auditRef)); + } + continue; + } + + // Push grouped audits as a group. + const groupDef = groupDefinitions[groupId]; + const auditGroupElem = this.renderAuditGroup(groupDef); + for (const auditRef of groupAuditRefs) { + auditGroupElem.appendChild(this.renderAudit(auditRef)); + } + auditGroupElem.classList.add(`lh-audit-group--${groupId}`); + auditElements.push(auditGroupElem); + } + + return auditElements; + } + + /** + * Take a set of audits, group them if they have groups, then render in a top-level + * clump that can't be expanded/collapsed. + * @param {Array} auditRefs + * @param {Object} groupDefinitions + * @return {Element} + */ + renderUnexpandableClump(auditRefs, groupDefinitions) { + const clumpElement = this.dom.createElement('div'); + const elements = this._renderGroupedAudits(auditRefs, groupDefinitions); + elements.forEach(elem => clumpElement.appendChild(elem)); + return clumpElement; + } + + /** + * Take a set of audits and render in a top-level, expandable clump that starts + * in a collapsed state. + * @param {Exclude} clumpId + * @param {{auditRefs: Array, description?: string}} clumpOpts + * @return {!Element} + */ + renderClump(clumpId, {auditRefs, description}) { + const clumpTmpl = this.dom.cloneTemplate('#tmpl-lh-clump', this.templateContext); + const clumpElement = this.dom.find('.lh-clump', clumpTmpl); + + if (clumpId === 'warning') { + clumpElement.setAttribute('open', ''); + } + + const summaryInnerEl = this.dom.find('div.lh-audit-group__summary', clumpElement); + summaryInnerEl.appendChild(this._createChevron()); + + const headerEl = this.dom.find('.lh-audit-group__header', clumpElement); + const title = this._clumpTitles[clumpId]; + this.dom.find('.lh-audit-group__title', headerEl).textContent = title; + if (description) { + const descriptionEl = this.dom.convertMarkdownLinkSnippets(description); + descriptionEl.classList.add('lh-audit-group__description'); + headerEl.appendChild(descriptionEl); + } + + const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement); + itemCountEl.textContent = `(${auditRefs.length})`; + + // Add all audit results to the clump. + const auditElements = auditRefs.map(this.renderAudit.bind(this)); + clumpElement.append(...auditElements); + + clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`); + return clumpElement; + } + + /** + * @param {ParentNode} context + */ + setTemplateContext(context) { + this.templateContext = context; + this.detailsRenderer.setTemplateContext(context); + } + + /** + * @param {LH.ReportResult.Category} category + * @param {Record} groupDefinitions + * @return {DocumentFragment} + */ + renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars + const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge', this.templateContext); + const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl); + wrapper.href = `#${category.id}`; + + if (Util.isPluginCategory(category.id)) { + wrapper.classList.add('lh-gauge__wrapper--plugin'); + } + + // Cast `null` to 0 + const numericScore = Number(category.score); + const gauge = this.dom.find('.lh-gauge', tmpl); + const gaugeArc = this.dom.find('circle.lh-gauge-arc', gauge); + + if (gaugeArc) this._setGaugeArc(gaugeArc, numericScore); + + const scoreOutOf100 = Math.round(numericScore * 100); + const percentageEl = this.dom.find('div.lh-gauge__percentage', tmpl); + percentageEl.textContent = scoreOutOf100.toString(); + if (category.score === null) { + percentageEl.textContent = '?'; + percentageEl.title = Util.i18n.strings.errorLabel; + } + + // Render a numerical score if the category has applicable audits, or no audits whatsoever. + if (category.auditRefs.length === 0 || this.hasApplicableAudits(category)) { + wrapper.classList.add(`lh-gauge__wrapper--${Util.calculateRating(category.score)}`); + } else { + wrapper.classList.add(`lh-gauge__wrapper--not-applicable`); + percentageEl.textContent = '-'; + percentageEl.title = Util.i18n.strings.notApplicableAuditsGroupTitle; + } + + this.dom.find('.lh-gauge__label', tmpl).textContent = category.title; + return tmpl; + } + + /** + * Returns true if an LH category has any non-"notApplicable" audits. + * @param {LH.ReportResult.Category} category + * @return {boolean} + */ + hasApplicableAudits(category) { + return category.auditRefs.some(ref => ref.result.scoreDisplayMode !== 'notApplicable'); + } + + /** + * Define the score arc of the gauge + * Credit to xgad for the original technique: https://codepen.io/xgad/post/svg-radial-progress-meters + * @param {SVGCircleElement} arcElem + * @param {number} percent + */ + _setGaugeArc(arcElem, percent) { + const circumferencePx = 2 * Math.PI * Number(arcElem.getAttribute('r')); + // The rounded linecap of the stroke extends the arc past its start and end. + // First, we tweak the -90deg rotation to start exactly at the top of the circle. + const strokeWidthPx = Number(arcElem.getAttribute('stroke-width')); + const rotationalAdjustmentPercent = 0.25 * strokeWidthPx / circumferencePx; + arcElem.style.transform = `rotate(${-90 + rotationalAdjustmentPercent * 360}deg)`; + + // Then, we terminate the line a little early as well. + let arcLengthPx = percent * circumferencePx - strokeWidthPx / 2; + // Special cases. No dot for 0, and full ring if 100 + if (percent === 0) arcElem.style.opacity = '0'; + if (percent === 1) arcLengthPx = circumferencePx; + + arcElem.style.strokeDasharray = `${Math.max(arcLengthPx, 0)} ${circumferencePx}`; + } + + /** + * @param {LH.ReportResult.AuditRef} audit + * @return {boolean} + */ + _auditHasWarning(audit) { + return Boolean(audit.result.warnings && audit.result.warnings.length); + } + + /** + * Returns the id of the top-level clump to put this audit in. + * @param {LH.ReportResult.AuditRef} auditRef + * @return {TopLevelClumpId} + */ + _getClumpIdForAuditRef(auditRef) { + const scoreDisplayMode = auditRef.result.scoreDisplayMode; + if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') { + return scoreDisplayMode; + } + + if (Util.showAsPassed(auditRef.result)) { + if (this._auditHasWarning(auditRef)) { + return 'warning'; + } else { + return 'passed'; + } + } else { + return 'failed'; + } + } + + /** + * Renders a set of top level sections (clumps), under a status of failed, warning, + * manual, passed, or notApplicable. The result ends up something like: + * + * failed clump + * ├── audit 1 (w/o group) + * ├── audit 2 (w/o group) + * ├── audit group + * | ├── audit 3 + * | └── audit 4 + * └── audit group + * ├── audit 5 + * └── audit 6 + * other clump (e.g. 'manual') + * ├── audit 1 + * ├── audit 2 + * ├── … + * ⋮ + * @param {LH.ReportResult.Category} category + * @param {Object} [groupDefinitions] + * @return {Element} + */ + render(category, groupDefinitions = {}) { + const element = this.dom.createElement('div', 'lh-category'); + this.createPermalinkSpan(element, category.id); + element.appendChild(this.renderCategoryHeader(category, groupDefinitions)); + + // Top level clumps for audits, in order they will appear in the report. + /** @type {Map>} */ + const clumps = new Map(); + clumps.set('failed', []); + clumps.set('warning', []); + clumps.set('manual', []); + clumps.set('passed', []); + clumps.set('notApplicable', []); + + // Sort audits into clumps. + for (const auditRef of category.auditRefs) { + const clumpId = this._getClumpIdForAuditRef(auditRef); + const clump = /** @type {Array} */ (clumps.get(clumpId)); // already defined + clump.push(auditRef); + clumps.set(clumpId, clump); + } + + // Render each clump. + for (const [clumpId, auditRefs] of clumps) { + if (auditRefs.length === 0) continue; + + if (clumpId === 'failed') { + const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions); + clumpElem.classList.add(`lh-clump--failed`); + element.appendChild(clumpElem); + continue; + } + + const description = clumpId === 'manual' ? category.manualDescription : undefined; + const clumpElem = this.renderClump(clumpId, {auditRefs, description}); + element.appendChild(clumpElem); + } + + return element; + } + + /** + * Create a non-semantic span used for hash navigation of categories + * @param {Element} element + * @param {string} id + */ + createPermalinkSpan(element, id) { + const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink'); + permalinkEl.id = id; + } + } + + /** + * @license + * Copyright 2017 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. + */ + + class CriticalRequestChainRenderer { + /** + * Create render context for critical-request-chain tree display. + * @param {LH.Audit.SimpleCriticalRequestNode} tree + * @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}} + */ + static initTree(tree) { + let startTime = 0; + const rootNodes = Object.keys(tree); + if (rootNodes.length > 0) { + const node = tree[rootNodes[0]]; + startTime = node.request.startTime; + } + + return {tree, startTime, transferSize: 0}; + } + + /** + * Helper to create context for each critical-request-chain node based on its + * parent. Calculates if this node is the last child, whether it has any + * children itself and what the tree looks like all the way back up to the root, + * so the tree markers can be drawn correctly. + * @param {LH.Audit.SimpleCriticalRequestNode} parent + * @param {string} id + * @param {number} startTime + * @param {number} transferSize + * @param {Array=} treeMarkers + * @param {boolean=} parentIsLastChild + * @return {CRCSegment} + */ + static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) { + const node = parent[id]; + const siblings = Object.keys(parent); + const isLastChild = siblings.indexOf(id) === (siblings.length - 1); + const hasChildren = !!node.children && Object.keys(node.children).length > 0; + + // Copy the tree markers so that we don't change by reference. + const newTreeMarkers = Array.isArray(treeMarkers) ? treeMarkers.slice(0) : []; + + // Add on the new entry. + if (typeof parentIsLastChild !== 'undefined') { + newTreeMarkers.push(!parentIsLastChild); + } + + return { + node, + isLastChild, + hasChildren, + startTime, + transferSize: transferSize + node.request.transferSize, + treeMarkers: newTreeMarkers, + }; + } + + /** + * Creates the DOM for a tree segment. + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {CRCSegment} segment + * @param {DetailsRenderer} detailsRenderer + * @return {Node} + */ + static createChainNode(dom, tmpl, segment, detailsRenderer) { + const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl); + + // Hovering over request shows full URL. + dom.find('.crc-node', chainsEl).setAttribute('title', segment.node.request.url); + + const treeMarkeEl = dom.find('.crc-node__tree-marker', chainsEl); + + // Construct lines and add spacers for sub requests. + segment.treeMarkers.forEach(separator => { + if (separator) { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert')); + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); + } else { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); + } + }); + + if (segment.isLastChild) { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker up-right')); + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); + } else { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert-right')); + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); + } + + if (segment.hasChildren) { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker horiz-down')); + } else { + treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); + } + + // Fill in url, host, and request size information. + const url = segment.node.request.url; + const linkEl = detailsRenderer.renderTextURL(url); + const treevalEl = dom.find('.crc-node__tree-value', chainsEl); + treevalEl.appendChild(linkEl); + + if (!segment.hasChildren) { + const {startTime, endTime, transferSize} = segment.node.request; + const span = dom.createElement('span', 'crc-node__chain-duration'); + span.textContent = ' - ' + Util.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', '; + const span2 = dom.createElement('span', 'crc-node__chain-duration'); + span2.textContent = Util.i18n.formatBytesToKiB(transferSize, 0.01); + + treevalEl.appendChild(span); + treevalEl.appendChild(span2); + } + + return chainsEl; + } + + /** + * Recursively builds a tree from segments. + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {CRCSegment} segment + * @param {Element} elem Parent element. + * @param {LH.Audit.Details.CriticalRequestChain} details + * @param {DetailsRenderer} detailsRenderer + */ + static buildTree(dom, tmpl, segment, elem, details, detailsRenderer) { + elem.appendChild(CRCRenderer.createChainNode(dom, tmpl, segment, detailsRenderer)); + if (segment.node.children) { + for (const key of Object.keys(segment.node.children)) { + const childSegment = CRCRenderer.createSegment(segment.node.children, key, + segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild); + CRCRenderer.buildTree(dom, tmpl, childSegment, elem, details, detailsRenderer); + } + } + } + + /** + * @param {DOM} dom + * @param {ParentNode} templateContext + * @param {LH.Audit.Details.CriticalRequestChain} details + * @param {DetailsRenderer} detailsRenderer + * @return {Element} + */ + static render(dom, templateContext, details, detailsRenderer) { + const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext); + const containerEl = dom.find('.lh-crc', tmpl); + + // Fill in top summary. + dom.find('.crc-initial-nav', tmpl).textContent = Util.i18n.strings.crcInitialNavigation; + dom.find('.lh-crc__longest_duration_label', tmpl).textContent = + Util.i18n.strings.crcLongestDurationLabel; + dom.find('.lh-crc__longest_duration', tmpl).textContent = + Util.i18n.formatMilliseconds(details.longestChain.duration); + + // Construct visual tree. + const root = CRCRenderer.initTree(details.chains); + for (const key of Object.keys(root.tree)) { + const segment = CRCRenderer.createSegment(root.tree, key, root.startTime, root.transferSize); + CRCRenderer.buildTree(dom, tmpl, segment, containerEl, details, detailsRenderer); + } + + return dom.find('.lh-crc-container', tmpl); + } + } + + // Alias b/c the name is really long. + const CRCRenderer = CriticalRequestChainRenderer; + + /** @typedef {{ + node: LH.Audit.SimpleCriticalRequestNode[string], + isLastChild: boolean, + hasChildren: boolean, + startTime: number, + transferSize: number, + treeMarkers: Array + }} CRCSegment + */ + + /** + * @license Copyright 2019 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. + */ + + /** @enum {number} */ + const LineVisibility = { + /** Show regardless of whether the snippet is collapsed or expanded */ + ALWAYS: 0, + WHEN_COLLAPSED: 1, + WHEN_EXPANDED: 2, + }; + + /** @enum {number} */ + const LineContentType = { + /** A line of content */ + CONTENT_NORMAL: 0, + /** A line of content that's emphasized by setting the CSS background color */ + CONTENT_HIGHLIGHTED: 1, + /** Use when some lines are hidden, shows the "..." placeholder */ + PLACEHOLDER: 2, + /** A message about a line of content or the snippet in general */ + MESSAGE: 3, + }; + + /** @typedef {{ + content: string; + lineNumber: string | number; + contentType: LineContentType; + truncated?: boolean; + visibility?: LineVisibility; + }} LineDetails */ + + const classNamesByContentType = { + [LineContentType.CONTENT_NORMAL]: ['lh-snippet__line--content'], + [LineContentType.CONTENT_HIGHLIGHTED]: [ + 'lh-snippet__line--content', + 'lh-snippet__line--content-highlighted', + ], + [LineContentType.PLACEHOLDER]: ['lh-snippet__line--placeholder'], + [LineContentType.MESSAGE]: ['lh-snippet__line--message'], + }; + + /** + * @param {LH.Audit.Details.SnippetValue['lines']} lines + * @param {number} lineNumber + * @return {{line?: LH.Audit.Details.SnippetValue['lines'][0], previousLine?: LH.Audit.Details.SnippetValue['lines'][0]}} + */ + function getLineAndPreviousLine(lines, lineNumber) { + return { + line: lines.find(l => l.lineNumber === lineNumber), + previousLine: lines.find(l => l.lineNumber === lineNumber - 1), + }; + } + + /** + * @param {LH.Audit.Details.SnippetValue["lineMessages"]} messages + * @param {number} lineNumber + */ + function getMessagesForLineNumber(messages, lineNumber) { + return messages.filter(h => h.lineNumber === lineNumber); + } + + /** + * @param {LH.Audit.Details.SnippetValue} details + * @return {LH.Audit.Details.SnippetValue['lines']} + */ + function getLinesWhenCollapsed(details) { + const SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED = 2; + return Util.filterRelevantLines( + details.lines, + details.lineMessages, + SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED + ); + } + + /** + * Render snippet of text with line numbers and annotations. + * By default we only show a few lines around each annotation and the user + * can click "Expand snippet" to show more. + * Content lines with annotations are highlighted. + */ + class SnippetRenderer { + /** + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {LH.Audit.Details.SnippetValue} details + * @param {DetailsRenderer} detailsRenderer + * @param {function} toggleExpandedFn + * @return {DocumentFragment} + */ + static renderHeader(dom, tmpl, details, detailsRenderer, toggleExpandedFn) { + const linesWhenCollapsed = getLinesWhenCollapsed(details); + const canExpand = linesWhenCollapsed.length < details.lines.length; + + const header = dom.cloneTemplate('#tmpl-lh-snippet__header', tmpl); + dom.find('.lh-snippet__title', header).textContent = details.title; + + const { + snippetCollapseButtonLabel, + snippetExpandButtonLabel, + } = Util.i18n.strings; + dom.find( + '.lh-snippet__btn-label-collapse', + header + ).textContent = snippetCollapseButtonLabel; + dom.find( + '.lh-snippet__btn-label-expand', + header + ).textContent = snippetExpandButtonLabel; + + const toggleExpandButton = dom.find('.lh-snippet__toggle-expand', header); + // If we're already showing all the available lines of the snippet, we don't need an + // expand/collapse button and can remove it from the DOM. + // If we leave the button in though, wire up the click listener to toggle visibility! + if (!canExpand) { + toggleExpandButton.remove(); + } else { + toggleExpandButton.addEventListener('click', () => toggleExpandedFn()); + } + + // We only show the source node of the snippet in DevTools because then the user can + // access the full element detail. Just being able to see the outer HTML isn't very useful. + if (details.node && dom.isDevTools()) { + const nodeContainer = dom.find('.lh-snippet__node', header); + nodeContainer.appendChild(detailsRenderer.renderNode(details.node)); + } + + return header; + } + + /** + * Renders a line (text content, message, or placeholder) as a DOM element. + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {LineDetails} lineDetails + * @return {Element} + */ + static renderSnippetLine( + dom, + tmpl, + {content, lineNumber, truncated, contentType, visibility} + ) { + const clonedTemplate = dom.cloneTemplate('#tmpl-lh-snippet__line', tmpl); + const contentLine = dom.find('.lh-snippet__line', clonedTemplate); + const {classList} = contentLine; + + classNamesByContentType[contentType].forEach(typeClass => + classList.add(typeClass) + ); + + if (visibility === LineVisibility.WHEN_COLLAPSED) { + classList.add('lh-snippet__show-if-collapsed'); + } else if (visibility === LineVisibility.WHEN_EXPANDED) { + classList.add('lh-snippet__show-if-expanded'); + } + + const lineContent = content + (truncated ? '…' : ''); + const lineContentEl = dom.find('.lh-snippet__line code', contentLine); + if (contentType === LineContentType.MESSAGE) { + lineContentEl.appendChild(dom.convertMarkdownLinkSnippets(lineContent)); + } else { + lineContentEl.textContent = lineContent; + } + + dom.find( + '.lh-snippet__line-number', + contentLine + ).textContent = lineNumber.toString(); + + return contentLine; + } + + /** + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {{message: string}} message + * @return {Element} + */ + static renderMessage(dom, tmpl, message) { + return SnippetRenderer.renderSnippetLine(dom, tmpl, { + lineNumber: ' ', + content: message.message, + contentType: LineContentType.MESSAGE, + }); + } + + /** + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {LineVisibility} visibility + * @return {Element} + */ + static renderOmittedLinesPlaceholder(dom, tmpl, visibility) { + return SnippetRenderer.renderSnippetLine(dom, tmpl, { + lineNumber: '…', + content: '', + visibility, + contentType: LineContentType.PLACEHOLDER, + }); + } + + /** + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {LH.Audit.Details.SnippetValue} details + * @return {DocumentFragment} + */ + static renderSnippetContent(dom, tmpl, details) { + const template = dom.cloneTemplate('#tmpl-lh-snippet__content', tmpl); + const snippetEl = dom.find('.lh-snippet__snippet-inner', template); + + // First render messages that don't belong to specific lines + details.generalMessages.forEach(m => + snippetEl.append(SnippetRenderer.renderMessage(dom, tmpl, m)) + ); + // Then render the lines and their messages, as well as placeholders where lines are omitted + snippetEl.append(SnippetRenderer.renderSnippetLines(dom, tmpl, details)); + + return template; + } + + /** + * @param {DOM} dom + * @param {DocumentFragment} tmpl + * @param {LH.Audit.Details.SnippetValue} details + * @return {DocumentFragment} + */ + static renderSnippetLines(dom, tmpl, details) { + const {lineMessages, generalMessages, lineCount, lines} = details; + const linesWhenCollapsed = getLinesWhenCollapsed(details); + const hasOnlyGeneralMessages = + generalMessages.length > 0 && lineMessages.length === 0; + + const lineContainer = dom.createFragment(); + + // When a line is not shown in the collapsed state we try to see if we also need an + // omitted lines placeholder for the expanded state, rather than rendering two separate + // placeholders. + let hasPendingOmittedLinesPlaceholderForCollapsedState = false; + + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { + const {line, previousLine} = getLineAndPreviousLine(lines, lineNumber); + const { + line: lineWhenCollapsed, + previousLine: previousLineWhenCollapsed, + } = getLineAndPreviousLine(linesWhenCollapsed, lineNumber); + + const showLineWhenCollapsed = !!lineWhenCollapsed; + const showPreviousLineWhenCollapsed = !!previousLineWhenCollapsed; + + // If we went from showing lines in the collapsed state to not showing them + // we need to render a placeholder + if (showPreviousLineWhenCollapsed && !showLineWhenCollapsed) { + hasPendingOmittedLinesPlaceholderForCollapsedState = true; + } + // If we are back to lines being visible in the collapsed and the placeholder + // hasn't been rendered yet then render it now + if ( + showLineWhenCollapsed && + hasPendingOmittedLinesPlaceholderForCollapsedState + ) { + lineContainer.append( + SnippetRenderer.renderOmittedLinesPlaceholder( + dom, + tmpl, + LineVisibility.WHEN_COLLAPSED + ) + ); + hasPendingOmittedLinesPlaceholderForCollapsedState = false; + } + + // Render omitted lines placeholder if we have not already rendered one for this gap + const isFirstOmittedLineWhenExpanded = !line && !!previousLine; + const isFirstLineOverallAndIsOmittedWhenExpanded = + !line && lineNumber === 1; + if ( + isFirstOmittedLineWhenExpanded || + isFirstLineOverallAndIsOmittedWhenExpanded + ) { + // In the collapsed state we don't show omitted lines placeholders around + // the edges of the snippet + const hasRenderedAllLinesVisibleWhenCollapsed = !linesWhenCollapsed.some( + l => l.lineNumber > lineNumber + ); + const onlyShowWhenExpanded = + hasRenderedAllLinesVisibleWhenCollapsed || lineNumber === 1; + lineContainer.append( + SnippetRenderer.renderOmittedLinesPlaceholder( + dom, + tmpl, + onlyShowWhenExpanded + ? LineVisibility.WHEN_EXPANDED + : LineVisibility.ALWAYS + ) + ); + hasPendingOmittedLinesPlaceholderForCollapsedState = false; + } + + if (!line) { + // Can't render the line if we don't know its content (instead we've rendered a placeholder) + continue; + } + + // Now render the line and any messages + const messages = getMessagesForLineNumber(lineMessages, lineNumber); + const highlightLine = messages.length > 0 || hasOnlyGeneralMessages; + const contentLineDetails = Object.assign({}, line, { + contentType: highlightLine + ? LineContentType.CONTENT_HIGHLIGHTED + : LineContentType.CONTENT_NORMAL, + visibility: lineWhenCollapsed + ? LineVisibility.ALWAYS + : LineVisibility.WHEN_EXPANDED, + }); + lineContainer.append( + SnippetRenderer.renderSnippetLine(dom, tmpl, contentLineDetails) + ); + + messages.forEach(message => { + lineContainer.append(SnippetRenderer.renderMessage(dom, tmpl, message)); + }); + } + + return lineContainer; + } + + /** + * @param {DOM} dom + * @param {ParentNode} templateContext + * @param {LH.Audit.Details.SnippetValue} details + * @param {DetailsRenderer} detailsRenderer + * @return {!Element} + */ + static render(dom, templateContext, details, detailsRenderer) { + const tmpl = dom.cloneTemplate('#tmpl-lh-snippet', templateContext); + const snippetEl = dom.find('.lh-snippet', tmpl); + + const header = SnippetRenderer.renderHeader( + dom, + tmpl, + details, + detailsRenderer, + () => snippetEl.classList.toggle('lh-snippet--expanded') + ); + const content = SnippetRenderer.renderSnippetContent(dom, tmpl, details); + snippetEl.append(header, content); + + return snippetEl; + } + } + + /** + * @license Copyright 2020 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. + */ + + /** + * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot + * @param {LH.Artifacts.Rect} rect + * @return {boolean} + */ + function screenshotOverlapsRect(screenshot, rect) { + return rect.left <= screenshot.width && + 0 <= rect.right && + rect.top <= screenshot.height && + 0 <= rect.bottom; + } + + /** + * @param {number} value + * @param {number} min + * @param {number} max + */ + function clamp(value, min, max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + /** + * @param {Rect} rect + */ + function getRectCenterPoint(rect) { + return { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }; + } + + class ElementScreenshotRenderer { + /** + * Given the location of an element and the sizes of the preview and screenshot, + * compute the absolute positions (in screenshot coordinate scale) of the screenshot content + * and the highlighted rect around the element. + * @param {Rect} elementRectSC + * @param {Size} elementPreviewSizeSC + * @param {Size} screenshotSize + */ + static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) { + const elementRectCenter = getRectCenterPoint(elementRectSC); + + // Try to center clipped region. + const screenshotLeftVisibleEdge = clamp( + elementRectCenter.x - elementPreviewSizeSC.width / 2, + 0, screenshotSize.width - elementPreviewSizeSC.width + ); + const screenshotTopVisisbleEdge = clamp( + elementRectCenter.y - elementPreviewSizeSC.height / 2, + 0, screenshotSize.height - elementPreviewSizeSC.height + ); + + return { + screenshot: { + left: screenshotLeftVisibleEdge, + top: screenshotTopVisisbleEdge, + }, + clip: { + left: elementRectSC.left - screenshotLeftVisibleEdge, + top: elementRectSC.top - screenshotTopVisisbleEdge, + }, + }; + } + + /** + * Render a clipPath SVG element to assist marking the element's rect. + * The elementRect and previewSize are in screenshot coordinate scale. + * @param {DOM} dom + * @param {HTMLElement} maskEl + * @param {{left: number, top: number}} positionClip + * @param {LH.Artifacts.Rect} elementRect + * @param {Size} elementPreviewSize + */ + static renderClipPathInScreenshot(dom, maskEl, positionClip, elementRect, elementPreviewSize) { + const clipPathEl = dom.find('clipPath', maskEl); + const clipId = `clip-${Util.getUniqueSuffix()}`; + clipPathEl.id = clipId; + maskEl.style.clipPath = `url(#${clipId})`; + + // Normalize values between 0-1. + const top = positionClip.top / elementPreviewSize.height; + const bottom = top + elementRect.height / elementPreviewSize.height; + const left = positionClip.left / elementPreviewSize.width; + const right = left + elementRect.width / elementPreviewSize.width; + + const polygonsPoints = [ + `0,0 1,0 1,${top} 0,${top}`, + `0,${bottom} 1,${bottom} 1,1 0,1`, + `0,${top} ${left},${top} ${left},${bottom} 0,${bottom}`, + `${right},${top} 1,${top} 1,${bottom} ${right},${bottom}`, + ]; + for (const points of polygonsPoints) { + clipPathEl.append(dom.createElementNS( + 'http://www.w3.org/2000/svg', 'polygon', undefined, {points})); + } + } + + /** + * Called by report renderer. Defines a css variable used by any element screenshots + * in the provided report element. + * Allows for multiple Lighthouse reports to be rendered on the page, each with their + * own full page screenshot. + * @param {HTMLElement} el + * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot + */ + static installFullPageScreenshot(el, screenshot) { + el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`); + } + + /** + * Installs the lightbox elements and wires up click listeners to all .lh-element-screenshot elements. + * @param {InstallOverlayFeatureParams} opts + */ + static installOverlayFeature(opts) { + const {dom, reportEl, overlayContainerEl, templateContext, 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); + + // Add a single listener to the provided element to handle all clicks within (event delegation). + reportEl.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. + const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot')); + if (!el) return; + + const overlay = dom.createElement('div', 'lh-element-screenshot__overlay'); + overlayContainerEl.append(overlay); + + // The newly-added overlay has the dimensions we need. + const maxLightboxSize = { + width: overlay.clientWidth * 0.95, + height: overlay.clientHeight * 0.80, + }; + + const elementRectSC = { + width: Number(el.dataset['rectWidth']), + height: Number(el.dataset['rectHeight']), + left: Number(el.dataset['rectLeft']), + right: Number(el.dataset['rectLeft']) + Number(el.dataset['rectWidth']), + top: Number(el.dataset['rectTop']), + bottom: Number(el.dataset['rectTop']) + Number(el.dataset['rectHeight']), + }; + const screenshotElement = ElementScreenshotRenderer.render( + dom, + templateContext, + fullPageScreenshot.screenshot, + elementRectSC, + maxLightboxSize + ); + + // This would be unexpected here. + // When `screenshotElement` is `null`, there is also no thumbnail element for the user to have clicked to make it this far. + if (!screenshotElement) { + overlay.remove(); + return; + } + overlay.appendChild(screenshotElement); + overlay.addEventListener('click', () => overlay.remove()); + }); + } + + /** + * Given the size of the element in the screenshot and the total available size of our preview container, + * compute the factor by which we need to zoom out to view the entire element with context. + * @param {LH.Artifacts.Rect} elementRectSC + * @param {Size} renderContainerSizeDC + * @return {number} + */ + static _computeZoomFactor(elementRectSC, renderContainerSizeDC) { + const targetClipToViewportRatio = 0.75; + const zoomRatioXY = { + x: renderContainerSizeDC.width / elementRectSC.width, + y: renderContainerSizeDC.height / elementRectSC.height, + }; + const zoomFactor = targetClipToViewportRatio * Math.min(zoomRatioXY.x, zoomRatioXY.y); + return Math.min(1, zoomFactor); + } + + /** + * Renders an element with surrounding context from the full page screenshot. + * Used to render both the thumbnail preview in details tables and the full-page screenshot in the lightbox. + * Returns null if element rect is outside screenshot bounds. + * @param {DOM} dom + * @param {ParentNode} templateContext + * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot + * @param {LH.Artifacts.Rect} elementRectSC Region of screenshot to highlight. + * @param {Size} maxRenderSizeDC e.g. maxThumbnailSize or maxLightboxSize. + * @return {Element|null} + */ + static render(dom, templateContext, screenshot, elementRectSC, maxRenderSizeDC) { + if (!screenshotOverlapsRect(screenshot, elementRectSC)) { + return null; + } + + const tmpl = dom.cloneTemplate('#tmpl-lh-element-screenshot', templateContext); + const containerEl = dom.find('div.lh-element-screenshot', tmpl); + + containerEl.dataset['rectWidth'] = elementRectSC.width.toString(); + containerEl.dataset['rectHeight'] = elementRectSC.height.toString(); + containerEl.dataset['rectLeft'] = elementRectSC.left.toString(); + containerEl.dataset['rectTop'] = elementRectSC.top.toString(); + + // Zoom out when highlighted region takes up most of the viewport. + // This provides more context for where on the page this element is. + const zoomFactor = this._computeZoomFactor(elementRectSC, maxRenderSizeDC); + + const elementPreviewSizeSC = { + width: maxRenderSizeDC.width / zoomFactor, + height: maxRenderSizeDC.height / zoomFactor, + }; + elementPreviewSizeSC.width = Math.min(screenshot.width, elementPreviewSizeSC.width); + /* This preview size is either the size of the thumbnail or size of the Lightbox */ + const elementPreviewSizeDC = { + width: elementPreviewSizeSC.width * zoomFactor, + height: elementPreviewSizeSC.height * zoomFactor, + }; + + const positions = ElementScreenshotRenderer.getScreenshotPositions( + elementRectSC, + elementPreviewSizeSC, + {width: screenshot.width, height: screenshot.height} + ); + + const contentEl = dom.find('div.lh-element-screenshot__content', containerEl); + contentEl.style.top = `-${elementPreviewSizeDC.height}px`; + + const imageEl = dom.find('div.lh-element-screenshot__image', containerEl); + imageEl.style.width = elementPreviewSizeDC.width + 'px'; + imageEl.style.height = elementPreviewSizeDC.height + 'px'; + + imageEl.style.backgroundPositionY = -(positions.screenshot.top * zoomFactor) + 'px'; + imageEl.style.backgroundPositionX = -(positions.screenshot.left * zoomFactor) + 'px'; + imageEl.style.backgroundSize = + `${screenshot.width * zoomFactor}px ${screenshot.height * zoomFactor}px`; + + const markerEl = dom.find('div.lh-element-screenshot__element-marker', containerEl); + markerEl.style.width = elementRectSC.width * zoomFactor + 'px'; + markerEl.style.height = elementRectSC.height * zoomFactor + 'px'; + markerEl.style.left = positions.clip.left * zoomFactor + 'px'; + markerEl.style.top = positions.clip.top * zoomFactor + 'px'; + + const maskEl = dom.find('div.lh-element-screenshot__mask', containerEl); + maskEl.style.width = elementPreviewSizeDC.width + 'px'; + maskEl.style.height = elementPreviewSizeDC.height + 'px'; + + ElementScreenshotRenderer.renderClipPathInScreenshot( + dom, + maskEl, + positions.clip, + elementRectSC, + elementPreviewSizeSC + ); + + return containerEl; + } + } + + /** + * @license + * Copyright 2017 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. + */ + + const URL_PREFIXES = ['http://', 'https://', 'data:']; + + class DetailsRenderer { + /** + * @param {DOM} dom + * @param {{fullPageScreenshot?: LH.Artifacts.FullPageScreenshot}} [options] + */ + constructor(dom, options = {}) { + this._dom = dom; + this._fullPageScreenshot = options.fullPageScreenshot; + + /** @type {ParentNode} */ + this._templateContext; // eslint-disable-line no-unused-expressions + } + + /** + * @param {ParentNode} context + */ + setTemplateContext(context) { + this._templateContext = context; + } + + /** + * @param {AuditDetails} details + * @return {Element|null} + */ + render(details) { + switch (details.type) { + case 'filmstrip': + return this._renderFilmstrip(details); + case 'list': + return this._renderList(details); + case 'table': + return this._renderTable(details); + case 'criticalrequestchain': + return CriticalRequestChainRenderer.render(this._dom, this._templateContext, details, this); + case 'opportunity': + return this._renderTable(details); + + // Internal-only details, not for rendering. + case 'screenshot': + case 'debugdata': + case 'full-page-screenshot': + case 'treemap-data': + return null; + + default: { + // @ts-expect-error tsc thinks this is unreachable, but be forward compatible + // with new unexpected detail types. + return this._renderUnknown(details.type, details); + } + } + } + + /** + * @param {{value: number, granularity?: number}} details + * @return {Element} + */ + _renderBytes(details) { + // TODO: handle displayUnit once we have something other than 'kb' + // Note that 'kb' is historical and actually represents KiB. + const value = Util.i18n.formatBytesToKiB(details.value, details.granularity); + const textEl = this._renderText(value); + textEl.title = Util.i18n.formatBytes(details.value); + return textEl; + } + + /** + * @param {{value: number, granularity?: number, displayUnit?: string}} details + * @return {Element} + */ + _renderMilliseconds(details) { + let value = Util.i18n.formatMilliseconds(details.value, details.granularity); + if (details.displayUnit === 'duration') { + value = Util.i18n.formatDuration(details.value); + } + + return this._renderText(value); + } + + /** + * @param {string} text + * @return {HTMLElement} + */ + renderTextURL(text) { + const url = text; + + let displayedPath; + let displayedHost; + let title; + try { + const parsed = Util.parseURL(url); + displayedPath = parsed.file === '/' ? parsed.origin : parsed.file; + displayedHost = parsed.file === '/' || parsed.hostname === '' ? '' : `(${parsed.hostname})`; + title = url; + } catch (e) { + displayedPath = url; + } + + const element = this._dom.createElement('div', 'lh-text__url'); + element.appendChild(this._renderLink({text: displayedPath, url})); + + if (displayedHost) { + const hostElem = this._renderText(displayedHost); + hostElem.classList.add('lh-text__url-host'); + element.appendChild(hostElem); + } + + if (title) { + element.title = url; + // set the url on the element's dataset which we use to check 3rd party origins + element.dataset.url = url; + } + return element; + } + + /** + * @param {{text: string, url: string}} details + * @return {HTMLElement} + */ + _renderLink(details) { + const allowedProtocols = ['https:', 'http:']; + let url; + try { + url = new URL(details.url); + } catch (_) {} + + if (!url || !allowedProtocols.includes(url.protocol)) { + // Fall back to just the link text if invalid or protocol not allowed. + const element = this._renderText(details.text); + element.classList.add('lh-link'); + return element; + } + + const a = this._dom.createElement('a'); + a.rel = 'noopener'; + a.target = '_blank'; + a.textContent = details.text; + a.href = url.href; + a.classList.add('lh-link'); + return a; + } + + /** + * @param {string} text + * @return {HTMLDivElement} + */ + _renderText(text) { + const element = this._dom.createElement('div', 'lh-text'); + element.textContent = text; + return element; + } + + /** + * @param {{value: number, granularity?: number}} details + * @return {Element} + */ + _renderNumeric(details) { + const value = Util.i18n.formatNumber(details.value, details.granularity); + const element = this._dom.createElement('div', 'lh-numeric'); + element.textContent = value; + return element; + } + + /** + * Create small thumbnail with scaled down image asset. + * @param {string} details + * @return {Element} + */ + _renderThumbnail(details) { + const element = this._dom.createElement('img', 'lh-thumbnail'); + const strValue = details; + element.src = strValue; + element.title = strValue; + element.alt = ''; + return element; + } + + /** + * @param {string} type + * @param {*} value + */ + _renderUnknown(type, value) { + // eslint-disable-next-line no-console + console.error(`Unknown details type: ${type}`, value); + const element = this._dom.createElement('details', 'lh-unknown'); + this._dom.createChildOf(element, 'summary').textContent = + `We don't know how to render audit details of type \`${type}\`. ` + + 'The Lighthouse version that collected this data is likely newer than the Lighthouse ' + + 'version of the report renderer. Expand for the raw JSON.'; + this._dom.createChildOf(element, 'pre').textContent = JSON.stringify(value, null, 2); + return element; + } + + /** + * Render a details item value for embedding in a table. Renders the value + * based on the heading's valueType, unless the value itself has a `type` + * property to override it. + * @param {TableItemValue} value + * @param {LH.Audit.Details.OpportunityColumnHeading} heading + * @return {Element|null} + */ + _renderTableValue(value, heading) { + if (value === undefined || value === null) { + return null; + } + + // First deal with the possible object forms of value. + if (typeof value === 'object') { + // The value's type overrides the heading's for this column. + switch (value.type) { + case 'code': { + return this._renderCode(value.value); + } + case 'link': { + return this._renderLink(value); + } + case 'node': { + return this.renderNode(value); + } + case 'numeric': { + return this._renderNumeric(value); + } + case 'source-location': { + return this.renderSourceLocation(value); + } + case 'url': { + return this.renderTextURL(value.value); + } + default: { + return this._renderUnknown(value.type, value); + } + } + } + + // Next, deal with primitives. + switch (heading.valueType) { + case 'bytes': { + const numValue = Number(value); + return this._renderBytes({value: numValue, granularity: heading.granularity}); + } + case 'code': { + const strValue = String(value); + return this._renderCode(strValue); + } + case 'ms': { + const msValue = { + value: Number(value), + granularity: heading.granularity, + displayUnit: heading.displayUnit, + }; + return this._renderMilliseconds(msValue); + } + case 'numeric': { + const numValue = Number(value); + return this._renderNumeric({value: numValue, granularity: heading.granularity}); + } + case 'text': { + const strValue = String(value); + return this._renderText(strValue); + } + case 'thumbnail': { + const strValue = String(value); + return this._renderThumbnail(strValue); + } + case 'timespanMs': { + const numValue = Number(value); + return this._renderMilliseconds({value: numValue}); + } + case 'url': { + const strValue = String(value); + if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) { + return this.renderTextURL(strValue); + } else { + // Fall back to
 rendering if not actually a URL.
+            return this._renderCode(strValue);
+          }
+        }
+        default: {
+          return this._renderUnknown(heading.valueType, value);
+        }
+      }
+    }
+
+    /**
+     * Get the headings of a table-like details object, converted into the
+     * OpportunityColumnHeading type until we have all details use the same
+     * heading format.
+     * @param {Table|OpportunityTable} tableLike
+     * @return {OpportunityTable['headings']}
+     */
+    _getCanonicalizedHeadingsFromTable(tableLike) {
+      if (tableLike.type === 'opportunity') {
+        return tableLike.headings;
+      }
+
+      return tableLike.headings.map(heading => this._getCanonicalizedHeading(heading));
+    }
+
+    /**
+     * Get the headings of a table-like details object, converted into the
+     * OpportunityColumnHeading type until we have all details use the same
+     * heading format.
+     * @param {Table['headings'][number]} heading
+     * @return {OpportunityTable['headings'][number]}
+     */
+    _getCanonicalizedHeading(heading) {
+      let subItemsHeading;
+      if (heading.subItemsHeading) {
+        subItemsHeading = this._getCanonicalizedsubItemsHeading(heading.subItemsHeading, heading);
+      }
+
+      return {
+        key: heading.key,
+        valueType: heading.itemType,
+        subItemsHeading,
+        label: heading.text,
+        displayUnit: heading.displayUnit,
+        granularity: heading.granularity,
+      };
+    }
+
+    /**
+     * @param {Exclude} subItemsHeading
+     * @param {LH.Audit.Details.TableColumnHeading} parentHeading
+     * @return {LH.Audit.Details.OpportunityColumnHeading['subItemsHeading']}
+     */
+    _getCanonicalizedsubItemsHeading(subItemsHeading, parentHeading) {
+      // Low-friction way to prevent commiting a falsy key (which is never allowed for
+      // a subItemsHeading) from passing in CI.
+      if (!subItemsHeading.key) {
+        // eslint-disable-next-line no-console
+        console.warn('key should not be null');
+      }
+
+      return {
+        key: subItemsHeading.key || '',
+        valueType: subItemsHeading.itemType || parentHeading.itemType,
+        granularity: subItemsHeading.granularity || parentHeading.granularity,
+        displayUnit: subItemsHeading.displayUnit || parentHeading.displayUnit,
+      };
+    }
+
+    /**
+     * Returns a new heading where the values are defined first by `heading.subItemsHeading`,
+     * and secondly by `heading`. If there is no subItemsHeading, returns null, which will
+     * be rendered as an empty column.
+     * @param {LH.Audit.Details.OpportunityColumnHeading} heading
+     * @return {LH.Audit.Details.OpportunityColumnHeading | null}
+     */
+    _getDerivedsubItemsHeading(heading) {
+      if (!heading.subItemsHeading) return null;
+      return {
+        key: heading.subItemsHeading.key || '',
+        valueType: heading.subItemsHeading.valueType || heading.valueType,
+        granularity: heading.subItemsHeading.granularity || heading.granularity,
+        displayUnit: heading.subItemsHeading.displayUnit || heading.displayUnit,
+        label: '',
+      };
+    }
+
+    /**
+     * @param {TableItem} item
+     * @param {(LH.Audit.Details.OpportunityColumnHeading | null)[]} headings
+     */
+    _renderTableRow(item, headings) {
+      const rowElem = this._dom.createElement('tr');
+
+      for (const heading of headings) {
+        // Empty cell if no heading or heading key for this column.
+        if (!heading || !heading.key) {
+          this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
+          continue;
+        }
+
+        const value = item[heading.key];
+        let valueElement;
+        if (value !== undefined && value !== null) {
+          valueElement = this._renderTableValue(value, heading);
+        }
+
+        if (valueElement) {
+          const classes = `lh-table-column--${heading.valueType}`;
+          this._dom.createChildOf(rowElem, 'td', classes).appendChild(valueElement);
+        } else {
+          // Empty cell is rendered for a column if:
+          // - the pair is null
+          // - the heading key is null
+          // - the value is undefined/null
+          this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
+        }
+      }
+
+      return rowElem;
+    }
+
+    /**
+     * Renders one or more rows from a details table item. A single table item can
+     * expand into multiple rows, if there is a subItemsHeading.
+     * @param {TableItem} item
+     * @param {LH.Audit.Details.OpportunityColumnHeading[]} headings
+     */
+    _renderTableRowsFromItem(item, headings) {
+      const fragment = this._dom.createFragment();
+      fragment.append(this._renderTableRow(item, headings));
+
+      if (!item.subItems) return fragment;
+
+      const subItemsHeadings = headings.map(this._getDerivedsubItemsHeading);
+      if (!subItemsHeadings.some(Boolean)) return fragment;
+
+      for (const subItem of item.subItems.items) {
+        const rowEl = this._renderTableRow(subItem, subItemsHeadings);
+        rowEl.classList.add('lh-sub-item-row');
+        fragment.append(rowEl);
+      }
+
+      return fragment;
+    }
+
+    /**
+     * @param {OpportunityTable|Table} details
+     * @return {Element}
+     */
+    _renderTable(details) {
+      if (!details.items.length) return this._dom.createElement('span');
+
+      const tableElem = this._dom.createElement('table', 'lh-table');
+      const theadElem = this._dom.createChildOf(tableElem, 'thead');
+      const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
+
+      const headings = this._getCanonicalizedHeadingsFromTable(details);
+
+      for (const heading of headings) {
+        const valueType = heading.valueType || 'text';
+        const classes = `lh-table-column--${valueType}`;
+        const labelEl = this._dom.createElement('div', 'lh-text');
+        labelEl.textContent = heading.label;
+        this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl);
+      }
+
+      const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
+      let even = true;
+      for (const item of details.items) {
+        const rowsFragment = this._renderTableRowsFromItem(item, headings);
+        for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
+          // For zebra styling.
+          rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
+        }
+        even = !even;
+        tbodyElem.append(rowsFragment);
+      }
+
+      return tableElem;
+    }
+
+    /**
+     * @param {LH.Audit.Details.List} details
+     * @return {Element}
+     */
+    _renderList(details) {
+      const listContainer = this._dom.createElement('div', 'lh-list');
+
+      details.items.forEach(item => {
+        const snippetEl = SnippetRenderer.render(this._dom, this._templateContext, item, this);
+        listContainer.appendChild(snippetEl);
+      });
+
+      return listContainer;
+    }
+
+    /**
+     * @param {LH.Audit.Details.NodeValue} item
+     * @return {Element}
+     */
+    renderNode(item) {
+      const element = this._dom.createElement('span', 'lh-node');
+      if (item.nodeLabel) {
+        const nodeLabelEl = this._dom.createElement('div');
+        nodeLabelEl.textContent = item.nodeLabel;
+        element.appendChild(nodeLabelEl);
+      }
+      if (item.snippet) {
+        const snippetEl = this._dom.createElement('div');
+        snippetEl.classList.add('lh-node__snippet');
+        snippetEl.textContent = item.snippet;
+        element.appendChild(snippetEl);
+      }
+      if (item.selector) {
+        element.title = item.selector;
+      }
+      if (item.path) element.setAttribute('data-path', item.path);
+      if (item.selector) element.setAttribute('data-selector', item.selector);
+      if (item.snippet) element.setAttribute('data-snippet', item.snippet);
+
+      if (!this._fullPageScreenshot) return element;
+
+      const rect = item.lhId && this._fullPageScreenshot.nodes[item.lhId];
+      if (!rect || rect.width === 0 || rect.height === 0) return element;
+
+      const maxThumbnailSize = {width: 147, height: 100};
+      const elementScreenshot = ElementScreenshotRenderer.render(
+        this._dom,
+        this._templateContext,
+        this._fullPageScreenshot.screenshot,
+        rect,
+        maxThumbnailSize
+      );
+      if (elementScreenshot) element.prepend(elementScreenshot);
+
+      return element;
+    }
+
+    /**
+     * @param {LH.Audit.Details.SourceLocationValue} item
+     * @return {Element|null}
+     * @protected
+     */
+    renderSourceLocation(item) {
+      if (!item.url) {
+        return null;
+      }
+
+      // Lines are shown as one-indexed.
+      const generatedLocation = `${item.url}:${item.line + 1}:${item.column}`;
+      let sourceMappedOriginalLocation;
+      if (item.original) {
+        const file = item.original.file || '';
+        sourceMappedOriginalLocation = `${file}:${item.original.line + 1}:${item.original.column}`;
+      }
+
+      // We render slightly differently based on presence of source map and provenance of URL.
+      let element;
+      if (item.urlProvider === 'network' && sourceMappedOriginalLocation) {
+        element = this._renderLink({
+          url: item.url,
+          text: sourceMappedOriginalLocation,
+        });
+        element.title = `maps to generated location ${generatedLocation}`;
+      } else if (item.urlProvider === 'network' && !sourceMappedOriginalLocation) {
+        element = this.renderTextURL(item.url);
+        this._dom.find('.lh-link', element).textContent += `:${item.line + 1}:${item.column}`;
+      } else if (item.urlProvider === 'comment' && sourceMappedOriginalLocation) {
+        element = this._renderText(`${sourceMappedOriginalLocation} (from source map)`);
+        element.title = `${generatedLocation} (from sourceURL)`;
+      } else if (item.urlProvider === 'comment' && !sourceMappedOriginalLocation) {
+        element = this._renderText(`${generatedLocation} (from sourceURL)`);
+      } else {
+        return null;
+      }
+
+      element.classList.add('lh-source-location');
+      element.setAttribute('data-source-url', item.url);
+      // DevTools expects zero-indexed lines.
+      element.setAttribute('data-source-line', String(item.line));
+      element.setAttribute('data-source-column', String(item.column));
+
+      return element;
+    }
+
+    /**
+     * @param {LH.Audit.Details.Filmstrip} details
+     * @return {Element}
+     */
+    _renderFilmstrip(details) {
+      const filmstripEl = this._dom.createElement('div', 'lh-filmstrip');
+
+      for (const thumbnail of details.items) {
+        const frameEl = this._dom.createChildOf(filmstripEl, 'div', 'lh-filmstrip__frame');
+        this._dom.createChildOf(frameEl, 'img', 'lh-filmstrip__thumbnail', {
+          src: thumbnail.data,
+          alt: `Screenshot`,
+        });
+      }
+      return filmstripEl;
+    }
+
+    /**
+     * @param {string} text
+     * @return {Element}
+     */
+    _renderCode(text) {
+      const pre = this._dom.createElement('pre', 'lh-code');
+      pre.textContent = text;
+      return pre;
+    }
+  }
+
+  /**
+   * @license Copyright 2020 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.
+   */
+
+  // Not named `NBSP` because that creates a duplicate identifier (util.js).
+  const NBSP2 = '\xa0';
+  const KiB = 1024;
+  const MiB = KiB * KiB;
+
+  /**
+   * @template T
+   */
+  class I18n {
+    /**
+     * @param {LH.Locale} locale
+     * @param {T} strings
+     */
+    constructor(locale, strings) {
+      // When testing, use a locale with more exciting numeric formatting.
+      if (locale === 'en-XA') locale = 'de';
+
+      this._numberDateLocale = locale;
+      this._numberFormatter = new Intl.NumberFormat(locale);
+      this._percentFormatter = new Intl.NumberFormat(locale, {style: 'percent'});
+      this._strings = strings;
+    }
+
+    get strings() {
+      return this._strings;
+    }
+
+    /**
+     * Format number.
+     * @param {number} number
+     * @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
+     * @return {string}
+     */
+    formatNumber(number, granularity = 0.1) {
+      const coarseValue = Math.round(number / granularity) * granularity;
+      return this._numberFormatter.format(coarseValue);
+    }
+
+    /**
+     * Format percent.
+     * @param {number} number 0–1
+     * @return {string}
+     */
+    formatPercent(number) {
+      return this._percentFormatter.format(number);
+    }
+
+    /**
+     * @param {number} size
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+     * @return {string}
+     */
+    formatBytesToKiB(size, granularity = 0.1) {
+      const formatter = this._byteFormatterForGranularity(granularity);
+      const kbs = formatter.format(Math.round(size / 1024 / granularity) * granularity);
+      return `${kbs}${NBSP2}KiB`;
+    }
+
+    /**
+     * @param {number} size
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+     * @return {string}
+     */
+    formatBytesToMiB(size, granularity = 0.1) {
+      const formatter = this._byteFormatterForGranularity(granularity);
+      const kbs = formatter.format(Math.round(size / (1024 ** 2) / granularity) * granularity);
+      return `${kbs}${NBSP2}MiB`;
+    }
+
+    /**
+     * @param {number} size
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
+     * @return {string}
+     */
+    formatBytes(size, granularity = 1) {
+      const formatter = this._byteFormatterForGranularity(granularity);
+      const kbs = formatter.format(Math.round(size / granularity) * granularity);
+      return `${kbs}${NBSP2}bytes`;
+    }
+
+    /**
+     * @param {number} size
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+     * @return {string}
+     */
+    formatBytesWithBestUnit(size, granularity = 0.1) {
+      if (size >= MiB) return this.formatBytesToMiB(size, granularity);
+      if (size >= KiB) return this.formatBytesToKiB(size, granularity);
+      return this.formatNumber(size, granularity) + '\xa0B';
+    }
+
+    /**
+     * Format bytes with a constant number of fractional digits, i.e for a granularity of 0.1, 10 becomes '10.0'
+     * @param {number} granularity Controls how coarse the displayed value is
+     * @return {Intl.NumberFormat}
+     */
+    _byteFormatterForGranularity(granularity) {
+      // assume any granularity above 1 will not contain fractional parts, i.e. will never be 1.5
+      let numberOfFractionDigits = 0;
+      if (granularity < 1) {
+        numberOfFractionDigits = -Math.floor(Math.log10(granularity));
+      }
+
+      return new Intl.NumberFormat(this._numberDateLocale, {
+        ...this._numberFormatter.resolvedOptions(),
+        maximumFractionDigits: numberOfFractionDigits,
+        minimumFractionDigits: numberOfFractionDigits,
+      });
+    }
+
+    /**
+     * @param {number} ms
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
+     * @return {string}
+     */
+    formatMilliseconds(ms, granularity = 10) {
+      const coarseTime = Math.round(ms / granularity) * granularity;
+      return coarseTime === 0
+        ? `${this._numberFormatter.format(0)}${NBSP2}ms`
+        : `${this._numberFormatter.format(coarseTime)}${NBSP2}ms`;
+    }
+
+    /**
+     * @param {number} ms
+     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+     * @return {string}
+     */
+    formatSeconds(ms, granularity = 0.1) {
+      const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
+      return `${this._numberFormatter.format(coarseTime)}${NBSP2}s`;
+    }
+
+    /**
+     * Format time.
+     * @param {string} date
+     * @return {string}
+     */
+    formatDateTime(date) {
+      /** @type {Intl.DateTimeFormatOptions} */
+      const options = {
+        month: 'short', day: 'numeric', year: 'numeric',
+        hour: 'numeric', minute: 'numeric', timeZoneName: 'short',
+      };
+
+      // Force UTC if runtime timezone could not be detected.
+      // See https://github.com/GoogleChrome/lighthouse/issues/1056
+      // and https://github.com/GoogleChrome/lighthouse/pull/9822
+      let formatter;
+      try {
+        formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
+      } catch (err) {
+        options.timeZone = 'UTC';
+        formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
+      }
+
+      return formatter.format(new Date(date));
+    }
+
+    /**
+     * Converts a time in milliseconds into a duration string, i.e. `1d 2h 13m 52s`
+     * @param {number} timeInMilliseconds
+     * @return {string}
+     */
+    formatDuration(timeInMilliseconds) {
+      let timeInSeconds = timeInMilliseconds / 1000;
+      if (Math.round(timeInSeconds) === 0) {
+        return 'None';
+      }
+
+      /** @type {Array} */
+      const parts = [];
+      /** @type {Record} */
+      const unitLabels = {
+        d: 60 * 60 * 24,
+        h: 60 * 60,
+        m: 60,
+        s: 1,
+      };
+
+      Object.keys(unitLabels).forEach(label => {
+        const unit = unitLabels[label];
+        const numberOfUnits = Math.floor(timeInSeconds / unit);
+        if (numberOfUnits > 0) {
+          timeInSeconds -= numberOfUnits * unit;
+          parts.push(`${numberOfUnits}\xa0${label}`);
+        }
+      });
+
+      return parts.join(' ');
+    }
+  }
+
+  /**
+   * @license
+   * Copyright 2018 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.
+   */
+
+  class PerformanceCategoryRenderer extends CategoryRenderer {
+    /**
+     * @param {LH.ReportResult.AuditRef} audit
+     * @return {!Element}
+     */
+    _renderMetric(audit) {
+      const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
+      const element = this.dom.find('.lh-metric', tmpl);
+      element.id = audit.result.id;
+      const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
+      element.classList.add(`lh-metric--${rating}`);
+
+      const titleEl = this.dom.find('.lh-metric__title', tmpl);
+      titleEl.textContent = audit.result.title;
+
+      const valueEl = this.dom.find('.lh-metric__value', tmpl);
+      valueEl.textContent = audit.result.displayValue || '';
+
+      const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
+      descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
+
+      if (audit.result.scoreDisplayMode === 'error') {
+        descriptionEl.textContent = '';
+        valueEl.textContent = 'Error!';
+        const tooltip = this.dom.createChildOf(descriptionEl, 'span');
+        tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
+      }
+
+      return element;
+    }
+
+    /**
+     * @param {LH.ReportResult.AuditRef} audit
+     * @param {number} scale
+     * @return {!Element}
+     */
+    _renderOpportunity(audit, scale) {
+      const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
+      const element = this.populateAuditValues(audit, oppTmpl);
+      element.id = audit.result.id;
+
+      if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
+        return element;
+      }
+      const details = audit.result.details;
+      if (details.type !== 'opportunity') {
+        return element;
+      }
+
+      // Overwrite the displayValue with opportunity's wastedMs
+      // TODO: normalize this to one tagName.
+      const displayEl =
+        this.dom.find('span.lh-audit__display-text, div.lh-audit__display-text', element);
+      const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
+      this.dom.find('div.lh-sparkline__bar', element).style.width = sparklineWidthPct;
+      displayEl.textContent = Util.i18n.formatSeconds(details.overallSavingsMs, 0.01);
+
+      // Set [title] tooltips
+      if (audit.result.displayValue) {
+        const displayValue = audit.result.displayValue;
+        this.dom.find('div.lh-load-opportunity__sparkline', element).title = displayValue;
+        displayEl.title = displayValue;
+      }
+
+      return element;
+    }
+
+    /**
+     * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
+     * Opportunities with an error won't have a details object, so MIN_VALUE is returned to keep any
+     * erroring opportunities last in sort order.
+     * @param {LH.ReportResult.AuditRef} audit
+     * @return {number}
+     */
+    _getWastedMs(audit) {
+      if (audit.result.details && audit.result.details.type === 'opportunity') {
+        const details = audit.result.details;
+        if (typeof details.overallSavingsMs !== 'number') {
+          throw new Error('non-opportunity details passed to _getWastedMs');
+        }
+        return details.overallSavingsMs;
+      } else {
+        return Number.MIN_VALUE;
+      }
+    }
+
+    /**
+     * Get a link to the interactive scoring calculator with the metric values.
+     * @param {LH.ReportResult.AuditRef[]} auditRefs
+     * @return {string}
+     */
+    _getScoringCalculatorHref(auditRefs) {
+      // TODO: filter by !!acronym when dropping renderer support of v7 LHRs.
+      const metrics = auditRefs.filter(audit => audit.group === 'metrics');
+      const fci = auditRefs.find(audit => audit.id === 'first-cpu-idle');
+      const fmp = auditRefs.find(audit => audit.id === 'first-meaningful-paint');
+      if (fci) metrics.push(fci);
+      if (fmp) metrics.push(fmp);
+
+      /**
+       * Clamp figure to 2 decimal places
+       * @param {number} val
+       * @return {number}
+       */
+      const clampTo2Decimals = val => Math.round(val * 100) / 100;
+
+      const metricPairs = metrics.map(audit => {
+        let value;
+        if (typeof audit.result.numericValue === 'number') {
+          value = audit.id === 'cumulative-layout-shift' ?
+            clampTo2Decimals(audit.result.numericValue) :
+            Math.round(audit.result.numericValue);
+          value = value.toString();
+        } else {
+          value = 'null';
+        }
+        return [audit.acronym || audit.id, value];
+      });
+      const paramPairs = [...metricPairs];
+
+      if (Util.reportJson) {
+        paramPairs.push(['device', Util.reportJson.configSettings.formFactor]);
+        paramPairs.push(['version', Util.reportJson.lighthouseVersion]);
+      }
+
+      const params = new URLSearchParams(paramPairs);
+      const url = new URL('https://googlechrome.github.io/lighthouse/scorecalc/');
+      url.hash = params.toString();
+      return url.href;
+    }
+
+    /**
+     * @param {LH.ReportResult.Category} category
+     * @param {Object} groups
+     * @param {'PSI'=} environment 'PSI' and undefined are the only valid values
+     * @return {Element}
+     * @override
+     */
+    render(category, groups, environment) {
+      const strings = Util.i18n.strings;
+      const element = this.dom.createElement('div', 'lh-category');
+      if (environment === 'PSI') {
+        const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
+        gaugeEl.appendChild(this.renderScoreGauge(category, groups));
+        element.appendChild(gaugeEl);
+      } else {
+        this.createPermalinkSpan(element, category.id);
+        element.appendChild(this.renderCategoryHeader(category, groups));
+      }
+
+      // Metrics.
+      const metricAuditsEl = this.renderAuditGroup(groups.metrics);
+
+      // Metric descriptions toggle.
+      const toggleTmpl = this.dom.cloneTemplate('#tmpl-lh-metrics-toggle', this.templateContext);
+      const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
+      metricAuditsEl.append(..._toggleEl.childNodes);
+
+      const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
+      const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
+
+      metricAudits.forEach(item => {
+        metricsBoxesEl.appendChild(this._renderMetric(item));
+      });
+
+      const estValuesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics__disclaimer');
+      const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
+      estValuesEl.appendChild(disclaimerEl);
+
+      // Add link to score calculator.
+      const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
+      calculatorLink.target = '_blank';
+      calculatorLink.textContent = strings.calculatorLink;
+      calculatorLink.href = this._getScoringCalculatorHref(category.auditRefs);
+
+
+      metricAuditsEl.classList.add('lh-audit-group--metrics');
+      element.appendChild(metricAuditsEl);
+
+      // Filmstrip
+      const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
+      const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
+      const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
+      if (thumbnailResult && thumbnailResult.details) {
+        timelineEl.id = thumbnailResult.id;
+        const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
+        filmstripEl && timelineEl.appendChild(filmstripEl);
+      }
+
+      // Opportunities
+      const opportunityAudits = category.auditRefs
+          .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
+          .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
+
+
+      const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
+      // TODO: only add if there are opportunities & diagnostics rendered.
+      if (filterableMetrics.length) {
+        this.renderMetricAuditFilter(filterableMetrics, element);
+      }
+
+      if (opportunityAudits.length) {
+        // Scale the sparklines relative to savings, minimum 2s to not overstate small savings
+        const minimumScale = 2000;
+        const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
+        const maxWaste = Math.max(...wastedMsValues);
+        const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
+        const groupEl = this.renderAuditGroup(groups['load-opportunities']);
+        const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
+
+        this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
+          strings.opportunityResourceColumnLabel;
+        this.dom.find('.lh-load-opportunity__col--two', tmpl).textContent =
+          strings.opportunitySavingsColumnLabel;
+
+        const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
+        groupEl.appendChild(headerEl);
+        opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
+        groupEl.classList.add('lh-audit-group--load-opportunities');
+        element.appendChild(groupEl);
+      }
+
+      // Diagnostics
+      const diagnosticAudits = category.auditRefs
+          .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
+          .sort((a, b) => {
+            const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
+            const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
+            return scoreA - scoreB;
+          });
+
+      if (diagnosticAudits.length) {
+        const groupEl = this.renderAuditGroup(groups['diagnostics']);
+        diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
+        groupEl.classList.add('lh-audit-group--diagnostics');
+        element.appendChild(groupEl);
+      }
+
+      // Passed audits
+      const passedAudits = category.auditRefs
+          .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
+              Util.showAsPassed(audit.result));
+
+      if (!passedAudits.length) return element;
+
+      const clumpOpts = {
+        auditRefs: passedAudits,
+        groupDefinitions: groups,
+      };
+      const passedElem = this.renderClump('passed', clumpOpts);
+      element.appendChild(passedElem);
+
+      // Budgets
+      /** @type {Array} */
+      const budgetTableEls = [];
+      ['performance-budget', 'timing-budget'].forEach((id) => {
+        const audit = category.auditRefs.find(audit => audit.id === id);
+        if (audit && audit.result.details) {
+          const table = this.detailsRenderer.render(audit.result.details);
+          if (table) {
+            table.id = id;
+            table.classList.add('lh-audit');
+            budgetTableEls.push(table);
+          }
+        }
+      });
+      if (budgetTableEls.length > 0) {
+        const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
+        budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
+        budgetsGroupEl.classList.add('lh-audit-group--budgets');
+        element.appendChild(budgetsGroupEl);
+      }
+
+      return element;
+    }
+
+    /**
+     * Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
+     * @param {LH.ReportResult.AuditRef[]} filterableMetrics
+     * @param {HTMLDivElement} categoryEl
+     */
+    renderMetricAuditFilter(filterableMetrics, categoryEl) {
+      const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
+      const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
+      textEl.textContent = Util.i18n.strings.showRelevantAudits;
+
+      const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
+        ({acronym: 'All'}),
+        ...filterableMetrics,
+      ]);
+      for (const metric of filterChoices) {
+        const elemId = `metric-${metric.acronym}`;
+        const radioEl = this.dom.createChildOf(metricFilterEl, 'input', 'lh-metricfilter__radio', {
+          type: 'radio',
+          name: 'metricsfilter',
+          id: elemId,
+        });
+
+        const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', {
+          for: elemId,
+          title: metric.result && metric.result.title,
+        });
+        labelEl.textContent = metric.acronym || metric.id;
+
+        if (metric.acronym === 'All') {
+          radioEl.checked = true;
+          labelEl.classList.add('lh-metricfilter__label--active');
+        }
+        categoryEl.append(metricFilterEl);
+
+        // Toggle class/hidden state based on filter choice.
+        radioEl.addEventListener('input', _ => {
+          for (const elem of categoryEl.querySelectorAll('label.lh-metricfilter__label')) {
+            elem.classList.toggle('lh-metricfilter__label--active', elem.htmlFor === elemId);
+          }
+          categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
+
+          for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
+            if (metric.acronym === 'All') {
+              perfAuditEl.hidden = false;
+              continue;
+            }
+
+            perfAuditEl.hidden = true;
+            if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
+              perfAuditEl.hidden = false;
+            }
+          }
+
+          // Hide groups/clumps if all child audits are also hidden.
+          const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
+          for (const groupEl of groupEls) {
+            groupEl.hidden = false;
+            const childEls = Array.from(groupEl.querySelectorAll('div.lh-audit'));
+            const areAllHidden = !!childEls.length && childEls.every(auditEl => auditEl.hidden);
+            groupEl.hidden = areAllHidden;
+          }
+        });
+      }
+    }
+  }
+
+  /**
+   * @license
+   * Copyright 2018 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.
+   */
+
+  class PwaCategoryRenderer extends CategoryRenderer {
+    /**
+     * @param {LH.ReportResult.Category} category
+     * @param {Object} [groupDefinitions]
+     * @return {Element}
+     */
+    render(category, groupDefinitions = {}) {
+      const categoryElem = this.dom.createElement('div', 'lh-category');
+      this.createPermalinkSpan(categoryElem, category.id);
+      categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions));
+
+      const auditRefs = category.auditRefs;
+
+      // Regular audits aren't split up into pass/fail/notApplicable clumps, they're
+      // all put in a top-level clump that isn't expandable/collapsible.
+      const regularAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode !== 'manual');
+      const auditsElem = this._renderAudits(regularAuditRefs, groupDefinitions);
+      categoryElem.appendChild(auditsElem);
+
+      // Manual audits are still in a manual clump.
+      const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
+      const manualElem = this.renderClump('manual',
+        {auditRefs: manualAuditRefs, description: category.manualDescription});
+      categoryElem.appendChild(manualElem);
+
+      return categoryElem;
+    }
+
+    /**
+     * @param {LH.ReportResult.Category} category
+     * @param {Record} groupDefinitions
+     * @return {DocumentFragment}
+     */
+    renderScoreGauge(category, groupDefinitions) {
+      // Defer to parent-gauge style if category error.
+      if (category.score === null) {
+        return super.renderScoreGauge(category, groupDefinitions);
+      }
+
+      const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge--pwa', this.templateContext);
+      const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
+      wrapper.href = `#${category.id}`;
+
+      // Correct IDs in case multiple instances end up in the page.
+      const svgRoot = tmpl.querySelector('svg');
+      if (!svgRoot) throw new Error('no SVG element found in PWA score gauge template');
+      PwaCategoryRenderer._makeSvgReferencesUnique(svgRoot);
+
+      const allGroups = this._getGroupIds(category.auditRefs);
+      const passingGroupIds = this._getPassingGroupIds(category.auditRefs);
+
+      if (passingGroupIds.size === allGroups.size) {
+        wrapper.classList.add('lh-badged--all');
+      } else {
+        for (const passingGroupId of passingGroupIds) {
+          wrapper.classList.add(`lh-badged--${passingGroupId}`);
+        }
+      }
+
+      this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
+      wrapper.title = this._getGaugeTooltip(category.auditRefs, groupDefinitions);
+      return tmpl;
+    }
+
+    /**
+     * Returns the group IDs found in auditRefs.
+     * @param {Array} auditRefs
+     * @return {!Set}
+     */
+    _getGroupIds(auditRefs) {
+      const groupIds = auditRefs.map(ref => ref.group).filter(/** @return {g is string} */ g => !!g);
+      return new Set(groupIds);
+    }
+
+    /**
+     * Returns the group IDs whose audits are all considered passing.
+     * @param {Array} auditRefs
+     * @return {Set}
+     */
+    _getPassingGroupIds(auditRefs) {
+      const uniqueGroupIds = this._getGroupIds(auditRefs);
+
+      // Remove any that have a failing audit.
+      for (const auditRef of auditRefs) {
+        if (!Util.showAsPassed(auditRef.result) && auditRef.group) {
+          uniqueGroupIds.delete(auditRef.group);
+        }
+      }
+
+      return uniqueGroupIds;
+    }
+
+    /**
+     * Returns a tooltip string summarizing group pass rates.
+     * @param {Array} auditRefs
+     * @param {Record} groupDefinitions
+     * @return {string}
+     */
+    _getGaugeTooltip(auditRefs, groupDefinitions) {
+      const groupIds = this._getGroupIds(auditRefs);
+
+      const tips = [];
+      for (const groupId of groupIds) {
+        const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId);
+        const auditCount = groupAuditRefs.length;
+        const passedCount = groupAuditRefs.filter(ref => Util.showAsPassed(ref.result)).length;
+
+        const title = groupDefinitions[groupId].title;
+        tips.push(`${title}: ${passedCount}/${auditCount}`);
+      }
+
+      return tips.join(', ');
+    }
+
+    /**
+     * Render non-manual audits in groups, giving a badge to any group that has
+     * all passing audits.
+     * @param {Array} auditRefs
+     * @param {Object} groupDefinitions
+     * @return {Element}
+     */
+    _renderAudits(auditRefs, groupDefinitions) {
+      const auditsElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
+
+      // Add a 'badged' class to group if all audits in that group pass.
+      const passsingGroupIds = this._getPassingGroupIds(auditRefs);
+      for (const groupId of passsingGroupIds) {
+        const groupElem = this.dom.find(`.lh-audit-group--${groupId}`, auditsElem);
+        groupElem.classList.add('lh-badged');
+      }
+
+      return auditsElem;
+    }
+
+    /**
+     * Alters SVG id references so multiple instances of an SVG element can coexist
+     * in a single page. If `svgRoot` has a `` block, gives all elements defined
+     * in it unique ids, then updates id references (``,
+     * `fill="url(#...)"`) to the altered ids in all descendents of `svgRoot`.
+     * @param {SVGElement} svgRoot
+     */
+    static _makeSvgReferencesUnique(svgRoot) {
+      const defsEl = svgRoot.querySelector('defs');
+      if (!defsEl) return;
+
+      const idSuffix = Util.getUniqueSuffix();
+      const elementsToUpdate = defsEl.querySelectorAll('[id]');
+      for (const el of elementsToUpdate) {
+        const oldId = el.id;
+        const newId = `${oldId}-${idSuffix}`;
+        el.id = newId;
+
+        // Update all s.
+        const useEls = svgRoot.querySelectorAll(`use[href="#${oldId}"]`);
+        for (const useEl of useEls) {
+          useEl.setAttribute('href', `#${newId}`);
+        }
+
+        // Update all fill="url(#...)"s.
+        const fillEls = svgRoot.querySelectorAll(`[fill="url(#${oldId})"]`);
+        for (const fillEl of fillEls) {
+          fillEl.setAttribute('fill', `url(#${newId})`);
+        }
+      }
+    }
+  }
+
+  /**
+   * @license
+   * Copyright 2017 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.
+   */
+
+  class ReportRenderer {
+    /**
+     * @param {DOM} dom
+     */
+    constructor(dom) {
+      /** @type {DOM} */
+      this._dom = dom;
+      /** @type {ParentNode} */
+      this._templateContext = this._dom.document();
+    }
+
+    /**
+     * @param {LH.Result} result
+     * @param {Element} container Parent element to render the report into.
+     * @return {!Element}
+     */
+    renderReport(result, container) {
+      this._dom.setLighthouseChannel(result.configSettings.channel || 'unknown');
+
+      const report = Util.prepareReportResult(result);
+
+      container.textContent = ''; // Remove previous report.
+      container.appendChild(this._renderReport(report));
+
+      return container;
+    }
+
+    /**
+     * Define a custom element for  to be extracted from. For example:
+     *     this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
+     * @param {ParentNode} context
+     */
+    setTemplateContext(context) {
+      this._templateContext = context;
+    }
+
+    /**
+     * @param {LH.ReportResult} report
+     * @return {DocumentFragment}
+     */
+    _renderReportTopbar(report) {
+      const el = this._dom.cloneTemplate('#tmpl-lh-topbar', this._templateContext);
+      const metadataUrl = this._dom.find('a.lh-topbar__url', el);
+      metadataUrl.href = metadataUrl.textContent = report.finalUrl;
+      metadataUrl.title = report.finalUrl;
+      return el;
+    }
+
+    /**
+     * @return {DocumentFragment}
+     */
+    _renderReportHeader() {
+      const el = this._dom.cloneTemplate('#tmpl-lh-heading', this._templateContext);
+      const domFragment = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext);
+      const placeholder = this._dom.find('.lh-scores-wrapper-placeholder', el);
+      placeholder.replaceWith(domFragment);
+      return el;
+    }
+
+    /**
+     * @param {LH.ReportResult} report
+     * @return {DocumentFragment}
+     */
+    _renderReportFooter(report) {
+      const footer = this._dom.cloneTemplate('#tmpl-lh-footer', this._templateContext);
+
+      const env = this._dom.find('.lh-env__items', footer);
+      env.id = 'runtime-settings';
+      this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
+
+      const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
+      const runtimeValues = [
+        {name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
+        {name: Util.i18n.strings.runtimeSettingsFetchTime,
+          description: Util.i18n.formatDateTime(report.fetchTime)},
+        ...envValues,
+        {name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
+        {name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
+        {name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
+          report.environment.networkUserAgent},
+        {name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
+          report.environment.benchmarkIndex.toFixed(0)},
+      ];
+      if (report.environment.credits && report.environment.credits['axe-core']) {
+        runtimeValues.push({
+          name: Util.i18n.strings.runtimeSettingsAxeVersion,
+          description: report.environment.credits['axe-core'],
+        });
+      }
+
+      for (const runtime of runtimeValues) {
+        if (!runtime.description) continue;
+
+        const item = this._dom.cloneTemplate('#tmpl-lh-env__items', env);
+        this._dom.find('.lh-env__name', item).textContent = runtime.name;
+        this._dom.find('.lh-env__description', item).textContent = runtime.description;
+        env.appendChild(item);
+      }
+
+      this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
+      this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
+      return footer;
+    }
+
+    /**
+     * Returns a div with a list of top-level warnings, or an empty div if no warnings.
+     * @param {LH.ReportResult} report
+     * @return {Node}
+     */
+    _renderReportWarnings(report) {
+      if (!report.runWarnings || report.runWarnings.length === 0) {
+        return this._dom.createElement('div');
+      }
+
+      const container = this._dom.cloneTemplate('#tmpl-lh-warnings--toplevel', this._templateContext);
+      const message = this._dom.find('.lh-warnings__msg', container);
+      message.textContent = Util.i18n.strings.toplevelWarningsMessage;
+
+      const warnings = this._dom.find('ul', container);
+      for (const warningString of report.runWarnings) {
+        const warning = warnings.appendChild(this._dom.createElement('li'));
+        warning.appendChild(this._dom.convertMarkdownLinkSnippets(warningString));
+      }
+
+      return container;
+    }
+
+    /**
+     * @param {LH.ReportResult} report
+     * @param {CategoryRenderer} categoryRenderer
+     * @param {Record} specificCategoryRenderers
+     * @return {!DocumentFragment[]}
+     */
+    _renderScoreGauges(report, categoryRenderer, specificCategoryRenderers) {
+      // Group gauges in this order: default, pwa, plugins.
+      const defaultGauges = [];
+      const customGauges = []; // PWA.
+      const pluginGauges = [];
+
+      for (const category of Object.values(report.categories)) {
+        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
+        const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
+
+        if (Util.isPluginCategory(category.id)) {
+          pluginGauges.push(categoryGauge);
+        } else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
+          // The renderer for default categories is just the default CategoryRenderer.
+          // If the functions are equal, then renderer is an instance of CategoryRenderer.
+          // For example, the PWA category uses PwaCategoryRenderer, which overrides
+          // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
+          // in the customGauges bucket.
+          defaultGauges.push(categoryGauge);
+        } else {
+          customGauges.push(categoryGauge);
+        }
+      }
+
+      return [...defaultGauges, ...customGauges, ...pluginGauges];
+    }
+
+    /**
+     * @param {LH.ReportResult} report
+     * @return {!DocumentFragment}
+     */
+    _renderReport(report) {
+      const i18n = new I18n(report.configSettings.locale, {
+        // Set missing renderer strings to default (english) values.
+        ...Util.UIStrings,
+        ...report.i18n.rendererFormattedStrings,
+      });
+      Util.i18n = i18n;
+      Util.reportJson = report;
+
+      const fullPageScreenshot =
+        report.audits['full-page-screenshot'] && report.audits['full-page-screenshot'].details &&
+        report.audits['full-page-screenshot'].details.type === 'full-page-screenshot' ?
+        report.audits['full-page-screenshot'].details : undefined;
+      const detailsRenderer = new DetailsRenderer(this._dom, {
+        fullPageScreenshot,
+      });
+
+      const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
+      categoryRenderer.setTemplateContext(this._templateContext);
+
+      /** @type {Record} */
+      const specificCategoryRenderers = {
+        performance: new PerformanceCategoryRenderer(this._dom, detailsRenderer),
+        pwa: new PwaCategoryRenderer(this._dom, detailsRenderer),
+      };
+      Object.values(specificCategoryRenderers).forEach(renderer => {
+        renderer.setTemplateContext(this._templateContext);
+      });
+
+      const headerContainer = this._dom.createElement('div');
+      headerContainer.appendChild(this._renderReportHeader());
+
+      const reportContainer = this._dom.createElement('div', 'lh-container');
+      const reportSection = this._dom.createElement('div', 'lh-report');
+      reportSection.appendChild(this._renderReportWarnings(report));
+
+      let scoreHeader;
+      const isSoloCategory = Object.keys(report.categories).length === 1;
+      if (!isSoloCategory) {
+        scoreHeader = this._dom.createElement('div', 'lh-scores-header');
+      } else {
+        headerContainer.classList.add('lh-header--solo-category');
+      }
+
+      if (scoreHeader) {
+        const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
+        const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
+        scoreHeader.append(
+          ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
+        scoresContainer.appendChild(scoreHeader);
+        scoresContainer.appendChild(scoreScale);
+
+        const stickyHeader = this._dom.createElement('div', 'lh-sticky-header');
+        stickyHeader.append(
+          ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
+        reportContainer.appendChild(stickyHeader);
+      }
+
+      const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories'));
+      for (const category of Object.values(report.categories)) {
+        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
+        // .lh-category-wrapper is full-width and provides horizontal rules between categories.
+        // .lh-category within has the max-width: var(--report-width);
+        const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
+        wrapper.appendChild(renderer.render(category, report.categoryGroups));
+      }
+
+      const reportFragment = this._dom.createFragment();
+      const topbarDocumentFragment = this._renderReportTopbar(report);
+
+      reportFragment.appendChild(topbarDocumentFragment);
+      reportFragment.appendChild(reportContainer);
+      reportContainer.appendChild(headerContainer);
+      reportContainer.appendChild(reportSection);
+      reportSection.appendChild(this._renderReportFooter(report));
+
+      if (fullPageScreenshot) {
+        ElementScreenshotRenderer.installFullPageScreenshot(
+          reportContainer, fullPageScreenshot.screenshot);
+      }
+
+      return reportFragment;
+    }
+  }
+
+  /**
+   * @license Copyright 2017 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.
+   */
+
+  /**
+   * @fileoverview
+   * @suppress {reportUnknownTypes}
+   */
+
+  /**
+   * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
+   * Date/time uses the local timezone, however Node has unreliable ICU
+   * support, so we must construct a YYYY-MM-DD date format manually. :/
+   * @param {{finalUrl: string, fetchTime: string}} lhr
+   * @return {string}
+   */
+  function getFilenamePrefix(lhr) {
+    const hostname = new URL(lhr.finalUrl).hostname;
+    const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
+
+    const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
+    const dateParts = date.toLocaleDateString('en-US', {
+      year: 'numeric', month: '2-digit', day: '2-digit',
+    }).split('/');
+    // @ts-expect-error - parts exists
+    dateParts.unshift(dateParts.pop());
+    const dateStr = dateParts.join('-');
+
+    const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
+    // replace characters that are unfriendly to filenames
+    return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
+  }
+
+  var fileNamer = {getFilenamePrefix};
+  var fileNamer_1 = fileNamer.getFilenamePrefix;
+
+  /**
+   * @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.
+   */
+
+  /* global btoa atob window CompressionStream Response */
+
+  const btoa_ = typeof btoa !== 'undefined' ?
+    btoa :
+    /** @param {string} str */
+    (str) => Buffer.from(str).toString('base64');
+  const atob_ = typeof atob !== 'undefined' ?
+    atob :
+    /** @param {string} str */
+    (str) => Buffer.from(str, 'base64').toString();
+
+  /**
+   * Takes an UTF-8 string and returns a base64 encoded string.
+   * If gzip is true, the UTF-8 bytes are gzipped before base64'd, using
+   * CompressionStream (currently only in Chrome), falling back to pako
+   * (which is only used to encode in our Node tests).
+   * @param {string} string
+   * @param {{gzip: boolean}} options
+   * @return {Promise}
+   */
+  async function toBase64(string, options) {
+    let bytes = new TextEncoder().encode(string);
+
+    if (options.gzip) {
+      if (typeof CompressionStream !== 'undefined') {
+        const cs = new CompressionStream('gzip');
+        const writer = cs.writable.getWriter();
+        writer.write(bytes);
+        writer.close();
+        const compAb = await new Response(cs.readable).arrayBuffer();
+        bytes = new Uint8Array(compAb);
+      } else {
+        /** @type {import('pako')=} */
+        const pako = window.pako;
+        bytes = pako.gzip(string);
+      }
+    }
+
+    let binaryString = '';
+    // This is ~25% faster than building the string one character at a time.
+    // https://jsbench.me/2gkoxazvjl
+    const chunkSize = 5000;
+    for (let i = 0; i < bytes.length; i += chunkSize) {
+      binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
+    }
+    return btoa_(binaryString);
+  }
+
+  /**
+   * @param {string} encoded
+   * @param {{gzip: boolean}} options
+   * @return {string}
+   */
+  function fromBase64(encoded, options) {
+    const binaryString = atob_(encoded);
+    const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
+
+    if (options.gzip) {
+      /** @type {import('pako')=} */
+      const pako = window.pako;
+      return pako.ungzip(bytes, {to: 'string'});
+    } else {
+      return new TextDecoder().decode(bytes);
+    }
+  }
+
+  const TextEncoding = {toBase64, fromBase64};
+
+  /**
+   * @license
+   * Copyright 2017 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.
+   */
+
+  /**
+   * @param {HTMLTableElement} tableEl
+   * @return {Array}
+   */
+  function getTableRows(tableEl) {
+    return Array.from(tableEl.tBodies[0].rows);
+  }
+
+  function getAppsOrigin() {
+    const isVercel = window.location.host.endsWith('.vercel.app');
+    const isDev = new URLSearchParams(window.location.search).has('dev');
+
+    if (isVercel) return `https://${window.location.host}/gh-pages`;
+    if (isDev) return 'http://localhost:8000';
+    return 'https://googlechrome.github.io/lighthouse';
+  }
+
+  class ReportUIFeatures {
+    /**
+     * @param {DOM} dom
+     */
+    constructor(dom) {
+      /** @type {LH.Result} */
+      this.json; // eslint-disable-line no-unused-expressions
+      /** @type {DOM} */
+      this._dom = dom;
+      /** @type {Document} */
+      this._document = this._dom.document();
+      /** @type {ParentNode} */
+      this._templateContext = this._dom.document();
+      /** @type {DropDown} */
+      this._dropDown = new DropDown(this._dom);
+      /** @type {boolean} */
+      this._copyAttempt = false;
+      /** @type {HTMLElement} */
+      this.topbarEl; // eslint-disable-line no-unused-expressions
+      /** @type {HTMLElement} */
+      this.scoreScaleEl; // eslint-disable-line no-unused-expressions
+      /** @type {HTMLElement} */
+      this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
+      /** @type {HTMLElement} */
+      this.highlightEl; // eslint-disable-line no-unused-expressions
+
+      this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
+      this.onCopy = this.onCopy.bind(this);
+      this.onDropDownMenuClick = this.onDropDownMenuClick.bind(this);
+      this.onKeyUp = this.onKeyUp.bind(this);
+      this.collapseAllDetails = this.collapseAllDetails.bind(this);
+      this.expandAllDetails = this.expandAllDetails.bind(this);
+      this._toggleDarkTheme = this._toggleDarkTheme.bind(this);
+      this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
+    }
+
+    /**
+     * Adds tools button, print, and other functionality to the report. The method
+     * should be called whenever the report needs to be re-rendered.
+     * @param {LH.Result} report
+     */
+    initFeatures(report) {
+      this.json = report;
+
+      this._setupMediaQueryListeners();
+      this._dropDown.setup(this.onDropDownMenuClick);
+      this._setupThirdPartyFilter();
+      this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document));
+      this._setUpCollapseDetailsAfterPrinting();
+      this._resetUIState();
+      this._document.addEventListener('keyup', this.onKeyUp);
+      this._document.addEventListener('copy', this.onCopy);
+
+      const topbarLogo = this._dom.find('.lh-topbar__logo', this._document);
+      topbarLogo.addEventListener('click', () => this._toggleDarkTheme());
+
+      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) {
+        turnOffTheLights = true;
+      }
+
+      // Fireworks!
+      // To get fireworks you need 100 scores in all core categories, except PWA (because going the PWA route is discretionary).
+      const fireworksRequiredCategoryIds = ['performance', 'accessibility', 'best-practices', 'seo'];
+      const scoresAll100 = fireworksRequiredCategoryIds.every(id => {
+        const cat = report.categories[id];
+        return cat && cat.score === 1;
+      });
+      if (scoresAll100) {
+        turnOffTheLights = true;
+        this._enableFireworks();
+      }
+
+      if (turnOffTheLights) {
+        this._toggleDarkTheme(true);
+      }
+
+      // There is only a sticky header when at least 2 categories are present.
+      if (Object.keys(this.json.categories).length >= 2) {
+        this._setupStickyHeaderElements();
+        const containerEl = this._dom.find('.lh-container', this._document);
+        const elToAddScrollListener = this._getScrollParent(containerEl);
+        elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
+
+        // Use ResizeObserver where available.
+        // TODO: there is an issue with incorrect position numbers and, as a result, performance
+        // issues due to layout thrashing.
+        // See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
+        // For now, limit to DevTools.
+        if (this._dom.isDevTools()) {
+          const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
+          resizeObserver.observe(containerEl);
+        } else {
+          window.addEventListener('resize', this._updateStickyHeaderOnScroll);
+        }
+      }
+
+      // Show the metric descriptions by default when there is an error.
+      const hasMetricError = report.categories.performance && report.categories.performance.auditRefs
+        .some(audit => Boolean(audit.group === 'metrics' && report.audits[audit.id].errorMessage));
+      if (hasMetricError) {
+        const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document);
+        toggleInputEl.checked = true;
+      }
+
+      const showTreemapApp =
+        this.json.audits['script-treemap-data'] && this.json.audits['script-treemap-data'].details;
+      if (showTreemapApp) {
+        this.addButton({
+          text: Util.i18n.strings.viewTreemapLabel,
+          icon: 'treemap',
+          onClick: () => ReportUIFeatures.openTreemap(this.json),
+        });
+      }
+
+      // Fill in all i18n data.
+      for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
+        // 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 i18nAttr = /** @type {keyof LH.I18NRendererStrings} */ (node.getAttribute('data-i18n'));
+        node.textContent = Util.i18n.strings[i18nAttr];
+      }
+    }
+
+    /**
+     * Define a custom element for  to be extracted from. For example:
+     *     this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
+     * @param {ParentNode} context
+     */
+    setTemplateContext(context) {
+      this._templateContext = context;
+    }
+
+    /**
+     * @param {{container?: Element, 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;
+
+      let buttonsEl = containerEl.querySelector('.lh-buttons');
+      if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons');
+
+      const classes = [
+        'lh-button',
+      ];
+      if (opts.icon) {
+        classes.push('report-icon');
+        classes.push(`report-icon--${opts.icon}`);
+      }
+      const buttonEl = this._dom.createChildOf(buttonsEl, 'button', classes.join(' '));
+      buttonEl.textContent = opts.text;
+      buttonEl.addEventListener('click', opts.onClick);
+      return buttonEl;
+    }
+
+    /**
+     * Finds the first scrollable ancestor of `element`. Falls back to the document.
+     * @param {Element} element
+     * @return {Node}
+     */
+    _getScrollParent(element) {
+      const {overflowY} = window.getComputedStyle(element);
+      const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';
+
+      if (isScrollable) {
+        return element;
+      }
+
+      if (element.parentElement) {
+        return this._getScrollParent(element.parentElement);
+      }
+
+      return document;
+    }
+
+    _enableFireworks() {
+      const scoresContainer = this._dom.find('.lh-scores-container', this._document);
+      scoresContainer.classList.add('score100');
+      scoresContainer.addEventListener('click', _ => {
+        scoresContainer.classList.toggle('fireworks-paused');
+      });
+    }
+
+    /**
+     * Fires a custom DOM event on target.
+     * @param {string} name Name of the event.
+     * @param {Node=} target DOM node to fire the event on.
+     * @param {*=} detail Custom data to include.
+     */
+    _fireEventOn(name, target = this._document, detail) {
+      const event = new CustomEvent(name, detail ? {detail} : undefined);
+      target.dispatchEvent(event);
+    }
+
+    _setupMediaQueryListeners() {
+      const mediaQuery = self.matchMedia('(max-width: 500px)');
+      mediaQuery.addListener(this.onMediaQueryChange);
+      // Ensure the handler is called on init
+      this.onMediaQueryChange(mediaQuery);
+    }
+
+    /**
+     * Handle media query change events.
+     * @param {MediaQueryList|MediaQueryListEvent} mql
+     */
+    onMediaQueryChange(mql) {
+      const root = this._dom.find('.lh-root', this._document);
+      root.classList.toggle('lh-narrow', mql.matches);
+    }
+
+    _setupThirdPartyFilter() {
+      // Some audits should not display the third party filter option.
+      const thirdPartyFilterAuditExclusions = [
+        // These audits deal explicitly with third party resources.
+        'uses-rel-preconnect',
+        'third-party-facades',
+      ];
+      // Some audits should hide third party by default.
+      const thirdPartyFilterAuditHideByDefault = [
+        // Only first party resources are actionable.
+        'legacy-javascript',
+      ];
+
+      // Get all tables with a text url column.
+      const tables = Array.from(this._document.querySelectorAll('table.lh-table'));
+      const tablesWithUrls = tables
+        .filter(el =>
+          el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
+        .filter(el => {
+          const containingAudit = el.closest('.lh-audit');
+          if (!containingAudit) throw new Error('.lh-table not within audit');
+          return !thirdPartyFilterAuditExclusions.includes(containingAudit.id);
+        });
+
+      tablesWithUrls.forEach((tableEl, index) => {
+        const rowEls = getTableRows(tableEl);
+        const thirdPartyRows = this._getThirdPartyRows(rowEls, this.json.finalUrl);
+
+        // create input box
+        const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._templateContext);
+        const filterInput = this._dom.find('input', filterTemplate);
+        const id = `lh-3p-filter-label--${index}`;
+
+        filterInput.id = id;
+        filterInput.addEventListener('change', e => {
+          const shouldHideThirdParty = e.target instanceof HTMLInputElement && !e.target.checked;
+          let even = true;
+          let rowEl = rowEls[0];
+          while (rowEl) {
+            const shouldHide = shouldHideThirdParty && thirdPartyRows.includes(rowEl);
+
+            // Iterate subsequent associated sub item rows.
+            do {
+              rowEl.classList.toggle('lh-row--hidden', shouldHide);
+              // Adjust for zebra styling.
+              rowEl.classList.toggle('lh-row--even', !shouldHide && even);
+              rowEl.classList.toggle('lh-row--odd', !shouldHide && !even);
+
+              rowEl = /** @type {HTMLElement} */ (rowEl.nextElementSibling);
+            } while (rowEl && rowEl.classList.contains('lh-sub-item-row'));
+
+            if (!shouldHide) even = !even;
+          }
+        });
+
+        this._dom.find('label', filterTemplate).setAttribute('for', id);
+        this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
+            `${thirdPartyRows.length}`;
+        this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
+            Util.i18n.strings.thirdPartyResourcesLabel;
+
+        const allThirdParty = thirdPartyRows.length === rowEls.length;
+        const allFirstParty = !thirdPartyRows.length;
+
+        // If all or none of the rows are 3rd party, disable the checkbox.
+        if (allThirdParty || allFirstParty) {
+          filterInput.disabled = true;
+          filterInput.checked = allThirdParty;
+        }
+
+        // Add checkbox to the DOM.
+        if (!tableEl.parentNode) return; // Keep tsc happy.
+        tableEl.parentNode.insertBefore(filterTemplate, tableEl);
+
+        // Hide third-party rows for some audits by default.
+        const containingAudit = tableEl.closest('.lh-audit');
+        if (!containingAudit) throw new Error('.lh-table not within audit');
+        if (thirdPartyFilterAuditHideByDefault.includes(containingAudit.id) && !allThirdParty) {
+          filterInput.click();
+        }
+      });
+    }
+
+    /**
+     * @param {Element} el
+     */
+    _setupElementScreenshotOverlay(el) {
+      const fullPageScreenshot =
+        this.json.audits['full-page-screenshot'] &&
+        this.json.audits['full-page-screenshot'].details &&
+        this.json.audits['full-page-screenshot'].details.type === 'full-page-screenshot' &&
+        this.json.audits['full-page-screenshot'].details;
+      if (!fullPageScreenshot) return;
+
+      ElementScreenshotRenderer.installOverlayFeature({
+        dom: this._dom,
+        reportEl: el,
+        overlayContainerEl: el,
+        templateContext: this._templateContext,
+        fullPageScreenshot,
+      });
+    }
+
+    /**
+     * From a table with URL entries, finds the rows containing third-party URLs
+     * and returns them.
+     * @param {HTMLElement[]} rowEls
+     * @param {string} finalUrl
+     * @return {Array}
+     */
+    _getThirdPartyRows(rowEls, finalUrl) {
+      /** @type {Array} */
+      const thirdPartyRows = [];
+      const finalUrlRootDomain = Util.getRootDomain(finalUrl);
+
+      for (const rowEl of rowEls) {
+        if (rowEl.classList.contains('lh-sub-item-row')) continue;
+
+        const urlItem = rowEl.querySelector('div.lh-text__url');
+        if (!urlItem) continue;
+
+        const datasetUrl = urlItem.dataset.url;
+        if (!datasetUrl) continue;
+        const isThirdParty = Util.getRootDomain(datasetUrl) !== finalUrlRootDomain;
+        if (!isThirdParty) continue;
+
+        thirdPartyRows.push(rowEl);
+      }
+
+      return thirdPartyRows;
+    }
+
+    _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);
+
+      // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
+      this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
+    }
+
+    /**
+     * Handle copy events.
+     * @param {ClipboardEvent} e
+     */
+    onCopy(e) {
+      // Only handle copy button presses (e.g. ignore the user copying page text).
+      if (this._copyAttempt && e.clipboardData) {
+        // We want to write our own data to the clipboard, not the user's text selection.
+        e.preventDefault();
+        e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
+
+        this._fireEventOn('lh-log', this._document, {
+          cmd: 'log', msg: 'Report JSON copied to clipboard',
+        });
+      }
+
+      this._copyAttempt = false;
+    }
+
+    /**
+     * Copies the report JSON to the clipboard (if supported by the browser).
+     */
+    onCopyButtonClick() {
+      this._fireEventOn('lh-analytics', this._document, {
+        cmd: 'send',
+        fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
+      });
+
+      try {
+        if (this._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._document.execCommand('copy')) {
+            this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
+
+            this._fireEventOn('lh-log', this._document, {
+              cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
+            });
+          }
+        }
+      } catch (/** @type {Error} */ e) {
+        this._copyAttempt = false;
+        this._fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
+      }
+    }
+
+    /**
+     * Resets the state of page before capturing the page for export.
+     * When the user opens the exported HTML page, certain UI elements should
+     * be in their closed state (not opened) and the templates should be unstamped.
+     */
+    _resetUIState() {
+      this._dropDown.close();
+      this._dom.resetTemplates();
+    }
+
+    /**
+     * Handler for tool button.
+     * @param {Event} e
+     */
+    onDropDownMenuClick(e) {
+      e.preventDefault();
+
+      const el = /** @type {?Element} */ (e.target);
+
+      if (!el || !el.hasAttribute('data-action')) {
+        return;
+      }
+
+      switch (el.getAttribute('data-action')) {
+        case 'copy':
+          this.onCopyButtonClick();
+          break;
+        case 'print-summary':
+          this.collapseAllDetails();
+          this._print();
+          break;
+        case 'print-expanded':
+          this.expandAllDetails();
+          this._print();
+          break;
+        case 'save-json': {
+          const jsonStr = JSON.stringify(this.json, null, 2);
+          this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
+          break;
+        }
+        case 'save-html': {
+          const htmlStr = this.getReportHtml();
+          try {
+            this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
+          } catch (/** @type {Error} */ e) {
+            this._fireEventOn('lh-log', this._document, {
+              cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
+            });
+          }
+          break;
+        }
+        case 'open-viewer': {
+          ReportUIFeatures.openTabAndSendJsonReportToViewer(this.json);
+          break;
+        }
+        case 'save-gist': {
+          this.saveAsGist();
+          break;
+        }
+        case 'toggle-dark': {
+          this._toggleDarkTheme();
+          break;
+        }
+      }
+
+      this._dropDown.close();
+    }
+
+    _print() {
+      self.print();
+    }
+
+    /**
+     * Keyup handler for the document.
+     * @param {KeyboardEvent} e
+     */
+    onKeyUp(e) {
+      // Ctrl+P - Expands audit details when user prints via keyboard shortcut.
+      if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) {
+        this._dropDown.close();
+      }
+    }
+
+    /**
+     * The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly.
+     * @param {LH.Result} json
+     * @protected
+     */
+    static computeWindowNameSuffix(json) {
+      // @ts-ignore - If this is a v2 LHR, use old `generatedTime`.
+      const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
+      const fetchTime = json.fetchTime || fallbackFetchTime;
+      return `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
+    }
+
+    /**
+     * Opens a new tab to the online viewer and sends the local page's JSON results
+     * to the online viewer using postMessage.
+     * @param {LH.Result} json
+     * @protected
+     */
+    static openTabAndSendJsonReportToViewer(json) {
+      const windowName = 'viewer-' + this.computeWindowNameSuffix(json);
+      const url = getAppsOrigin() + '/viewer/';
+      ReportUIFeatures.openTabAndSendData({lhr: json}, url, windowName);
+    }
+
+    /**
+     * Opens a new tab to the treemap app and sends the JSON results using URL.fragment
+     * @param {LH.Result} json
+     */
+    static openTreemap(json) {
+      const treemapData = json.audits['script-treemap-data'].details;
+      if (!treemapData) {
+        throw new Error('no script treemap data found');
+      }
+
+      /** @type {LH.Treemap.Options} */
+      const treemapOptions = {
+        lhr: {
+          requestedUrl: json.requestedUrl,
+          finalUrl: json.finalUrl,
+          audits: {
+            'script-treemap-data': json.audits['script-treemap-data'],
+          },
+          configSettings: {
+            locale: json.configSettings.locale,
+          },
+        },
+      };
+      const url = getAppsOrigin() + '/treemap/';
+      const windowName = 'treemap-' + this.computeWindowNameSuffix(json);
+
+      ReportUIFeatures.openTabWithUrlData(treemapOptions, url, windowName);
+    }
+
+    /**
+     * Opens a new tab to an external page and sends data using postMessage.
+     * @param {{lhr: LH.Result} | LH.Treemap.Options} data
+     * @param {string} url
+     * @param {string} windowName
+     * @protected
+     */
+    static openTabAndSendData(data, url, windowName) {
+      const origin = new URL(url).origin;
+      // Chrome doesn't allow us to immediately postMessage to a popup right
+      // after it's created. Normally, we could also listen for the popup window's
+      // load event, however it is cross-domain and won't fire. Instead, listen
+      // for a message from the target app saying "I'm open".
+      window.addEventListener('message', function msgHandler(messageEvent) {
+        if (messageEvent.origin !== origin) {
+          return;
+        }
+        if (popup && messageEvent.data.opened) {
+          popup.postMessage(data, origin);
+          window.removeEventListener('message', msgHandler);
+        }
+      });
+
+      const popup = window.open(url, windowName);
+    }
+
+    /**
+     * Opens a new tab to an external page and sends data via base64 encoded url params.
+     * @param {{lhr: LH.Result} | LH.Treemap.Options} data
+     * @param {string} url_
+     * @param {string} windowName
+     * @protected
+     */
+    static async openTabWithUrlData(data, url_, windowName) {
+      const url = new URL(url_);
+      const gzip = Boolean(window.CompressionStream);
+      url.hash = await TextEncoding.toBase64(JSON.stringify(data), {
+        gzip,
+      });
+      if (gzip) url.searchParams.set('gzip', '1');
+      window.open(url.toString(), windowName);
+    }
+
+    /**
+     * Expands all audit `
`. + * Ideally, a print stylesheet could take care of this, but CSS has no way to + * open a `
` element. + */ + expandAllDetails() { + const details = this._dom.findAll('.lh-categories details', this._document); + details.map(detail => detail.open = true); + } + + /** + * Collapses all audit `
`. + * open a `
` element. + */ + collapseAllDetails() { + const details = this._dom.findAll('.lh-categories details', this._document); + details.map(detail => detail.open = false); + } + + /** + * Sets up listeners to collapse audit `
` when the user closes the + * print dialog, all `
` are collapsed. + */ + _setUpCollapseDetailsAfterPrinting() { + // FF and IE implement these old events. + if ('onbeforeprint' in self) { + self.addEventListener('afterprint', this.collapseAllDetails); + } else { + // Note: FF implements both window.onbeforeprint and media listeners. However, + // it doesn't matchMedia doesn't fire when matching 'print'. + self.matchMedia('print').addListener(mql => { + if (mql.matches) { + this.expandAllDetails(); + } else { + this.collapseAllDetails(); + } + }); + } + } + + /** + * Returns the html that recreates this report. + * @return {string} + * @protected + */ + getReportHtml() { + this._resetUIState(); + return this._document.documentElement.outerHTML; + } + + /** + * Save json as a gist. Unimplemented in base UI features. + * @protected + */ + saveAsGist() { + throw new Error('Cannot save as gist from base report'); + } + + /** + * Downloads a file (blob) using a[download]. + * @param {Blob|File} blob The file to save. + * @private + */ + _saveFile(blob) { + const filename = fileNamer_1({ + finalUrl: this.json.finalUrl, + fetchTime: this.json.fetchTime, + }); + + const ext = blob.type.match('json') ? '.json' : '.html'; + const href = URL.createObjectURL(blob); + + const a = this._dom.createElement('a'); + a.download = `${filename}${ext}`; + a.href = href; + this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM. + a.click(); + + // cleanup. + this._document.body.removeChild(a); + setTimeout(_ => URL.revokeObjectURL(href), 500); + } + + /** + * @private + * @param {boolean} [force] + */ + _toggleDarkTheme(force) { + const el = this._dom.find('.lh-vars', this._document); + // 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 + if (typeof force === 'undefined') { + el.classList.toggle('dark'); + } else { + el.classList.toggle('dark', force); + } + } + + _updateStickyHeaderOnScroll() { + // Show sticky header when the score scale begins to go underneath the topbar. + const topbarBottom = this.topbarEl.getBoundingClientRect().bottom; + const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top; + const showStickyHeader = topbarBottom >= scoreScaleTop; + + // 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 categoriesAboveTheMiddle = + categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0); + const highlightIndex = + categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0; + + // Category order matches gauge order in sticky header. + const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper'); + const gaugeToHighlight = gaugeWrapperEls[highlightIndex]; + const origin = gaugeWrapperEls[0].getBoundingClientRect().left; + const offset = gaugeToHighlight.getBoundingClientRect().left - origin; + + // Mutate at end to avoid layout thrashing. + this.highlightEl.style.transform = `translate(${offset}px)`; + this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader); + } + } + + class DropDown { + /** + * @param {DOM} dom + */ + constructor(dom) { + /** @type {DOM} */ + this._dom = dom; + /** @type {HTMLElement} */ + this._toggleEl; // eslint-disable-line no-unused-expressions + /** @type {HTMLElement} */ + this._menuEl; // eslint-disable-line no-unused-expressions + + this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this); + this.onToggleClick = this.onToggleClick.bind(this); + this.onToggleKeydown = this.onToggleKeydown.bind(this); + this.onMenuFocusOut = this.onMenuFocusOut.bind(this); + this.onMenuKeydown = this.onMenuKeydown.bind(this); + + this._getNextMenuItem = this._getNextMenuItem.bind(this); + this._getNextSelectableNode = this._getNextSelectableNode.bind(this); + this._getPreviousMenuItem = this._getPreviousMenuItem.bind(this); + } + + /** + * @param {function(MouseEvent): any} menuClickHandler + */ + setup(menuClickHandler) { + this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document()); + 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.addEventListener('keydown', this.onMenuKeydown); + this._menuEl.addEventListener('click', menuClickHandler); + } + + close() { + this._toggleEl.classList.remove('active'); + this._toggleEl.setAttribute('aria-expanded', 'false'); + 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); + } + + /** + * @param {HTMLElement} firstFocusElement + */ + open(firstFocusElement) { + if (this._toggleEl.classList.contains('active')) { + // If the drop down is already open focus on the element + firstFocusElement.focus(); + } else { + // Wait for drop down transition to complete so options are focusable. + this._menuEl.addEventListener('transitionend', () => { + firstFocusElement.focus(); + }, {once: true}); + } + + this._toggleEl.classList.add('active'); + this._toggleEl.setAttribute('aria-expanded', 'true'); + this._menuEl.addEventListener('focusout', this.onMenuFocusOut); + this._dom.document().addEventListener('keydown', this.onDocumentKeyDown); + } + + /** + * Click handler for tools button. + * @param {Event} e + */ + onToggleClick(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + if (this._toggleEl.classList.contains('active')) { + this.close(); + } else { + this.open(this._getNextMenuItem()); + } + } + + /** + * Handler for tool button. + * @param {KeyboardEvent} e + */ + onToggleKeydown(e) { + switch (e.code) { + case 'ArrowUp': + e.preventDefault(); + this.open(this._getPreviousMenuItem()); + break; + case 'ArrowDown': + case 'Enter': + case ' ': + e.preventDefault(); + this.open(this._getNextMenuItem()); + break; + // no op + } + } + + /** + * Handler for tool DropDown. + * @param {KeyboardEvent} e + */ + onMenuKeydown(e) { + const el = /** @type {?HTMLElement} */ (e.target); + + switch (e.code) { + case 'ArrowUp': + e.preventDefault(); + this._getPreviousMenuItem(el).focus(); + break; + case 'ArrowDown': + e.preventDefault(); + this._getNextMenuItem(el).focus(); + break; + case 'Home': + e.preventDefault(); + this._getNextMenuItem().focus(); + break; + case 'End': + e.preventDefault(); + this._getPreviousMenuItem().focus(); + break; + // no op + } + } + + /** + * Keydown handler for the document. + * @param {KeyboardEvent} e + */ + onDocumentKeyDown(e) { + if (e.keyCode === 27) { // ESC + this.close(); + } + } + + /** + * Focus out handler for the drop down menu. + * @param {FocusEvent} e + */ + onMenuFocusOut(e) { + const focusedEl = /** @type {?HTMLElement} */ (e.relatedTarget); + + if (!this._menuEl.contains(focusedEl)) { + this.close(); + } + } + + /** + * @param {Array} allNodes + * @param {?HTMLElement=} startNode + * @returns {HTMLElement} + */ + _getNextSelectableNode(allNodes, startNode) { + const nodes = allNodes.filter(/** @return {node is HTMLElement} */ (node) => { + if (!(node instanceof HTMLElement)) { + return false; + } + + // 'Save as Gist' option may be disabled. + if (node.hasAttribute('disabled')) { + return false; + } + + // 'Save as Gist' option may have display none. + if (window.getComputedStyle(node).display === 'none') { + return false; + } + + return true; + }); + + let nextIndex = startNode ? (nodes.indexOf(startNode) + 1) : 0; + if (nextIndex >= nodes.length) { + nextIndex = 0; + } + + return nodes[nextIndex]; + } + + /** + * @param {?HTMLElement=} startEl + * @returns {HTMLElement} + */ + _getNextMenuItem(startEl) { + const nodes = Array.from(this._menuEl.childNodes); + return this._getNextSelectableNode(nodes, startEl); + } + + /** + * @param {?HTMLElement=} startEl + * @returns {HTMLElement} + */ + _getPreviousMenuItem(startEl) { + const nodes = Array.from(this._menuEl.childNodes).reverse(); + return this._getNextSelectableNode(nodes, startEl); + } + } + + /** + * @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. + */ + + function __initLighthouseReport__() { + const dom = new DOM(document); + const renderer = new ReportRenderer(dom); + + const container = document.querySelector('main'); + renderer.renderReport(window.__LIGHTHOUSE_JSON__, container); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(window.__LIGHTHOUSE_JSON__); + } + + if (document.readyState === 'loading') { + window.addEventListener('DOMContentLoaded', __initLighthouseReport__); + } else { + __initLighthouseReport__(); + } + + document.addEventListener('lh-analytics', e => { + if (window.ga) { + ga(e.detail.cmd, e.detail.fields); + } + }); + + document.addEventListener('lh-log', e => { + const logger = new Logger(document.querySelector('#lh-log')); + + switch (e.detail.cmd) { + case 'log': + logger.log(e.detail.msg); + break; + case 'warn': + logger.warn(e.detail.msg); + break; + case 'error': + logger.error(e.detail.msg); + break; + case 'hide': + logger.hide(); + break; + } + }); + +}()); diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js index 17be28ad9565..20c5ab635a31 100644 --- a/lighthouse-core/report/html/renderer/psi.js +++ b/lighthouse-core/report/html/renderer/psi.js @@ -1,3 +1,8 @@ +// TODO: restructure folders? +// html/renderer/common -> mostly everything, including index.js +// html/renderer/clients -> psi.js, standalone.js +// TODO: figure out how psi.js / report code should be added to google3. + /** * @license * Copyright 2018 The Lighthouse Authors. All Rights Reserved. @@ -16,13 +21,13 @@ */ 'use strict'; -import {DetailsRenderer} from './details-renderer.js'; -import {DOM} from './dom.js'; -import {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; -import {I18n} from './i18n.js'; -import {PerformanceCategoryRenderer} from './performance-category-renderer.js'; -import {ReportUIFeatures} from './report-ui-features.js'; -import {Util} from './util.js'; +import {DetailsRenderer} from './common/details-renderer.js'; +import {DOM} from './common/dom.js'; +import {ElementScreenshotRenderer} from './common/element-screenshot-renderer.js'; +import {I18n} from './common/i18n.js'; +import {PerformanceCategoryRenderer} from './common/performance-category-renderer.js'; +import {ReportUIFeatures} from './common/report-ui-features.js'; +import {Util} from './common/util.js'; /** * Returns all the elements that PSI needs to render the report diff --git a/lighthouse-core/report/html/renderer/main.js b/lighthouse-core/report/html/renderer/standalone.js similarity index 89% rename from lighthouse-core/report/html/renderer/main.js rename to lighthouse-core/report/html/renderer/standalone.js index 3a94fd794bea..e1a729027ed6 100644 --- a/lighthouse-core/report/html/renderer/main.js +++ b/lighthouse-core/report/html/renderer/standalone.js @@ -7,10 +7,10 @@ /* global document window */ -import {DOM} from './dom.js'; -import {Logger} from './logger.js'; -import {ReportRenderer} from './report-renderer.js'; -import {ReportUIFeatures} from './report-ui-features.js'; +import {DOM} from './common/dom.js'; +import {Logger} from './common/logger.js'; +import {ReportRenderer} from './common/report-renderer.js'; +import {ReportUIFeatures} from './common/report-ui-features.js'; function __initLighthouseReport__() { const dom = new DOM(document); diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html index a586ca0bf6da..4bce64f6eee4 100644 --- a/lighthouse-core/report/html/report-template.html +++ b/lighthouse-core/report/html/report-template.html @@ -31,46 +31,6 @@
- %%LIGHTHOUSE_JAVASCRIPT_MODULES%% - - - - diff --git a/lighthouse-core/report/report-generator.js b/lighthouse-core/report/report-generator.js index 51c3ed7ea367..736c4bc61c52 100644 --- a/lighthouse-core/report/report-generator.js +++ b/lighthouse-core/report/report-generator.js @@ -39,17 +39,17 @@ class ReportGenerator { .replace(/\u2029/g, '\\u2029'); // replaces paragraph separators const sanitizedJavascript = htmlReportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/'); - let sanitizedJavascriptModules = ''; - for (const [id, code] of Object.entries(htmlReportAssets.REPORT_JAVASCRIPT_MODULES)) { - const sanitizedCode = code.replace(/<\//g, '\\u003c/'); - sanitizedJavascriptModules += - ``; - } + // let sanitizedJavascriptModules = ''; + // for (const [id, code] of Object.entries(htmlReportAssets.REPORT_JAVASCRIPT_MODULES)) { + // const sanitizedCode = code.replace(/<\//g, '\\u003c/'); + // sanitizedJavascriptModules += + // ``; + // } return ReportGenerator.replaceStrings(htmlReportAssets.REPORT_TEMPLATE, [ {search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson}, {search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: sanitizedJavascript}, - {search: '%%LIGHTHOUSE_JAVASCRIPT_MODULES%%', replacement: sanitizedJavascriptModules}, + // {search: '%%LIGHTHOUSE_JAVASCRIPT_MODULES%%', replacement: sanitizedJavascriptModules}, {search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: htmlReportAssets.REPORT_CSS}, {search: '%%LIGHTHOUSE_TEMPLATES%%', replacement: htmlReportAssets.REPORT_TEMPLATES}, ]); diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index 62f5f00986fc..11a3edd279ad 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -169,6 +169,12 @@ class Runner { assetSaver.saveLhr(lhr, path); } + // Build report if in local dev env so we don't have to run a watch command. + // TODO: dev checkout only. what to look for? existence of `dist/`? + if (settings.output === 'html') { + await require('../build/build-report.js').buildStandaloneReport(); + } + // Create the HTML, JSON, and/or CSV string const report = generateReport(lhr, settings.output); diff --git a/package.json b/package.json index 92c9243bd82d..51534999c697 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,8 @@ "pako": "^2.0.3", "pretty-json-stringify": "^0.0.2", "puppeteer": "^9.1.1", + "rollup": "^2.50.6", + "rollup-plugin-commonjs": "^10.1.0", "tabulator-tables": "^4.9.3", "terser": "^5.3.8", "typed-query-selector": "^2.4.0", diff --git a/types/i18n.d.ts b/types/i18n.d.ts index 624c2aec087f..86ac93ddaa31 100644 --- a/types/i18n.d.ts +++ b/types/i18n.d.ts @@ -4,7 +4,7 @@ * 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 {Util} from '../lighthouse-core/report/html/renderer/util.js'; +import {Util} from '../lighthouse-core/report/html/renderer/common/util.js'; declare global { module LH { diff --git a/yarn.lock b/yarn.lock index 29622eb096c0..89610e1b5068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3408,6 +3408,11 @@ estree-is-function@^1.0.0: resolved "https://registry.yarnpkg.com/estree-is-function/-/estree-is-function-1.0.0.tgz#c0adc29806d7f18a74db7df0f3b2666702e37ad2" integrity sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA== +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -3797,7 +3802,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.2: +fsevents@^2.3.2, fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -4692,6 +4697,13 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-reference@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" @@ -5749,6 +5761,13 @@ magic-string@0.25.1: dependencies: sourcemap-codec "^1.4.1" +magic-string@^0.25.2: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -7087,7 +7106,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.4.0: +resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.4.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -7147,6 +7166,31 @@ robots-parser@^2.0.1: resolved "https://registry.npmjs.org/robots-parser/-/robots-parser-2.1.0.tgz#d16b78ce34e861ab6afbbf0aac65974dbe01566e" integrity sha512-k07MeDS1Tl1zjoYs5bHfUbgQ0MfaeTOepDcjZFxdYXd84p6IeLDQyUwlMk2AZ9c2yExA30I3ayWhmqz9tg0DzQ== +rollup-plugin-commonjs@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb" + integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q== + dependencies: + estree-walker "^0.6.1" + is-reference "^1.1.2" + magic-string "^0.25.2" + resolve "^1.11.0" + rollup-pluginutils "^2.8.1" + +rollup-pluginutils@^2.8.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@^2.50.6: + version "2.50.6" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.50.6.tgz#24e2211caf9031081656e98a5e5e94d3b5e786e2" + integrity sha512-6c5CJPLVgo0iNaZWWliNu1Kl43tjP9LZcp6D/tkf2eLH2a9/WeHxg9vfTFl8QV/2SOyaJX37CEm9XuGM0rviUg== + optionalDependencies: + fsevents "~2.3.1" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -7425,7 +7469,7 @@ source-map@^0.7.3, source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sourcemap-codec@^1.4.1: +sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== From 9b5e3039959a7f7b6984591f8ef37bca79a67345 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Mon, 21 Jun 2021 16:36:23 -0700 Subject: [PATCH 03/71] copy and transform util.js to commonjs --- .github/workflows/ci.yml | 1 + lighthouse-core/audits/audit.js | 54 +- lighthouse-core/computed/resource-summary.js | 56 +- lighthouse-core/lib/url-shim.js | 126 +--- lighthouse-core/scripts/copy-util-commonjs.sh | 18 + lighthouse-core/util-commonjs.js | 643 ++++++++++++++++++ 6 files changed, 665 insertions(+), 233 deletions(-) create mode 100644 lighthouse-core/scripts/copy-util-commonjs.sh create mode 100644 lighthouse-core/util-commonjs.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a0b933e28c1..28bfd259e619 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - run: yarn test-legacy-javascript - run: yarn i18n:checks - run: yarn dogfood-lhci + - run: sh lighthouse-core/scripts/copy-util-commonjs.sh # Fail if any changes were written to any source files or generated untracked files (ex, from: build/build-cdt-lib.js). - run: git add -A && git diff --cached --exit-code diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js index cc2ea3987ae0..dea749fd9c1a 100644 --- a/lighthouse-core/audits/audit.js +++ b/lighthouse-core/audits/audit.js @@ -7,59 +7,7 @@ const {isUnderTest} = require('../lib/lh-env.js'); const statistics = require('../lib/statistics.js'); -// const Util = require('../report/html/renderer/util.js'); -class Util { - static PASS_THRESHOLD = 0.9; - - /** - * Returns only lines that are near a message, or the first few lines if there are - * no line messages. - * @param {LH.Audit.Details.SnippetValue['lines']} lines - * @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages - * @param {number} surroundingLineCount Number of lines to include before and after - * the message. If this is e.g. 2 this function might return 5 lines. - */ - static filterRelevantLines(lines, lineMessages, surroundingLineCount) { - if (lineMessages.length === 0) { - // no lines with messages, just return the first bunch of lines - return lines.slice(0, surroundingLineCount * 2 + 1); - } - - const minGapSize = 3; - const lineNumbersToKeep = new Set(); - // Sort messages so we can check lineNumbersToKeep to see how big the gap to - // the previous line is. - lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0)); - lineMessages.forEach(({lineNumber}) => { - let firstSurroundingLineNumber = lineNumber - surroundingLineCount; - let lastSurroundingLineNumber = lineNumber + surroundingLineCount; - - while (firstSurroundingLineNumber < 1) { - // make sure we still show (surroundingLineCount * 2 + 1) lines in total - firstSurroundingLineNumber++; - lastSurroundingLineNumber++; - } - // If only a few lines would be omitted normally then we prefer to include - // extra lines to avoid the tiny gap - if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) { - firstSurroundingLineNumber -= minGapSize; - } - for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) { - const surroundingLineNumber = i; - lineNumbersToKeep.add(surroundingLineNumber); - } - }); - - return lines.filter(line => lineNumbersToKeep.has(line.lineNumber)); - } - - /** - * @param {string} categoryId - */ - static isPluginCategory(categoryId) { - return categoryId.startsWith('lighthouse-plugin-'); - } -} +const Util = require('../util-commonjs.js'); const DEFAULT_PASS = 'defaultPass'; diff --git a/lighthouse-core/computed/resource-summary.js b/lighthouse-core/computed/resource-summary.js index 6c1f57f9335b..a7944472fb6d 100644 --- a/lighthouse-core/computed/resource-summary.js +++ b/lighthouse-core/computed/resource-summary.js @@ -11,61 +11,7 @@ const URL = require('../lib/url-shim.js'); const NetworkRequest = require('../lib/network-request.js'); const MainResource = require('./main-resource.js'); const Budget = require('../config/budget.js'); -// const Util = require('../report/html/renderer/util.js'); - -// 25 most used tld plus one domains (aka public suffixes) from http archive. -// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 -// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes -const listOfTlds = [ - 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', - 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', -]; -class Util { - /** - * @param {string|URL} value - * @return {!URL} - */ - static createOrReturnURL(value) { - if (value instanceof URL) { - return value; - } - - return new URL(value); - } - - /** - * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). - * @param {string|URL} url hostname or URL object - * @returns {string} - */ - static getRootDomain(url) { - const hostname = Util.createOrReturnURL(url).hostname; - const tld = Util.getTld(hostname); - - // tld is .com or .co.uk which means we means that length is 1 to big - // .com => 2 & .co.uk => 3 - const splitTld = tld.split('.'); - - // get TLD + root domain - return hostname.split('.').slice(-splitTld.length).join('.'); - } - - /** - * Gets the tld of a domain - * - * @param {string} hostname - * @return {string} tld - */ - static getTld(hostname) { - const tlds = hostname.split('.').slice(-2); - - if (!listOfTlds.includes(tlds[0])) { - return `.${tlds[tlds.length - 1]}`; - } - - return `.${tlds.join('.')}`; - } -} +const Util = require('../util-commonjs.js'); /** @typedef {{count: number, resourceSize: number, transferSize: number}} ResourceEntry */ diff --git a/lighthouse-core/lib/url-shim.js b/lighthouse-core/lib/url-shim.js index 94f0161de1de..4926c31d3c08 100644 --- a/lighthouse-core/lib/url-shim.js +++ b/lighthouse-core/lib/url-shim.js @@ -9,131 +9,7 @@ * URL shim so we keep our code DRY */ -// const Util = require('../report/html/renderer/util.js'); -const ELLIPSIS = '\u2026'; -// 25 most used tld plus one domains (aka public suffixes) from http archive. -// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 -// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes -const listOfTlds = [ - 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', - 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', -]; -class Util { - /** - * @param {string|URL} value - * @return {!URL} - */ - static createOrReturnURL(value) { - if (value instanceof URL) { - return value; - } - - return new URL(value); - } - - /** - * @param {URL} parsedUrl - * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options - * @return {string} - */ - static getURLDisplayName(parsedUrl, options) { - // Closure optional properties aren't optional in tsc, so fallback needs undefined values. - options = options || {numPathParts: undefined, preserveQuery: undefined, - preserveHost: undefined}; - const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2; - const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true; - const preserveHost = options.preserveHost || false; - - let name; - - if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') { - // Handle 'about:*' and 'data:*' URLs specially since they have no path. - name = parsedUrl.href; - } else { - name = parsedUrl.pathname; - const parts = name.split('/').filter(part => part.length); - if (numPathParts && parts.length > numPathParts) { - name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/'); - } - - if (preserveHost) { - name = `${parsedUrl.host}/${name.replace(/^\//, '')}`; - } - if (preserveQuery) { - name = `${name}${parsedUrl.search}`; - } - } - - const MAX_LENGTH = 64; - // Always elide hexadecimal hash - name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`); - // Also elide other hash-like mixed-case strings - name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g, - `$1${ELLIPSIS}`); - // Also elide long number sequences - name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`); - // Merge any adjacent ellipses - name = name.replace(/\u2026+/g, ELLIPSIS); - - // Elide query params first - if (name.length > MAX_LENGTH && name.includes('?')) { - // Try to leave the first query parameter intact - name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`); - - // Remove it all if it's still too long - if (name.length > MAX_LENGTH) { - name = name.replace(/\?.*/, `?${ELLIPSIS}`); - } - } - - // Elide too long names next - if (name.length > MAX_LENGTH) { - const dotIndex = name.lastIndexOf('.'); - if (dotIndex >= 0) { - name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + - // Show file extension - `${ELLIPSIS}${name.slice(dotIndex)}`; - } else { - name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS; - } - } - - return name; - } - - /** - * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). - * @param {string|URL} url hostname or URL object - * @returns {string} - */ - static getRootDomain(url) { - const hostname = Util.createOrReturnURL(url).hostname; - const tld = Util.getTld(hostname); - - // tld is .com or .co.uk which means we means that length is 1 to big - // .com => 2 & .co.uk => 3 - const splitTld = tld.split('.'); - - // get TLD + root domain - return hostname.split('.').slice(-splitTld.length).join('.'); - } - - /** - * Gets the tld of a domain - * - * @param {string} hostname - * @return {string} tld - */ - static getTld(hostname) { - const tlds = hostname.split('.').slice(-2); - - if (!listOfTlds.includes(tlds[0])) { - return `.${tlds[tlds.length - 1]}`; - } - - return `.${tlds.join('.')}`; - } -} +const Util = require('../util-commonjs.js'); /** @typedef {import('./network-request.js')} NetworkRequest */ diff --git a/lighthouse-core/scripts/copy-util-commonjs.sh b/lighthouse-core/scripts/copy-util-commonjs.sh new file mode 100644 index 000000000000..1083e5254158 --- /dev/null +++ b/lighthouse-core/scripts/copy-util-commonjs.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +## +# @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. +## + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LH_ROOT_DIR="$SCRIPT_DIR/../.." + +OUT_FILE="$LH_ROOT_DIR"/lighthouse-core/util-commonjs.js + +echo '// @ts-nocheck' > "$OUT_FILE" +echo '// Auto-generated by lighthouse-core/scripts/copy-util-commonjs.sh' >> "$OUT_FILE" +echo '// Temporary solution until all our code uses esmodules' >> "$OUT_FILE" +sed 's/export //g' "$LH_ROOT_DIR"/lighthouse-core/report/html/renderer/common/util.js >> "$OUT_FILE" +echo 'module.exports = Util;' >> "$OUT_FILE" diff --git a/lighthouse-core/util-commonjs.js b/lighthouse-core/util-commonjs.js new file mode 100644 index 000000000000..07fdce225d47 --- /dev/null +++ b/lighthouse-core/util-commonjs.js @@ -0,0 +1,643 @@ +// @ts-nocheck +// Auto-generated by lighthouse-core/scripts/copy-util-commonjs.sh +// Temporary solution until all our code uses esmodules +/** + * @license + * Copyright 2017 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'; + +/** @template T @typedef {import('./i18n').I18n} I18n */ + +const ELLIPSIS = '\u2026'; +const NBSP = '\xa0'; +const PASS_THRESHOLD = 0.9; +const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,'; + +const RATINGS = { + PASS: {label: 'pass', minScore: PASS_THRESHOLD}, + AVERAGE: {label: 'average', minScore: 0.5}, + FAIL: {label: 'fail'}, + ERROR: {label: 'error'}, +}; + +// 25 most used tld plus one domains (aka public suffixes) from http archive. +// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 +// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes +const listOfTlds = [ + 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', + 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', +]; + +class Util { + static get PASS_THRESHOLD() { + return PASS_THRESHOLD; + } + + static get MS_DISPLAY_VALUE() { + return `%10d${NBSP}ms`; + } + + /** + * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer. + * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be + * compatible with current renderer. + * The LHR passed in is not mutated. + * TODO(team): we all agree the LHR shape change is technical debt we should fix + * @param {LH.Result} result + * @return {LH.ReportResult} + */ + static prepareReportResult(result) { + // If any mutations happen to the report within the renderers, we want the original object untouched + const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result))); + + // If LHR is older (≤3.0.3), it has no locale setting. Set default. + if (!clone.configSettings.locale) { + clone.configSettings.locale = 'en'; + } + if (!clone.configSettings.formFactor) { + // @ts-expect-error fallback handling for emulatedFormFactor + clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor; + } + + for (const audit of Object.values(clone.audits)) { + // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions) + // into 'notApplicable' (LHR ≥4.0). + // @ts-expect-error tsc rightly flags that these values shouldn't occur. + // eslint-disable-next-line max-len + if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') { + audit.scoreDisplayMode = 'notApplicable'; + } + + if (audit.details) { + // Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0) + // into 'debugdata' (LHR ≥5.0). + // @ts-expect-error tsc rightly flags that these values shouldn't occur. + if (audit.details.type === undefined || audit.details.type === 'diagnostic') { + // @ts-expect-error details is of type never. + audit.details.type = 'debugdata'; + } + + // Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0). + if (audit.details.type === 'filmstrip') { + for (const screenshot of audit.details.items) { + if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) { + screenshot.data = SCREENSHOT_PREFIX + screenshot.data; + } + } + } + } + } + + // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group) + if (typeof clone.categories !== 'object') throw new Error('No categories provided.'); + + /** @type {Map>} */ + const relevantAuditToMetricsMap = new Map(); + + for (const category of Object.values(clone.categories)) { + // Make basic lookup table for relevantAudits + category.auditRefs.forEach(metricRef => { + if (!metricRef.relevantAudits) return; + metricRef.relevantAudits.forEach(auditId => { + const arr = relevantAuditToMetricsMap.get(auditId) || []; + arr.push(metricRef); + relevantAuditToMetricsMap.set(auditId, arr); + }); + }); + + category.auditRefs.forEach(auditRef => { + const result = clone.audits[auditRef.id]; + auditRef.result = result; + + // Attach any relevantMetric auditRefs + if (relevantAuditToMetricsMap.has(auditRef.id)) { + auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id); + } + + // attach the stackpacks to the auditRef object + if (clone.stackPacks) { + clone.stackPacks.forEach(pack => { + if (pack.descriptions[auditRef.id]) { + auditRef.stackPacks = auditRef.stackPacks || []; + auditRef.stackPacks.push({ + title: pack.title, + iconDataURL: pack.iconDataURL, + description: pack.descriptions[auditRef.id], + }); + } + }); + } + }); + } + + return clone; + } + + /** + * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed" + * sections of the report. + * + * @param {{score: (number|null), scoreDisplayMode: string}} audit + * @return {boolean} + */ + static showAsPassed(audit) { + switch (audit.scoreDisplayMode) { + case 'manual': + case 'notApplicable': + return true; + case 'error': + case 'informative': + return false; + case 'numeric': + case 'binary': + default: + return Number(audit.score) >= RATINGS.PASS.minScore; + } + } + + /** + * Convert a score to a rating label. + * @param {number|null} score + * @param {string=} scoreDisplayMode + * @return {string} + */ + static calculateRating(score, scoreDisplayMode) { + // Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error' + if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') { + return RATINGS.PASS.label; + } else if (scoreDisplayMode === 'error') { + return RATINGS.ERROR.label; + } else if (score === null) { + return RATINGS.FAIL.label; + } + + // At this point, we're rating a standard binary/numeric audit + let rating = RATINGS.FAIL.label; + if (score >= RATINGS.PASS.minScore) { + rating = RATINGS.PASS.label; + } else if (score >= RATINGS.AVERAGE.minScore) { + rating = RATINGS.AVERAGE.label; + } + return rating; + } + + /** + * Split a string by markdown code spans (enclosed in `backticks`), splitting + * into segments that were enclosed in backticks (marked as `isCode === true`) + * and those that outside the backticks (`isCode === false`). + * @param {string} text + * @return {Array<{isCode: true, text: string}|{isCode: false, text: string}>} + */ + static splitMarkdownCodeSpans(text) { + /** @type {Array<{isCode: true, text: string}|{isCode: false, text: string}>} */ + const segments = []; + + // Split on backticked code spans. + const parts = text.split(/`(.*?)`/g); + for (let i = 0; i < parts.length; i ++) { + const text = parts[i]; + + // Empty strings are an artifact of splitting, not meaningful. + if (!text) continue; + + // Alternates between plain text and code segments. + const isCode = i % 2 !== 0; + segments.push({ + isCode, + text, + }); + } + + return segments; + } + + /** + * Split a string on markdown links (e.g. [some link](https://...)) into + * segments of plain text that weren't part of a link (marked as + * `isLink === false`), and segments with text content and a URL that did make + * up a link (marked as `isLink === true`). + * @param {string} text + * @return {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} + */ + static splitMarkdownLink(text) { + /** @type {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} */ + const segments = []; + + const parts = text.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g); + while (parts.length) { + // Shift off the same number of elements as the pre-split and capture groups. + const [preambleText, linkText, linkHref] = parts.splice(0, 3); + + if (preambleText) { // Skip empty text as it's an artifact of splitting, not meaningful. + segments.push({ + isLink: false, + text: preambleText, + }); + } + + // Append link if there are any. + if (linkText && linkHref) { + segments.push({ + isLink: true, + text: linkText, + linkHref, + }); + } + } + + return segments; + } + + /** + * @param {URL} parsedUrl + * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options + * @return {string} + */ + static getURLDisplayName(parsedUrl, options) { + // Closure optional properties aren't optional in tsc, so fallback needs undefined values. + options = options || {numPathParts: undefined, preserveQuery: undefined, + preserveHost: undefined}; + const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2; + const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true; + const preserveHost = options.preserveHost || false; + + let name; + + if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') { + // Handle 'about:*' and 'data:*' URLs specially since they have no path. + name = parsedUrl.href; + } else { + name = parsedUrl.pathname; + const parts = name.split('/').filter(part => part.length); + if (numPathParts && parts.length > numPathParts) { + name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/'); + } + + if (preserveHost) { + name = `${parsedUrl.host}/${name.replace(/^\//, '')}`; + } + if (preserveQuery) { + name = `${name}${parsedUrl.search}`; + } + } + + const MAX_LENGTH = 64; + // Always elide hexadecimal hash + name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`); + // Also elide other hash-like mixed-case strings + name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g, + `$1${ELLIPSIS}`); + // Also elide long number sequences + name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`); + // Merge any adjacent ellipses + name = name.replace(/\u2026+/g, ELLIPSIS); + + // Elide query params first + if (name.length > MAX_LENGTH && name.includes('?')) { + // Try to leave the first query parameter intact + name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`); + + // Remove it all if it's still too long + if (name.length > MAX_LENGTH) { + name = name.replace(/\?.*/, `?${ELLIPSIS}`); + } + } + + // Elide too long names next + if (name.length > MAX_LENGTH) { + const dotIndex = name.lastIndexOf('.'); + if (dotIndex >= 0) { + name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + + // Show file extension + `${ELLIPSIS}${name.slice(dotIndex)}`; + } else { + name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS; + } + } + + return name; + } + + /** + * Split a URL into a file, hostname and origin for easy display. + * @param {string} url + * @return {{file: string, hostname: string, origin: string}} + */ + static parseURL(url) { + const parsedUrl = new URL(url); + return { + file: Util.getURLDisplayName(parsedUrl), + hostname: parsedUrl.hostname, + origin: parsedUrl.origin, + }; + } + + /** + * @param {string|URL} value + * @return {!URL} + */ + static createOrReturnURL(value) { + if (value instanceof URL) { + return value; + } + + return new URL(value); + } + + /** + * Gets the tld of a domain + * + * @param {string} hostname + * @return {string} tld + */ + static getTld(hostname) { + const tlds = hostname.split('.').slice(-2); + + if (!listOfTlds.includes(tlds[0])) { + return `.${tlds[tlds.length - 1]}`; + } + + return `.${tlds.join('.')}`; + } + + /** + * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). + * @param {string|URL} url hostname or URL object + * @returns {string} + */ + static getRootDomain(url) { + const hostname = Util.createOrReturnURL(url).hostname; + const tld = Util.getTld(hostname); + + // tld is .com or .co.uk which means we means that length is 1 to big + // .com => 2 & .co.uk => 3 + const splitTld = tld.split('.'); + + // get TLD + root domain + return hostname.split('.').slice(-splitTld.length).join('.'); + } + + /** + * @param {LH.Config.Settings} settings + * @return {!Array<{name: string, description: string}>} + */ + static getEnvironmentDisplayValues(settings) { + const emulationDesc = Util.getEmulationDescriptions(settings); + + return [ + { + name: Util.i18n.strings.runtimeSettingsDevice, + description: emulationDesc.deviceEmulation, + }, + { + name: Util.i18n.strings.runtimeSettingsNetworkThrottling, + description: emulationDesc.networkThrottling, + }, + { + name: Util.i18n.strings.runtimeSettingsCPUThrottling, + description: emulationDesc.cpuThrottling, + }, + ]; + } + + /** + * @param {LH.Config.Settings} settings + * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}} + */ + static getEmulationDescriptions(settings) { + let cpuThrottling; + let networkThrottling; + + const throttling = settings.throttling; + + switch (settings.throttlingMethod) { + case 'provided': + cpuThrottling = Util.i18n.strings.throttlingProvided; + networkThrottling = Util.i18n.strings.throttlingProvided; + break; + case 'devtools': { + const {cpuSlowdownMultiplier, requestLatencyMs} = throttling; + cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`; + networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` + + `${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` + + `${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`; + break; + } + case 'simulate': { + const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling; + cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`; + networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` + + `${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`; + break; + } + default: + cpuThrottling = Util.i18n.strings.runtimeUnknown; + networkThrottling = Util.i18n.strings.runtimeUnknown; + } + + // TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796 + const deviceEmulation = { + mobile: Util.i18n.strings.runtimeMobileEmulation, + desktop: Util.i18n.strings.runtimeDesktopEmulation, + }[settings.formFactor] || Util.i18n.strings.runtimeNoEmulation; + + return { + deviceEmulation, + cpuThrottling, + networkThrottling, + }; + } + + /** + * Returns only lines that are near a message, or the first few lines if there are + * no line messages. + * @param {LH.Audit.Details.SnippetValue['lines']} lines + * @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages + * @param {number} surroundingLineCount Number of lines to include before and after + * the message. If this is e.g. 2 this function might return 5 lines. + */ + static filterRelevantLines(lines, lineMessages, surroundingLineCount) { + if (lineMessages.length === 0) { + // no lines with messages, just return the first bunch of lines + return lines.slice(0, surroundingLineCount * 2 + 1); + } + + const minGapSize = 3; + const lineNumbersToKeep = new Set(); + // Sort messages so we can check lineNumbersToKeep to see how big the gap to + // the previous line is. + lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0)); + lineMessages.forEach(({lineNumber}) => { + let firstSurroundingLineNumber = lineNumber - surroundingLineCount; + let lastSurroundingLineNumber = lineNumber + surroundingLineCount; + + while (firstSurroundingLineNumber < 1) { + // make sure we still show (surroundingLineCount * 2 + 1) lines in total + firstSurroundingLineNumber++; + lastSurroundingLineNumber++; + } + // If only a few lines would be omitted normally then we prefer to include + // extra lines to avoid the tiny gap + if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) { + firstSurroundingLineNumber -= minGapSize; + } + for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) { + const surroundingLineNumber = i; + lineNumbersToKeep.add(surroundingLineNumber); + } + }); + + return lines.filter(line => lineNumbersToKeep.has(line.lineNumber)); + } + + /** + * @param {string} categoryId + */ + static isPluginCategory(categoryId) { + return categoryId.startsWith('lighthouse-plugin-'); + } +} + +/** + * Some parts of the report renderer require data found on the LHR. Instead of wiring it + * through, we have this global. + * @type {LH.ReportResult | null} + */ +Util.reportJson = null; + +/** + * An always-increasing counter for making unique SVG ID suffixes. + */ +Util.getUniqueSuffix = (() => { + let svgSuffix = 0; + return function() { + return svgSuffix++; + }; +})(); + +/** @type {I18n} */ +// @ts-expect-error: Is set in report renderer. +Util.i18n = null; + +/** + * Report-renderer-specific strings. + */ +Util.UIStrings = { + /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */ + varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.', + /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */ + calculatorLink: 'See calculator.', + /** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */ + showRelevantAudits: 'Show audits relevant to:', + /** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */ + opportunityResourceColumnLabel: 'Opportunity', + /** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */ + opportunitySavingsColumnLabel: 'Estimated Savings', + + /** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */ + errorMissingAuditInfo: 'Report error: no audit information', + /** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */ + errorLabel: 'Error!', + /** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */ + warningHeader: 'Warnings: ', + /** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */ + warningAuditsGroupTitle: 'Passed audits but with warnings', + /** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + passedAuditsGroupTitle: 'Passed audits', + /** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + notApplicableAuditsGroupTitle: 'Not applicable', + /** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ + manualAuditsGroupTitle: 'Additional items to manually check', + + /** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */ + toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:', + + /** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */ + crcInitialNavigation: 'Initial Navigation', + /** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */ + crcLongestDurationLabel: 'Maximum critical path latency:', + + /** Label for button that shows all lines of the snippet when clicked */ + snippetExpandButtonLabel: 'Expand snippet', + /** Label for button that only shows a few lines of the snippet when clicked */ + snippetCollapseButtonLabel: 'Collapse snippet', + + /** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */ + lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.', + /** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */ + labDataTitle: 'Lab Data', + + /** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */ + thirdPartyResourcesLabel: 'Show 3rd-party resources', + /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */ + viewTreemapLabel: 'View Treemap', + + /** Option in a dropdown menu that opens a small, summary report in a print dialog. */ + dropdownPrintSummary: 'Print Summary', + /** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */ + dropdownPrintExpanded: 'Print Expanded', + /** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */ + dropdownCopyJSON: 'Copy JSON', + /** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */ + dropdownSaveHTML: 'Save as HTML', + /** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */ + dropdownSaveJSON: 'Save as JSON', + /** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */ + dropdownViewer: 'Open in Viewer', + /** Option in a dropdown menu that saves the current report as a new GitHub Gist. */ + dropdownSaveGist: 'Save as Gist', + /** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */ + dropdownDarkTheme: 'Toggle Dark Theme', + + /** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */ + runtimeSettingsTitle: 'Runtime Settings', + /** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */ + runtimeSettingsUrl: 'URL', + /** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */ + runtimeSettingsFetchTime: 'Fetch Time', + /** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */ + runtimeSettingsDevice: 'Device', + /** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */ + runtimeSettingsNetworkThrottling: 'Network throttling', + /** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/ + runtimeSettingsCPUThrottling: 'CPU throttling', + /** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */ + runtimeSettingsChannel: 'Channel', + /** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */ + runtimeSettingsUA: 'User agent (host)', + /** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */ + runtimeSettingsUANetwork: 'User agent (network)', + /** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */ + runtimeSettingsBenchmark: 'CPU/Memory Power', + /** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */ + runtimeSettingsAxeVersion: 'Axe version', + + /** Label for button to create an issue against the Lighthouse GitHub project. */ + footerIssue: 'File an issue', + + /** Descriptive explanation for emulation setting when no device emulation is set. */ + runtimeNoEmulation: 'No emulation', + /** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */ + runtimeMobileEmulation: 'Emulated Moto G4', + /** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */ + runtimeDesktopEmulation: 'Emulated Desktop', + /** Descriptive explanation for a runtime setting that is set to an unknown value. */ + runtimeUnknown: 'Unknown', + + /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */ + throttlingProvided: 'Provided by environment', +}; +module.exports = Util; From f32562dd760c8b45fd5fc6ef5556c6d91dc1d947 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 22 Jun 2021 13:23:55 -0700 Subject: [PATCH 04/71] tweak --- lighthouse-core/scripts/roll-to-devtools.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index b31c5fab60da..436ff0cc698f 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -45,9 +45,9 @@ echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." # copy report code $fe_lh_dir fe_lh_report_dir="$fe_lh_dir/report/" -rsync -avh lighthouse-core/report/html/renderer/ "$fe_lh_report_dir" --exclude="BUILD.gn" --delete +rsync -avh lighthouse-core/report/html/renderer/ "$fe_lh_report_dir" --exclude="BUILD.gn" --exclude="report-tsconfig.json" --exclude="generated" --exclude="psi.js" --delete # file-namer.js is not used, but we should export something so it compiles. -echo 'export const getFilenamePrefix = () => {};' > "$fe_lh_report_dir/file-namer.js" +echo 'export const getFilenamePrefix = () => {};' > "$fe_lh_report_dir/common/file-namer.js" echo -e "$check Report code copied." # copy report generator + cached resources into $fe_lh_dir From e6bb97259aa43f42e23dcdb79350e657ec9b3f73 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 22 Jun 2021 14:55:19 -0700 Subject: [PATCH 05/71] update for cdt --- .../report/html/renderer/standalone.js | 36 +++++++++++-------- lighthouse-core/scripts/roll-to-devtools.sh | 11 +++++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lighthouse-core/report/html/renderer/standalone.js b/lighthouse-core/report/html/renderer/standalone.js index e1a729027ed6..943c0ede1e75 100644 --- a/lighthouse-core/report/html/renderer/standalone.js +++ b/lighthouse-core/report/html/renderer/standalone.js @@ -5,7 +5,7 @@ */ 'use strict'; -/* global document window */ +/* global document window ga */ import {DOM} from './common/dom.js'; import {Logger} from './common/logger.js'; @@ -15,14 +15,16 @@ import {ReportUIFeatures} from './common/report-ui-features.js'; function __initLighthouseReport__() { const dom = new DOM(document); const renderer = new ReportRenderer(dom); - - const container = document.querySelector('main'); - renderer.renderReport(window.__LIGHTHOUSE_JSON__, container); + const container = dom.find('main', document); + /** @type {LH.ReportResult} */ + // @ts-expect-error + const lhr = window.__LIGHTHOUSE_JSON__; + renderer.renderReport(lhr, container); // Hook in JS features and page-level event listeners after the report // is in the document. const features = new ReportUIFeatures(dom); - features.initFeatures(window.__LIGHTHOUSE_JSON__); + features.initFeatures(lhr); } if (document.readyState === 'loading') { @@ -31,24 +33,28 @@ if (document.readyState === 'loading') { __initLighthouseReport__(); } -document.addEventListener('lh-analytics', e => { - if (window.ga) { - ga(e.detail.cmd, e.detail.fields); - } +document.addEventListener('lh-analytics', /** @param {Event} e */ e => { + // @ts-expect-error + if (window.ga) ga(e.detail.cmd, e.detail.fields); }); -document.addEventListener('lh-log', e => { - const logger = new Logger(document.querySelector('#lh-log')); +document.addEventListener('lh-log', /** @param {Event} e */ e => { + const el = document.querySelector('#lh-log'); + if (!el) return; + + const logger = new Logger(el); + // @ts-expect-error + const detail = e.detail; - switch (e.detail.cmd) { + switch (detail.cmd) { case 'log': - logger.log(e.detail.msg); + logger.log(detail.msg); break; case 'warn': - logger.warn(e.detail.msg); + logger.warn(detail.msg); break; case 'error': - logger.error(e.detail.msg); + logger.error(detail.msg); break; case 'hide': logger.hide(); diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index 436ff0cc698f..b7066f526be2 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -43,13 +43,22 @@ lh_bg_js="dist/lighthouse-dt-bundle.js" cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." +# generate .d.ts files +npx tsc --allowJs --declaration --emitDeclarationOnly lighthouse-core/report/html/renderer/standalone.js + # copy report code $fe_lh_dir fe_lh_report_dir="$fe_lh_dir/report/" rsync -avh lighthouse-core/report/html/renderer/ "$fe_lh_report_dir" --exclude="BUILD.gn" --exclude="report-tsconfig.json" --exclude="generated" --exclude="psi.js" --delete # file-namer.js is not used, but we should export something so it compiles. -echo 'export const getFilenamePrefix = () => {};' > "$fe_lh_report_dir/common/file-namer.js" +echo 'export const getFilenamePrefix = () => {throw new Error("not used in CDT")};' > "$fe_lh_report_dir/common/file-namer.js" echo -e "$check Report code copied." +# delete those .d.ts files +rm -rf lighthouse-core/report/html/renderer/**/*.d.ts +rm lighthouse-core/lib/file-namer.d.ts +# weird that this is needed too ... +rm lighthouse-core/report/html/renderer/standalone.d.ts + # copy report generator + cached resources into $fe_lh_dir fe_lh_report_assets_dir="$fe_lh_dir/report-assets/" rsync -avh dist/dt-report-resources/ "$fe_lh_report_assets_dir" --delete From 937c71041b2621177c2f9b463205d3c42dd1a555 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 22 Jun 2021 16:15:01 -0700 Subject: [PATCH 06/71] import --- clients/devtools-report-assets.js | 2 +- lighthouse-core/runner.js | 8 +- urls-small.txt | 1 + urls.txt | 520 ++++++++++++++++++++++++++++++ 4 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 urls-small.txt create mode 100644 urls.txt diff --git a/clients/devtools-report-assets.js b/clients/devtools-report-assets.js index 9f962af142d2..39ff340677e3 100644 --- a/clients/devtools-report-assets.js +++ b/clients/devtools-report-assets.js @@ -25,7 +25,7 @@ module.exports = { return cachedResources.get('third_party/lighthouse/report-assets/report.css'); }, get REPORT_JAVASCRIPT() { - return cachedResources.get('third_party/lighthouse/report-assets/report.js'); + return cachedResources.get('third_party/lighthouse/report/standalone.js'); }, get REPORT_TEMPLATE() { return cachedResources.get('third_party/lighthouse/report-assets/template.html'); diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index 11a3edd279ad..6095ae27bc45 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -170,9 +170,11 @@ class Runner { } // Build report if in local dev env so we don't have to run a watch command. - // TODO: dev checkout only. what to look for? existence of `dist/`? - if (settings.output === 'html') { - await require('../build/build-report.js').buildStandaloneReport(); + if (settings.output === 'html' && !global.isDevtools && !global.isLightrider && + fs.existsSync('dist') && fs.existsSync('.git')) { + // Prevent bundling. + const buildReportPath = '../build/build-report.js'; + await require(buildReportPath).buildStandaloneReport(); } // Create the HTML, JSON, and/or CSV string diff --git a/urls-small.txt b/urls-small.txt new file mode 100644 index 000000000000..7a77bf8de9e9 --- /dev/null +++ b/urls-small.txt @@ -0,0 +1 @@ +http://mobiledtp.charislms.com/login.php \ No newline at end of file diff --git a/urls.txt b/urls.txt new file mode 100644 index 000000000000..0c1737700728 --- /dev/null +++ b/urls.txt @@ -0,0 +1,520 @@ +http://abehiroshi.la.coocan.jp/ +http://best-hashtags.com/ +http://bionit.com.ua/ +http://bluex.in/ +http://brasilista.blogspot.com/2017/08/maiores-pepitas-de-ouro-encontradas-no.html?m=0 +http://c98540qo.beget.tech/ +http://centrallibrary.cit.ac.in/ +http://client.seotool.com/tool/dashboard.cfm +http://design.hire-webdeveloper.com/mobileapptester/v2/about-us.html +http://exocolonist.com/ +http://imf.fuertedevelopers.com/# +http://integranaweb.com.br/novo-site/ +http://jellywp.com/theme/disto/demo/ +http://klausrinkestudio.com/index.html +http://kuafor.me/ +# http://melodic-class.glitch.me/debugger.html +http://mobiledtp.charislms.com/login.php +http://montessori.guru/ +http://nobisnet.dk/kea/10_exam/karen_copenhagen/index.html +http://portal.uib.ac.id:81/ +http://promotiespullen.web2printsoftware.nl/ +http://ratnarajyamavi.edu.np/ +http://staging.getmecab.com/ +http://themonsite.com/ +http://universovigil.com/ +http://wamatex.pl/ +http://webmail.cpanel-box5109.bluehost.com/cpsess4207005569/webmail/bluehost/index.html?login=1&post_login=4721379514539 +http://webtest.services.thron.com/alberto.deagostini/test-seo/examples/ +http://www.asrar-e-deen.com/ +http://www.bewoksushi.com/index.html +http://www.dreamgateway.in/ +http://www.indetec.gob.mx/ +http://www.nicoleehrlich.com/ +http://www.pinkpigworld.com/how-to-fix-hunger-after-workout/ +http://www.pompifresh.com/hemocalm/ +http://www.promotiespullen.com/ +http://www.rainbow-map.us/ +http://www.ucarinc.com/ +http://www.zoomworld.in/demo/yhz/ +http://yargs.js.org/ +http://yveschaput.com/ +https://7up.in/ +https://academy.elice.io/ +https://activecorp.com.br/ +https://agencia.pw/ +https://agentur22.de/ +https://aimementoring.com/ +https://akash-manna.bitbucket.io/ +https://akkikhambhata.wordpress.com/tag/jquery-get-element-value-from-ul-li/ +https://alexandrammr.github.io/blog/10-things-I-love-about-Romania/ +https://allcorp2.aspro-demo.ru/ +https://alrowad.sa/ +https://altbalajifire.firebaseapp.com/ +https://amp.campmor.com/c/s/clothing/womens-clothing/womens-rainwear +https://amradelata.github.io/ +https://angiewoodcreations.com/ +https://animal-noises-d0f7b.firebaseapp.com/index.html +https://aopensolutions.com/ +https://aphome.ru/ +https://app.ft.com/myft/feed +https://app.starbucks.com/menu +https://applynow.peoples.com/openaccount/acs?region=11&externalProductCodes=001#!/acctopening +https://archiveleeds.co.uk/ +https://archivosmercury.com/ +https://artiely.github.io/blog/original/ +https://babyfairytale.com/home +https://bacloud14.github.io/Interactive_Arrays/ +https://bags.sas.se/ +https://bar.utoronto.ca/~asullivan/RNA-Browser/ +https://bargainairticket.com/ +https://beeem.co/p/HU/Pecs/Beeem/FerencfromBeeem +https://belajarpwa-4k93wli64.now.sh/ +https://beldisegno.com/ +https://bestepraxistipps.de/wohnungsbesichtigung-absagen/ +https://bg.eurostrah.com/ +https://biservis.com.tr/ +https://bitbucket.org/ +https://blog.automationxone.com/ +https://blog.level99.mx/ +https://bluealba.com/ +https://brave.com/ +https://browser.sentry-cdn.com/5.9.1/bundle.min.js +https://btphan95.github.io/ +https://bugs.chromium.org/p/chromium/issues/detail?id=1065323 +https://burnjet.com/ +https://bycharlotte.com.au/ +https://candidchronicle.com/ +https://carlosvmpe.github.io/portafolio-angular/#/home +https://cbsedge.com/ +https://cdnportable.com/ +https://cferrari.dev/ +https://chromedevtools.github.io/devtools-protocol/tot/Debugger/#method-setSkipAllPauses +https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2366297 +https://cidu.com.co/ +https://cloud.githubusercontent.com/assets/39191/15980735/86a95e88-2f22-11e6-8000-24a7401e1943.png +https://cloud.githubusercontent.com/assets/39191/21671005/64045bf4-d2cd-11e6-8a01-0e811d2537b3.png +https://cloud.githubusercontent.com/assets/39191/21671031/8c57cc1c-d2cd-11e6-85d0-9cbae2461825.png +https://codereview.chromium.org/2615083002 +https://csc.edu.vn/thiet-ke-website#chuyen-de3~chuyen-vien-thiet-ke-do-hoa-web-131 +https://css-blocks.com/ +https://custerhospitality.com/ +https://dailyoffice.app/ +https://damettoluca.com/ +https://danadidik.id/ +https://dandfplumbing.com/ +https://daringfireball.net/projects/markdown/ +https://data-11fec.firebaseapp.com/ +https://dealsfinders.blog/ +https://deardiary.wtf/ +https://deb.nodesource.com/setup_14.x +https://designrevision.com/demo/shards-dashboards/index.html +https://dev-bongfood-explorer.web.app/home +https://dev-denton.app/wordpress/ +https://dev.dlzpgroup.com/ +https://devclub-kisii.firebaseapp.com/ +https://developers.google.com/machine-learning/glossary/#c +https://developers.google.com/web/tools/lighthouse/ +https://developers.google.com/web/tools/lighthouse/#devtools +https://developers.google.com/web/tools/puppeteer/troubleshooting +https://developers.google.com/web/tools/puppeteer/troubleshooting#tips +https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android?hl=en +https://developers.google.com/web/updates/2015/08/using-manifest-to-set-sitewide-theme-color?hl=en +https://development.talbots.com/on/demandware.store/Sites-talbotsus-Site +https://dijitul.uk/ +https://dinnerbooking.com/dk/en-US/p/press +https://dl.google.com/linux/linux_signing_key.pub +https://dnorton94-helloworld-pwa.glitch.me/ +https://driivz.com/ +https://drm39.ru/spec/tunisia/Kaliningrad/april/?early +https://eip.ceh.ac.uk/hydrology/water-resources/ +https://elgentos.nl/ +https://embeddedt.github.io/BucketGame/ +https://enforcesoftwares.com/ +https://eracreditservices.com/ +https://erfolg-c.ru/ +https://exablaze.com/ +https://example.com/ +https://fall-arrest.com/ +https://findmymobile.samsung.com/#dialog +https://fire.honeywell.com/#/overview/dashboard +https://fosroc.com/?Region=2 +https://foxtailapp.com/ +https://free.test.io/?_ga=2.259134990.589578322.1555874257-2086775186.1555874257 +https://ftnnews.com/ +https://gathern.co/ +https://gatsby-starter-hero-blog.greglobinski.com/ +https://gdgkozhikode.org/ +https://geoknigi.com/ +https://ggstudyabroad.com/ +https://gianguyenglass.vn/ +https://giktar.ru/ +https://glitch.com/edit/#!/speckled-eocursor +https://glitch.com/edit/#!/speckled-eocursor?path=.env:9:0 +https://goaccess.io/download +https://gohealthline.com/ +https://gohealthline.com/32-foods-that-burn-belly-fat-fast/ +https://golftocs.com/ +https://googlechrome.github.io/devtools-samples/debug-js/get-started +https://googlechrome.github.io/lighthouse/viewer/?gist=56e8a72211f5a377e0418c0ae22f1de9 +https://googlechrome.github.io/lighthouse/viewer/?gist=6d3d0224e5ebdaf3b2c787de38a6befa +https://googlecodelabs-your-first-pwapp-110.glitch.me/ +https://googlecodelabs-your-first-pwapp-471.glitch.me/ +https://googlecodelabs-your-first-pwapp-4991.glitch.me/ +https://habr.com/ru/all/page2/ +https://handyytest.bar-at.in/ +https://healthinsurance.benepath.com/health/test.aspx +https://hogventure.com/ +https://hub.docker.com/r/femtopixel/google-lighthouse +https://i.imgur.com/4JjkGgb.png +https://i.imgur.com/DsllMiB.png +https://i.imgur.com/iD2ZBRM.jpg.html +https://icommuse.com/ctcaelite/5/ +https://icommuse.com/ctcaelite/5/#/tab/home +https://images-eu.ssl-images-amazon.com/images/G/01/AUIClients/ClientSideMetricsAUIJavascript@jserrorsForester.10f2559e93ec589d92509318a7e2acbac74c343a._V2_.js +https://indonesian-online.com/ +https://ismaeljdz7.com/ +https://isweb.iata.org/ +https://jamstack.wtf/ +https://jimgerland.com/ +https://jionews.com/ +https://joeybabcock.me/blog/ +https://josephakayesi.com/ +https://jsbin.com/ +https://kangzhiqing.com/ +https://keralabookstore.com/new-books.do +https://kindly-tiny-toy.glitch.me/ +https://kolhoz.gold/ +https://kpr.online/take-over-kpr-bank-syariah-mandiri/ +https://kukadi.newsoftprojects.com/index.php +https://kumbier.it/ +https://kursjs.pl/kurs/debuger/debuger.php +https://kursjs.pl/kurs/debuger/debuger.php#zakladka-audits +https://learn.javascript.ru/ +https://learn.simbibot.com/ +https://learning-templates.com/ +https://letshang-app-v000.appspot.com/ +https://lifetoolsdigital.com/5days/ +https://lonelycpp.github.io/ +https://lootess.com/ +https://lovangi.ru/ +https://lp.the123.co.il/sp/licoplus123/ +https://m-g-shahriar.github.io/portfolio/ +https://m.douban.com/ +https://m.media-amazon.com/images/G/01/csm/showads.v2.js +https://m.mishifeng.com/city +https://mapy.cz/zakladni?x=14.4007705&y=50.0713520&z=11 +https://marcoc76.github.io/calificaciones/ +https://marriedgames.com.br/ +https://mdalatam.university/ +https://me.fathomapp.com/10-test +https://mecasualty.com/ +https://mentors.codingcoach.io/?language=html +https://minigames.mail.ru/biljard_devyatka +https://mnlegal.net/ +https://mp.weixin.qq.com/s?__biz=MjM5MjAxNDM4MA==&mid=414293326&idx=1&sn=3d95dfb8e6bc4219a5714fbce89a8fc4&mpshare=1&scene=23&srcid=02245KJvVrwF3BVaUjhBMx9r#rd +https://mp.weixin.qq.com/s/L1xHLd-gKDsmi0lnSmm6pQ +https://mru.org/ +https://mws-eka.firebaseapp.com/ +https://myjoshem.github.io/assignments/weathersite/gallery.html +https://myw.natiki.net.au/ +https://nandomaciel.com/ +https://netk5.com.cn/ +https://newspakistan.tv/ +https://nicholasbering.ca/tools/2016/10/09/devtools-disable-caching/ +https://nicholasireland.ca/ +https://nigel-aves-photography.com/ +https://nmsnewhaven.org/ +https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode +https://nutrifacts-app.firebaseapp.com/search +https://oautah.org/ +https://ocioalicante.net/ +https://onohughes.com/ +https://oodle-demo.firebaseapp.com/#/ +https://osteklenie812.ru/blog/moskitnie-setki-okna +https://ovpv.me/blog/ +https://password-generator.kamilnowak.com/ +https://payplus.secure-solutions4.biz/dev_andy003/ezWeb360Mobile/ +https://pdfbooksfree.pk/ +https://pedrosa-andre.github.io/assignments/templesite/home.html +https://personalinjuryclaimsservice.com/ +https://phaser-planes.glitch.me/ +https://physicaltherapyweb.com/ +https://piao.fr/2019/04/tatoueur-de-talent-le-travail-de-pierre-nous-a-bluffe/ +https://poly.nomial.co.uk/ +https://praveenpal4232.github.io/ +https://preactjs.com/ +https://prepaidcompare.net/ +https://prus.dev/ +https://punyachatterjee.com/ +https://pushdemo-f7986.firebaseapp.com/selfietravel/login +https://qa2.peoples.com/openaccount/acs?region=2&externalProductCodes=001 +https://quickbooks.intuit.com/oicms/accounting-copy/ +https://quickbooks.intuit.com/oicms/accounting/oip-accounting-sales-tax-copy/ +https://rachio.com/ +https://rburkitt.github.io/BlazorApplication2/ +https://realisticdildos.com/ +https://renato66.github.io/ +https://retail.onlinesbi.com/ +https://reviewsinscope.com/ +https://rightwords.ro/ +https://runner.africa/admin/app/web/index.php +https://sa2019.siggraph.org/ +https://sanjaytandon.in/ +https://sapepp.mawared.qa/irj/portal +https://sarkariresultz.in/kptcl-recruitment/ +https://saverl.com/ +https://shivam-verma9999.github.io/sliding-tiles-puzzle/ +https://shodipoayomide.com/ +https://silelinhchi.com/ +https://simplybearings.co.uk/shop/advanced_search_result.php?search_in_description=1&keywords=0.75X1.25X0.25_R23&search_inactive=on +https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-perfLoggingPrefs-object +https://sites.google.com/view/education-inc/home +https://smartfinancial.com/ +https://solopacker.artoon.in/login +https://sproboticworks.com/user/home +https://stackexchange.com/ +https://stackoverflow.com/questions/18455644/programmatically-get-memory-usage-in-chrome +https://stackoverflow.com/questions/37494330/display-none-in-a-for-loop-and-its-affect-on-reflow +https://stackoverflow.com/questions/tagged/node.js +https://staging.digitary.net/ +https://staging.suttacentral.net/ +https://static.fsf.org/fsforg/rss/blogs.xml +https://static.wixstatic.com/media/5298cb_2b526f7f59e349aca6cb6ed92cf3956d~mv2.png/v1/crop/x_49,y_0,w_399,h_502/fill/w_350,h_440,al_c,q_80,usm_0.66_1.00_0.01/3.webp +https://studiodentisticolamera.it/ +https://stylesnbags.com/ +https://sudsy-crush.glitch.me/ +https://summarizer.legalmind.tech/ +https://surge.sh/ +https://suzaanlainglaw.com/ +https://t.ayaya.id/#/home +https://taslim.me/ +https://template-page.firebaseapp.com/ +https://testpwa-8ea96.firebaseapp.com/ +https://thanosjs.org/?fbclid=IwAR16WOtqUeVaVeyDa-FZ3Q1aK-Q-2lWjYQcwx7gyVjkq75SG6Nv553NK1C0 +https://therapyplayground.com/ +https://ticketphone.com.br/site/ +https://timable.com/ +https://timesheets.esyasoft.com/ +https://timlive.tim.com.br/ +https://token.mduc.xyz/ +https://trello.com/b/yl0WWJQN/srs +https://trenton-telge.github.io/MediView/ +https://truthinmydays.com/ +https://uhrengrosshandelweb.de/home-mobile/ +https://unitedbyblue.com/ +https://unleashed-design.de/#/ +https://upcreativ.com/ +https://vaticatest.vaticahealth-labs.com/VaticaHealth.Web.UI/#/home/provider/1113 +https://vcetrainer.com/ +https://vehigrupo.com/ +https://vivodeltrading.com/ +https://vladipur.by/pur-klej-dlya-proizvodstva-pokrytij-iz-rezinovoj-kroshki/ +https://volaresystems.com/ +https://vue-agric.firebaseapp.com/ +https://watch.sling.com/watch?channelId=261371704fd24f8fb9f29e483ba45062 +https://wcmountainrealty.com/default.asp?content=expanded&search_content=results&this_format=1&mls_number=26011412&page=1&query_id=182715670&sortby=1 +https://web.dev/measure/ +https://web.gencat.cat/ca/inici/ +https://webeventconsole.com/AutocastProNew/Page/Pagebuilder?Eventid=testemail +https://weboas.is/ +https://wfuneradoc-1.web.app/index.html +https://wiki.rookie-inc.com/serverapps/security/ssh +https://wildberries.ua/catalog?category=366&sort=popular +https://wildhunt.org/ +https://wilrotours.co.za/services/ +https://womantalk.com/ +https://wordpress.com/block-editor/post/eltotowers679771852.wordpress.com/132 +https://wwos.nine.com.au/motorsport/live-scores/motogp-circuit-of-the-americas-2019/motogp-3-2019-0 +https://www.aciaalfenas.com.br/home +https://www.adamlowecreative.com/headshots/ +https://www.ahmad-ali.co.uk/ +https://www.airbnb.com.br/ +https://www.alexandersparks.com/ +https://www.amazon.com/ +https://www.amazon.fr/ +https://www.amrutam.co.in/ +https://www.anthem.com/ +https://www.apostillecanada.org/ +https://www.artistictile.com/ +https://www.asaudeonline.com.br/ +https://www.ashware.nl/ +https://www.atlantisbahamas.com/ +https://www.attra.com/ +https://www.avalara.com/us/en/index.html +https://www.awwwards.com/ +https://www.backmeup.co.uk/ +https://www.bandlab.com/mix-editor +https://www.belk.com/women/ +https://www.berfect.de/ +https://www.bigbreaks.com/ +https://www.bio-rad-antibodies.com/ +https://www.blbrokers.com/ +https://www.bmu.edu.in/ +https://www.bookxcessonline.com/ +https://www.boticario.com.br/ +https://www.brick-a-brack.com/ +https://www.broadcom.com/ +https://www.careerguide.com/career/working-professionals/10-highest-paying-jobs-for-commerce-students +https://www.carwale.com/new/best-cars-under-15-lakh-in-india/ +https://www.celebsgo.com/ +https://www.chetu.com/ +https://www.chromatix.com.au/ +https://www.chromium.org/developers/bisect-builds-py +https://www.chronoshop2shop.fr/fr +https://www.clouty.ru/ +https://www.clublr.gr/ +https://www.codecademy.com/pro/membership +https://www.coleparmer.com/ +https://www.consumerfinance.gov/about-us/blog/economic-impact-payment-prepaid-card/ +https://www.correio24horas.com.br/noticia/nid/casos-de-covid-19-devem-se-expandir-em-sp-ate-2021-diz-butantan/ +https://www.cort.com/ +https://www.countystonegranite.co.uk/ +https://www.cybermiles.io/en-us/ +https://www.damacproperties.com/ +https://www.deakin.edu.au/ +https://www.dejongintra.nl/ +https://www.deque.com/axe/ +https://www.derby.ac.uk/ +https://www.donatekart.com/ +https://www.drlucianopellegrino.com.br/ +https://www.drsampaioplastica.com.br/ +https://www.e2language.com/ +https://www.easywayphotography.com.au/how-it-works/ +https://www.ebay.com/ +https://www.ecoconcepts.co/ +https://www.edgeclothing.com.au/collections/womens-pants +https://www.elitesingles.com/ +https://www.elliothospital.org/website/urgent-care-bedford.php +https://www.eon.com/en.html +https://www.eq-love.com/fr/ +https://www.eyebuydirect.com/eyeglasses/women +https://www.fashionworld.co.uk/ +https://www.flenco.in/ +https://www.flipkart.com/ +https://www.floweraura.com/ +https://www.foo.software/monitoring-page-experience-with-pagespeed-insights-api-and-lighthouse/ +https://www.framalhocorretor.com.br/ +https://www.freehosting.com/tos.html +https://www.freizi.at/ +https://www.frioval.cl/ +https://www.ft.com/ +https://www.gemini-us.com/ +https://www.glosarioit.com/ +https://www.goldenplanet.dk/ +https://www.google.com/ +https://www.google.com/_/chrome/newtab?ie=UTF-8 +https://www.google.com/_/chrome/newtab?rlz=1C1CHBD_esES751ES751&ie=UTF-8 +https://www.google.com/search?q=hartabumi&oq=harta&aqs=chrome.5.69i57j69i60l4j69i59.4861j0j8&sourceid=chrome&ie=UTF-8 +https://www.google.es/ +https://www.graphitas.co.uk/ +https://www.gstatic.com/mobilesdk/190114_mobilesdk/bootanim-sprite.png +https://www.gundtoft.dk/ +https://www.gupshup.io/developer/docs/bot-platform/guide/intro-to-gupshup-bot-builder +https://www.guthriebowron.co.nz/ +https://www.hamilecarsi.com/ +https://www.hastayataklari.co/ +https://www.holidify.com/places/tehri-garhwal/photos.html +https://www.homeworkmarket.com/users/charandry?page=2 +https://www.htmracing.it/ +https://www.iiad.edu.in/ +https://www.imovies.cc/movies/12227/Le-Trou/RUS/HIGH +https://www.indiatvnews.com/elections +https://www.iontocentre.com/ +https://www.itriangle.in/ +https://www.jako-o.com/de_DE +https://www.jasta5.org/ +https://www.jonniegrieve.co.uk/ +https://www.joseluisandrade.com/ +https://www.jungelinke.at/ +https://www.justacote.com/paris-75015/restaurant/le-quinzieme-53422.htm +https://www.justeat.it/ +https://www.kcholidays.in/ +https://www.kearsleys.com/ +https://www.kenamobile.it/ +https://www.kentuckymathematics.org/ +https://www.kidsgo.de/ +https://www.kitchenaid.com.co/ksm150psmy/p +https://www.koningkaart.nl/ +https://www.kttape.com/ +https://www.kyrillrapp.com/ +https://www.logycacolabora.com/ +https://www.makello.com/ +https://www.mcafee.com/consumer/en-us_multi/spromos/aff/l1079/DEV0000/PN0014/ST0019.html?clickid=TYPQ6SwauxyJRft0MSU5wRc5UklxYrXJrQNkyA0&lqmcat=Affiliate:IR:null:316166:10813:10813:null&sharedid=299463 +https://www.mediaplayer10.com/ +https://www.medlife.com/ +https://www.meetmatch.biz/ +https://www.milforded.org/staff-clone +https://www.montirbox.com/?m=1 +https://www.mudanzasmitre.com/ +https://www.muhammadshoaib.me/ +https://www.mysense.com.my/ +https://www.naqshi.com/ +https://www.news18.com/ +https://www.noom.com/#/ +https://www.npmjs.com/package/which +https://www.nsowo.com/16624/%d8%a3%d9%81%d8%b6%d9%84-%d9%88%d8%b8%d8%a7%d8%a6%d9%81-%d9%81%d9%8a-%d8%a7%d9%84%d8%b9%d8%a7%d9%84%d9%85-%d9%88%d8%a7%d9%84%d8%a3%d8%b9%d9%84%d9%89-%d8%b1%d8%a7%d8%aa%d8%a8%d8%a7/ +https://www.nsowo.com/30876/%D8%A3%D8%B3%D9%85%D8%A7%D8%A1-%D8%A8%D9%86%D8%A7%D8%AA-%D9%85%D8%B9-%D8%A7%D9%84%D8%B5%D9%81%D8%A7%D8%AA-%D9%84%D9%83%D9%84-%D8%A7%D8%B3%D9%85-%D9%81%D8%AA%D8%A7%D8%A9/ +https://www.observepoint.com/ +https://www.officedepot.com/configurator/create-on-demand/#/product/poster/1041 +https://www.oilandgasjobsearch.com/ +https://www.olx.in/ +https://www.openstudio.one/ +https://www.origin.io/ +https://www.ourkyj.com/#/ +https://www.phixer.net/ +https://www.pioneerbricks.com/ +https://www.pioneerelectronics.com/PUSA/Car/DVD+Receivers +https://www.pragtech.co.in/ +https://www.psm.org.ph/guides-notes-faqs/ +https://www.redfin.com/NY/Deer-Park/214-Old-Country-Rd-11729/home/21430946 +https://www.reesby.com.au/ +https://www.resultadofacil.com.br/ +https://www.roblox.com/games?SortFilter=6&TimeFilter=0 +https://www.royeinteriors.com/ +https://www.saatchiart.com/artadvisory +https://www.safestyle-windows.co.uk/ +https://www.sce.com/ +https://www.schekino.net/news/zhiteli-goroda-shhekino-podali-kollektivnuyu-zhalobu-na-upravlyayushhuyu-kompaniyu-everest/ +https://www.sdsgroup.ro/ +https://www.sellsa.co.za/ +https://www.sitespeed.io/ +https://www.smarterhealth.id/5-rumah-sakit-malaysia-pilihan-pasien-indonesia/ +https://www.solactive.com/ +https://www.stage-gate.la/ +https://www.startech.com.bd/ +https://www.stenaline.se/till-danmark +https://www.stylecraze.com/articles/eyelash-extensions-guide/ +https://www.tawuniya.com.sa/ +https://www.tekscan.com/ +https://www.thelallantop.com/news/ +https://www.tiffany.com/ +https://www.tpkha.com/ +https://www.trekksoft.com/ +https://www.tributemedia.com/ +https://www.uncappedfibre.co.za/telkom/openserve?fbclid=IwAR3mmZzaalR7eWrObcYi3LAagagaocBzroJdAj0DYe4AVwkci0EjdtLnwl4 +https://www.uptownshots.com/ +https://www.valdevivo.com/ +https://www.verivox.de/ +https://www.vsointernational.org/ +https://www.w3schools.com/ +https://www.wannaexpresso.com/2021/02/19/m1-macbook-rbenv/ +https://www.wannaexpresso.com/2021/02/20/m1-macbook-minecraft/ +https://www.webpagetest.org//results.php?test=191102_N6_eed705551506349bd8674d5742195212 +https://www.webpagetest.org//results.php?test=191102_WJ_6eb1d4826d72f71ad8148fe4bc2411ae +https://www.wikimass.com/c/biggest-of-three-numbers +https://www.woolaroc.org/become-a-member +https://www.worldrace.org/ +https://www.worthless-stuff.com/ +https://www.yardhopping.com/ +https://www.zenlayer.com/# +https://x2pos.com/ +https://xpos-master.firebaseapp.com/portal +https://yabbermarketing.com/ +https://yenzsocks.com/pages/our-socks +https://yhotie.com/ +https://youngandtheinvested.com/ +https://zaimy24.com.ua/ +https://zawajco.blogspot.com/2019/01/Girls-Photo-numbers-Sons-of-the-UAE.html?m=1 +https://zionmedicinals.com/ +https://zoihospitals.com/ \ No newline at end of file From c745cbdb2fb023e01f76e33d500a5db8ea6c0c7b Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 23 Jun 2021 12:47:48 -0700 Subject: [PATCH 07/71] remove some changes --- .../report/html/html-report-assets.js | 42 +- lighthouse-core/report/report-generator.js | 8 - urls-small.txt | 1 - urls.txt | 520 ------------------ 4 files changed, 17 insertions(+), 554 deletions(-) delete mode 100644 urls-small.txt delete mode 100644 urls.txt diff --git a/lighthouse-core/report/html/html-report-assets.js b/lighthouse-core/report/html/html-report-assets.js index c0b387c4b87a..af2ef7c8afcb 100644 --- a/lighthouse-core/report/html/html-report-assets.js +++ b/lighthouse-core/report/html/html-report-assets.js @@ -8,30 +8,23 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8'); -const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/renderer/generated/standalone.js', 'utf8'); - -/* eslint-disable max-len */ -const REPORT_JAVASCRIPT_MODULES = { - // './logger.js': fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - // './i18n.js': fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - // './text-encoding.js': fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), - // './util.js': fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - // './dom.js': fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - // './crc-details-renderer.js': fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - // './snippet-renderer.js': fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - // './element-screenshot-renderer.js': fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - // './category-renderer.js': fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - // './performance-category-renderer.js': fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - // './pwa-category-renderer.js': fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - // './details-renderer.js': fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - // '../../../lib/file-namer.js': fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), - // './file-namer.js': fs.readFileSync(__dirname + '/renderer/file-namer.js', 'utf8'), - // './report-ui-features.js': fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - // './report-renderer.js': fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - // './main.js': fs.readFileSync(__dirname + '/renderer/main.js', 'utf8'), -}; -/* eslint-enable max-len */ - +const REPORT_JAVASCRIPT = [ + fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), +].join(';\n'); const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8'); @@ -41,6 +34,5 @@ module.exports = { REPORT_TEMPLATE, REPORT_TEMPLATES, REPORT_JAVASCRIPT, - REPORT_JAVASCRIPT_MODULES, REPORT_CSS, }; diff --git a/lighthouse-core/report/report-generator.js b/lighthouse-core/report/report-generator.js index 736c4bc61c52..9996be60f9ea 100644 --- a/lighthouse-core/report/report-generator.js +++ b/lighthouse-core/report/report-generator.js @@ -39,17 +39,9 @@ class ReportGenerator { .replace(/\u2029/g, '\\u2029'); // replaces paragraph separators const sanitizedJavascript = htmlReportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/'); - // let sanitizedJavascriptModules = ''; - // for (const [id, code] of Object.entries(htmlReportAssets.REPORT_JAVASCRIPT_MODULES)) { - // const sanitizedCode = code.replace(/<\//g, '\\u003c/'); - // sanitizedJavascriptModules += - // ``; - // } - return ReportGenerator.replaceStrings(htmlReportAssets.REPORT_TEMPLATE, [ {search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson}, {search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: sanitizedJavascript}, - // {search: '%%LIGHTHOUSE_JAVASCRIPT_MODULES%%', replacement: sanitizedJavascriptModules}, {search: '/*%%LIGHTHOUSE_CSS%%*/', replacement: htmlReportAssets.REPORT_CSS}, {search: '%%LIGHTHOUSE_TEMPLATES%%', replacement: htmlReportAssets.REPORT_TEMPLATES}, ]); diff --git a/urls-small.txt b/urls-small.txt deleted file mode 100644 index 7a77bf8de9e9..000000000000 --- a/urls-small.txt +++ /dev/null @@ -1 +0,0 @@ -http://mobiledtp.charislms.com/login.php \ No newline at end of file diff --git a/urls.txt b/urls.txt deleted file mode 100644 index 0c1737700728..000000000000 --- a/urls.txt +++ /dev/null @@ -1,520 +0,0 @@ -http://abehiroshi.la.coocan.jp/ -http://best-hashtags.com/ -http://bionit.com.ua/ -http://bluex.in/ -http://brasilista.blogspot.com/2017/08/maiores-pepitas-de-ouro-encontradas-no.html?m=0 -http://c98540qo.beget.tech/ -http://centrallibrary.cit.ac.in/ -http://client.seotool.com/tool/dashboard.cfm -http://design.hire-webdeveloper.com/mobileapptester/v2/about-us.html -http://exocolonist.com/ -http://imf.fuertedevelopers.com/# -http://integranaweb.com.br/novo-site/ -http://jellywp.com/theme/disto/demo/ -http://klausrinkestudio.com/index.html -http://kuafor.me/ -# http://melodic-class.glitch.me/debugger.html -http://mobiledtp.charislms.com/login.php -http://montessori.guru/ -http://nobisnet.dk/kea/10_exam/karen_copenhagen/index.html -http://portal.uib.ac.id:81/ -http://promotiespullen.web2printsoftware.nl/ -http://ratnarajyamavi.edu.np/ -http://staging.getmecab.com/ -http://themonsite.com/ -http://universovigil.com/ -http://wamatex.pl/ -http://webmail.cpanel-box5109.bluehost.com/cpsess4207005569/webmail/bluehost/index.html?login=1&post_login=4721379514539 -http://webtest.services.thron.com/alberto.deagostini/test-seo/examples/ -http://www.asrar-e-deen.com/ -http://www.bewoksushi.com/index.html -http://www.dreamgateway.in/ -http://www.indetec.gob.mx/ -http://www.nicoleehrlich.com/ -http://www.pinkpigworld.com/how-to-fix-hunger-after-workout/ -http://www.pompifresh.com/hemocalm/ -http://www.promotiespullen.com/ -http://www.rainbow-map.us/ -http://www.ucarinc.com/ -http://www.zoomworld.in/demo/yhz/ -http://yargs.js.org/ -http://yveschaput.com/ -https://7up.in/ -https://academy.elice.io/ -https://activecorp.com.br/ -https://agencia.pw/ -https://agentur22.de/ -https://aimementoring.com/ -https://akash-manna.bitbucket.io/ -https://akkikhambhata.wordpress.com/tag/jquery-get-element-value-from-ul-li/ -https://alexandrammr.github.io/blog/10-things-I-love-about-Romania/ -https://allcorp2.aspro-demo.ru/ -https://alrowad.sa/ -https://altbalajifire.firebaseapp.com/ -https://amp.campmor.com/c/s/clothing/womens-clothing/womens-rainwear -https://amradelata.github.io/ -https://angiewoodcreations.com/ -https://animal-noises-d0f7b.firebaseapp.com/index.html -https://aopensolutions.com/ -https://aphome.ru/ -https://app.ft.com/myft/feed -https://app.starbucks.com/menu -https://applynow.peoples.com/openaccount/acs?region=11&externalProductCodes=001#!/acctopening -https://archiveleeds.co.uk/ -https://archivosmercury.com/ -https://artiely.github.io/blog/original/ -https://babyfairytale.com/home -https://bacloud14.github.io/Interactive_Arrays/ -https://bags.sas.se/ -https://bar.utoronto.ca/~asullivan/RNA-Browser/ -https://bargainairticket.com/ -https://beeem.co/p/HU/Pecs/Beeem/FerencfromBeeem -https://belajarpwa-4k93wli64.now.sh/ -https://beldisegno.com/ -https://bestepraxistipps.de/wohnungsbesichtigung-absagen/ -https://bg.eurostrah.com/ -https://biservis.com.tr/ -https://bitbucket.org/ -https://blog.automationxone.com/ -https://blog.level99.mx/ -https://bluealba.com/ -https://brave.com/ -https://browser.sentry-cdn.com/5.9.1/bundle.min.js -https://btphan95.github.io/ -https://bugs.chromium.org/p/chromium/issues/detail?id=1065323 -https://burnjet.com/ -https://bycharlotte.com.au/ -https://candidchronicle.com/ -https://carlosvmpe.github.io/portafolio-angular/#/home -https://cbsedge.com/ -https://cdnportable.com/ -https://cferrari.dev/ -https://chromedevtools.github.io/devtools-protocol/tot/Debugger/#method-setSkipAllPauses -https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2366297 -https://cidu.com.co/ -https://cloud.githubusercontent.com/assets/39191/15980735/86a95e88-2f22-11e6-8000-24a7401e1943.png -https://cloud.githubusercontent.com/assets/39191/21671005/64045bf4-d2cd-11e6-8a01-0e811d2537b3.png -https://cloud.githubusercontent.com/assets/39191/21671031/8c57cc1c-d2cd-11e6-85d0-9cbae2461825.png -https://codereview.chromium.org/2615083002 -https://csc.edu.vn/thiet-ke-website#chuyen-de3~chuyen-vien-thiet-ke-do-hoa-web-131 -https://css-blocks.com/ -https://custerhospitality.com/ -https://dailyoffice.app/ -https://damettoluca.com/ -https://danadidik.id/ -https://dandfplumbing.com/ -https://daringfireball.net/projects/markdown/ -https://data-11fec.firebaseapp.com/ -https://dealsfinders.blog/ -https://deardiary.wtf/ -https://deb.nodesource.com/setup_14.x -https://designrevision.com/demo/shards-dashboards/index.html -https://dev-bongfood-explorer.web.app/home -https://dev-denton.app/wordpress/ -https://dev.dlzpgroup.com/ -https://devclub-kisii.firebaseapp.com/ -https://developers.google.com/machine-learning/glossary/#c -https://developers.google.com/web/tools/lighthouse/ -https://developers.google.com/web/tools/lighthouse/#devtools -https://developers.google.com/web/tools/puppeteer/troubleshooting -https://developers.google.com/web/tools/puppeteer/troubleshooting#tips -https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android?hl=en -https://developers.google.com/web/updates/2015/08/using-manifest-to-set-sitewide-theme-color?hl=en -https://development.talbots.com/on/demandware.store/Sites-talbotsus-Site -https://dijitul.uk/ -https://dinnerbooking.com/dk/en-US/p/press -https://dl.google.com/linux/linux_signing_key.pub -https://dnorton94-helloworld-pwa.glitch.me/ -https://driivz.com/ -https://drm39.ru/spec/tunisia/Kaliningrad/april/?early -https://eip.ceh.ac.uk/hydrology/water-resources/ -https://elgentos.nl/ -https://embeddedt.github.io/BucketGame/ -https://enforcesoftwares.com/ -https://eracreditservices.com/ -https://erfolg-c.ru/ -https://exablaze.com/ -https://example.com/ -https://fall-arrest.com/ -https://findmymobile.samsung.com/#dialog -https://fire.honeywell.com/#/overview/dashboard -https://fosroc.com/?Region=2 -https://foxtailapp.com/ -https://free.test.io/?_ga=2.259134990.589578322.1555874257-2086775186.1555874257 -https://ftnnews.com/ -https://gathern.co/ -https://gatsby-starter-hero-blog.greglobinski.com/ -https://gdgkozhikode.org/ -https://geoknigi.com/ -https://ggstudyabroad.com/ -https://gianguyenglass.vn/ -https://giktar.ru/ -https://glitch.com/edit/#!/speckled-eocursor -https://glitch.com/edit/#!/speckled-eocursor?path=.env:9:0 -https://goaccess.io/download -https://gohealthline.com/ -https://gohealthline.com/32-foods-that-burn-belly-fat-fast/ -https://golftocs.com/ -https://googlechrome.github.io/devtools-samples/debug-js/get-started -https://googlechrome.github.io/lighthouse/viewer/?gist=56e8a72211f5a377e0418c0ae22f1de9 -https://googlechrome.github.io/lighthouse/viewer/?gist=6d3d0224e5ebdaf3b2c787de38a6befa -https://googlecodelabs-your-first-pwapp-110.glitch.me/ -https://googlecodelabs-your-first-pwapp-471.glitch.me/ -https://googlecodelabs-your-first-pwapp-4991.glitch.me/ -https://habr.com/ru/all/page2/ -https://handyytest.bar-at.in/ -https://healthinsurance.benepath.com/health/test.aspx -https://hogventure.com/ -https://hub.docker.com/r/femtopixel/google-lighthouse -https://i.imgur.com/4JjkGgb.png -https://i.imgur.com/DsllMiB.png -https://i.imgur.com/iD2ZBRM.jpg.html -https://icommuse.com/ctcaelite/5/ -https://icommuse.com/ctcaelite/5/#/tab/home -https://images-eu.ssl-images-amazon.com/images/G/01/AUIClients/ClientSideMetricsAUIJavascript@jserrorsForester.10f2559e93ec589d92509318a7e2acbac74c343a._V2_.js -https://indonesian-online.com/ -https://ismaeljdz7.com/ -https://isweb.iata.org/ -https://jamstack.wtf/ -https://jimgerland.com/ -https://jionews.com/ -https://joeybabcock.me/blog/ -https://josephakayesi.com/ -https://jsbin.com/ -https://kangzhiqing.com/ -https://keralabookstore.com/new-books.do -https://kindly-tiny-toy.glitch.me/ -https://kolhoz.gold/ -https://kpr.online/take-over-kpr-bank-syariah-mandiri/ -https://kukadi.newsoftprojects.com/index.php -https://kumbier.it/ -https://kursjs.pl/kurs/debuger/debuger.php -https://kursjs.pl/kurs/debuger/debuger.php#zakladka-audits -https://learn.javascript.ru/ -https://learn.simbibot.com/ -https://learning-templates.com/ -https://letshang-app-v000.appspot.com/ -https://lifetoolsdigital.com/5days/ -https://lonelycpp.github.io/ -https://lootess.com/ -https://lovangi.ru/ -https://lp.the123.co.il/sp/licoplus123/ -https://m-g-shahriar.github.io/portfolio/ -https://m.douban.com/ -https://m.media-amazon.com/images/G/01/csm/showads.v2.js -https://m.mishifeng.com/city -https://mapy.cz/zakladni?x=14.4007705&y=50.0713520&z=11 -https://marcoc76.github.io/calificaciones/ -https://marriedgames.com.br/ -https://mdalatam.university/ -https://me.fathomapp.com/10-test -https://mecasualty.com/ -https://mentors.codingcoach.io/?language=html -https://minigames.mail.ru/biljard_devyatka -https://mnlegal.net/ -https://mp.weixin.qq.com/s?__biz=MjM5MjAxNDM4MA==&mid=414293326&idx=1&sn=3d95dfb8e6bc4219a5714fbce89a8fc4&mpshare=1&scene=23&srcid=02245KJvVrwF3BVaUjhBMx9r#rd -https://mp.weixin.qq.com/s/L1xHLd-gKDsmi0lnSmm6pQ -https://mru.org/ -https://mws-eka.firebaseapp.com/ -https://myjoshem.github.io/assignments/weathersite/gallery.html -https://myw.natiki.net.au/ -https://nandomaciel.com/ -https://netk5.com.cn/ -https://newspakistan.tv/ -https://nicholasbering.ca/tools/2016/10/09/devtools-disable-caching/ -https://nicholasireland.ca/ -https://nigel-aves-photography.com/ -https://nmsnewhaven.org/ -https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode -https://nutrifacts-app.firebaseapp.com/search -https://oautah.org/ -https://ocioalicante.net/ -https://onohughes.com/ -https://oodle-demo.firebaseapp.com/#/ -https://osteklenie812.ru/blog/moskitnie-setki-okna -https://ovpv.me/blog/ -https://password-generator.kamilnowak.com/ -https://payplus.secure-solutions4.biz/dev_andy003/ezWeb360Mobile/ -https://pdfbooksfree.pk/ -https://pedrosa-andre.github.io/assignments/templesite/home.html -https://personalinjuryclaimsservice.com/ -https://phaser-planes.glitch.me/ -https://physicaltherapyweb.com/ -https://piao.fr/2019/04/tatoueur-de-talent-le-travail-de-pierre-nous-a-bluffe/ -https://poly.nomial.co.uk/ -https://praveenpal4232.github.io/ -https://preactjs.com/ -https://prepaidcompare.net/ -https://prus.dev/ -https://punyachatterjee.com/ -https://pushdemo-f7986.firebaseapp.com/selfietravel/login -https://qa2.peoples.com/openaccount/acs?region=2&externalProductCodes=001 -https://quickbooks.intuit.com/oicms/accounting-copy/ -https://quickbooks.intuit.com/oicms/accounting/oip-accounting-sales-tax-copy/ -https://rachio.com/ -https://rburkitt.github.io/BlazorApplication2/ -https://realisticdildos.com/ -https://renato66.github.io/ -https://retail.onlinesbi.com/ -https://reviewsinscope.com/ -https://rightwords.ro/ -https://runner.africa/admin/app/web/index.php -https://sa2019.siggraph.org/ -https://sanjaytandon.in/ -https://sapepp.mawared.qa/irj/portal -https://sarkariresultz.in/kptcl-recruitment/ -https://saverl.com/ -https://shivam-verma9999.github.io/sliding-tiles-puzzle/ -https://shodipoayomide.com/ -https://silelinhchi.com/ -https://simplybearings.co.uk/shop/advanced_search_result.php?search_in_description=1&keywords=0.75X1.25X0.25_R23&search_inactive=on -https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-perfLoggingPrefs-object -https://sites.google.com/view/education-inc/home -https://smartfinancial.com/ -https://solopacker.artoon.in/login -https://sproboticworks.com/user/home -https://stackexchange.com/ -https://stackoverflow.com/questions/18455644/programmatically-get-memory-usage-in-chrome -https://stackoverflow.com/questions/37494330/display-none-in-a-for-loop-and-its-affect-on-reflow -https://stackoverflow.com/questions/tagged/node.js -https://staging.digitary.net/ -https://staging.suttacentral.net/ -https://static.fsf.org/fsforg/rss/blogs.xml -https://static.wixstatic.com/media/5298cb_2b526f7f59e349aca6cb6ed92cf3956d~mv2.png/v1/crop/x_49,y_0,w_399,h_502/fill/w_350,h_440,al_c,q_80,usm_0.66_1.00_0.01/3.webp -https://studiodentisticolamera.it/ -https://stylesnbags.com/ -https://sudsy-crush.glitch.me/ -https://summarizer.legalmind.tech/ -https://surge.sh/ -https://suzaanlainglaw.com/ -https://t.ayaya.id/#/home -https://taslim.me/ -https://template-page.firebaseapp.com/ -https://testpwa-8ea96.firebaseapp.com/ -https://thanosjs.org/?fbclid=IwAR16WOtqUeVaVeyDa-FZ3Q1aK-Q-2lWjYQcwx7gyVjkq75SG6Nv553NK1C0 -https://therapyplayground.com/ -https://ticketphone.com.br/site/ -https://timable.com/ -https://timesheets.esyasoft.com/ -https://timlive.tim.com.br/ -https://token.mduc.xyz/ -https://trello.com/b/yl0WWJQN/srs -https://trenton-telge.github.io/MediView/ -https://truthinmydays.com/ -https://uhrengrosshandelweb.de/home-mobile/ -https://unitedbyblue.com/ -https://unleashed-design.de/#/ -https://upcreativ.com/ -https://vaticatest.vaticahealth-labs.com/VaticaHealth.Web.UI/#/home/provider/1113 -https://vcetrainer.com/ -https://vehigrupo.com/ -https://vivodeltrading.com/ -https://vladipur.by/pur-klej-dlya-proizvodstva-pokrytij-iz-rezinovoj-kroshki/ -https://volaresystems.com/ -https://vue-agric.firebaseapp.com/ -https://watch.sling.com/watch?channelId=261371704fd24f8fb9f29e483ba45062 -https://wcmountainrealty.com/default.asp?content=expanded&search_content=results&this_format=1&mls_number=26011412&page=1&query_id=182715670&sortby=1 -https://web.dev/measure/ -https://web.gencat.cat/ca/inici/ -https://webeventconsole.com/AutocastProNew/Page/Pagebuilder?Eventid=testemail -https://weboas.is/ -https://wfuneradoc-1.web.app/index.html -https://wiki.rookie-inc.com/serverapps/security/ssh -https://wildberries.ua/catalog?category=366&sort=popular -https://wildhunt.org/ -https://wilrotours.co.za/services/ -https://womantalk.com/ -https://wordpress.com/block-editor/post/eltotowers679771852.wordpress.com/132 -https://wwos.nine.com.au/motorsport/live-scores/motogp-circuit-of-the-americas-2019/motogp-3-2019-0 -https://www.aciaalfenas.com.br/home -https://www.adamlowecreative.com/headshots/ -https://www.ahmad-ali.co.uk/ -https://www.airbnb.com.br/ -https://www.alexandersparks.com/ -https://www.amazon.com/ -https://www.amazon.fr/ -https://www.amrutam.co.in/ -https://www.anthem.com/ -https://www.apostillecanada.org/ -https://www.artistictile.com/ -https://www.asaudeonline.com.br/ -https://www.ashware.nl/ -https://www.atlantisbahamas.com/ -https://www.attra.com/ -https://www.avalara.com/us/en/index.html -https://www.awwwards.com/ -https://www.backmeup.co.uk/ -https://www.bandlab.com/mix-editor -https://www.belk.com/women/ -https://www.berfect.de/ -https://www.bigbreaks.com/ -https://www.bio-rad-antibodies.com/ -https://www.blbrokers.com/ -https://www.bmu.edu.in/ -https://www.bookxcessonline.com/ -https://www.boticario.com.br/ -https://www.brick-a-brack.com/ -https://www.broadcom.com/ -https://www.careerguide.com/career/working-professionals/10-highest-paying-jobs-for-commerce-students -https://www.carwale.com/new/best-cars-under-15-lakh-in-india/ -https://www.celebsgo.com/ -https://www.chetu.com/ -https://www.chromatix.com.au/ -https://www.chromium.org/developers/bisect-builds-py -https://www.chronoshop2shop.fr/fr -https://www.clouty.ru/ -https://www.clublr.gr/ -https://www.codecademy.com/pro/membership -https://www.coleparmer.com/ -https://www.consumerfinance.gov/about-us/blog/economic-impact-payment-prepaid-card/ -https://www.correio24horas.com.br/noticia/nid/casos-de-covid-19-devem-se-expandir-em-sp-ate-2021-diz-butantan/ -https://www.cort.com/ -https://www.countystonegranite.co.uk/ -https://www.cybermiles.io/en-us/ -https://www.damacproperties.com/ -https://www.deakin.edu.au/ -https://www.dejongintra.nl/ -https://www.deque.com/axe/ -https://www.derby.ac.uk/ -https://www.donatekart.com/ -https://www.drlucianopellegrino.com.br/ -https://www.drsampaioplastica.com.br/ -https://www.e2language.com/ -https://www.easywayphotography.com.au/how-it-works/ -https://www.ebay.com/ -https://www.ecoconcepts.co/ -https://www.edgeclothing.com.au/collections/womens-pants -https://www.elitesingles.com/ -https://www.elliothospital.org/website/urgent-care-bedford.php -https://www.eon.com/en.html -https://www.eq-love.com/fr/ -https://www.eyebuydirect.com/eyeglasses/women -https://www.fashionworld.co.uk/ -https://www.flenco.in/ -https://www.flipkart.com/ -https://www.floweraura.com/ -https://www.foo.software/monitoring-page-experience-with-pagespeed-insights-api-and-lighthouse/ -https://www.framalhocorretor.com.br/ -https://www.freehosting.com/tos.html -https://www.freizi.at/ -https://www.frioval.cl/ -https://www.ft.com/ -https://www.gemini-us.com/ -https://www.glosarioit.com/ -https://www.goldenplanet.dk/ -https://www.google.com/ -https://www.google.com/_/chrome/newtab?ie=UTF-8 -https://www.google.com/_/chrome/newtab?rlz=1C1CHBD_esES751ES751&ie=UTF-8 -https://www.google.com/search?q=hartabumi&oq=harta&aqs=chrome.5.69i57j69i60l4j69i59.4861j0j8&sourceid=chrome&ie=UTF-8 -https://www.google.es/ -https://www.graphitas.co.uk/ -https://www.gstatic.com/mobilesdk/190114_mobilesdk/bootanim-sprite.png -https://www.gundtoft.dk/ -https://www.gupshup.io/developer/docs/bot-platform/guide/intro-to-gupshup-bot-builder -https://www.guthriebowron.co.nz/ -https://www.hamilecarsi.com/ -https://www.hastayataklari.co/ -https://www.holidify.com/places/tehri-garhwal/photos.html -https://www.homeworkmarket.com/users/charandry?page=2 -https://www.htmracing.it/ -https://www.iiad.edu.in/ -https://www.imovies.cc/movies/12227/Le-Trou/RUS/HIGH -https://www.indiatvnews.com/elections -https://www.iontocentre.com/ -https://www.itriangle.in/ -https://www.jako-o.com/de_DE -https://www.jasta5.org/ -https://www.jonniegrieve.co.uk/ -https://www.joseluisandrade.com/ -https://www.jungelinke.at/ -https://www.justacote.com/paris-75015/restaurant/le-quinzieme-53422.htm -https://www.justeat.it/ -https://www.kcholidays.in/ -https://www.kearsleys.com/ -https://www.kenamobile.it/ -https://www.kentuckymathematics.org/ -https://www.kidsgo.de/ -https://www.kitchenaid.com.co/ksm150psmy/p -https://www.koningkaart.nl/ -https://www.kttape.com/ -https://www.kyrillrapp.com/ -https://www.logycacolabora.com/ -https://www.makello.com/ -https://www.mcafee.com/consumer/en-us_multi/spromos/aff/l1079/DEV0000/PN0014/ST0019.html?clickid=TYPQ6SwauxyJRft0MSU5wRc5UklxYrXJrQNkyA0&lqmcat=Affiliate:IR:null:316166:10813:10813:null&sharedid=299463 -https://www.mediaplayer10.com/ -https://www.medlife.com/ -https://www.meetmatch.biz/ -https://www.milforded.org/staff-clone -https://www.montirbox.com/?m=1 -https://www.mudanzasmitre.com/ -https://www.muhammadshoaib.me/ -https://www.mysense.com.my/ -https://www.naqshi.com/ -https://www.news18.com/ -https://www.noom.com/#/ -https://www.npmjs.com/package/which -https://www.nsowo.com/16624/%d8%a3%d9%81%d8%b6%d9%84-%d9%88%d8%b8%d8%a7%d8%a6%d9%81-%d9%81%d9%8a-%d8%a7%d9%84%d8%b9%d8%a7%d9%84%d9%85-%d9%88%d8%a7%d9%84%d8%a3%d8%b9%d9%84%d9%89-%d8%b1%d8%a7%d8%aa%d8%a8%d8%a7/ -https://www.nsowo.com/30876/%D8%A3%D8%B3%D9%85%D8%A7%D8%A1-%D8%A8%D9%86%D8%A7%D8%AA-%D9%85%D8%B9-%D8%A7%D9%84%D8%B5%D9%81%D8%A7%D8%AA-%D9%84%D9%83%D9%84-%D8%A7%D8%B3%D9%85-%D9%81%D8%AA%D8%A7%D8%A9/ -https://www.observepoint.com/ -https://www.officedepot.com/configurator/create-on-demand/#/product/poster/1041 -https://www.oilandgasjobsearch.com/ -https://www.olx.in/ -https://www.openstudio.one/ -https://www.origin.io/ -https://www.ourkyj.com/#/ -https://www.phixer.net/ -https://www.pioneerbricks.com/ -https://www.pioneerelectronics.com/PUSA/Car/DVD+Receivers -https://www.pragtech.co.in/ -https://www.psm.org.ph/guides-notes-faqs/ -https://www.redfin.com/NY/Deer-Park/214-Old-Country-Rd-11729/home/21430946 -https://www.reesby.com.au/ -https://www.resultadofacil.com.br/ -https://www.roblox.com/games?SortFilter=6&TimeFilter=0 -https://www.royeinteriors.com/ -https://www.saatchiart.com/artadvisory -https://www.safestyle-windows.co.uk/ -https://www.sce.com/ -https://www.schekino.net/news/zhiteli-goroda-shhekino-podali-kollektivnuyu-zhalobu-na-upravlyayushhuyu-kompaniyu-everest/ -https://www.sdsgroup.ro/ -https://www.sellsa.co.za/ -https://www.sitespeed.io/ -https://www.smarterhealth.id/5-rumah-sakit-malaysia-pilihan-pasien-indonesia/ -https://www.solactive.com/ -https://www.stage-gate.la/ -https://www.startech.com.bd/ -https://www.stenaline.se/till-danmark -https://www.stylecraze.com/articles/eyelash-extensions-guide/ -https://www.tawuniya.com.sa/ -https://www.tekscan.com/ -https://www.thelallantop.com/news/ -https://www.tiffany.com/ -https://www.tpkha.com/ -https://www.trekksoft.com/ -https://www.tributemedia.com/ -https://www.uncappedfibre.co.za/telkom/openserve?fbclid=IwAR3mmZzaalR7eWrObcYi3LAagagaocBzroJdAj0DYe4AVwkci0EjdtLnwl4 -https://www.uptownshots.com/ -https://www.valdevivo.com/ -https://www.verivox.de/ -https://www.vsointernational.org/ -https://www.w3schools.com/ -https://www.wannaexpresso.com/2021/02/19/m1-macbook-rbenv/ -https://www.wannaexpresso.com/2021/02/20/m1-macbook-minecraft/ -https://www.webpagetest.org//results.php?test=191102_N6_eed705551506349bd8674d5742195212 -https://www.webpagetest.org//results.php?test=191102_WJ_6eb1d4826d72f71ad8148fe4bc2411ae -https://www.wikimass.com/c/biggest-of-three-numbers -https://www.woolaroc.org/become-a-member -https://www.worldrace.org/ -https://www.worthless-stuff.com/ -https://www.yardhopping.com/ -https://www.zenlayer.com/# -https://x2pos.com/ -https://xpos-master.firebaseapp.com/portal -https://yabbermarketing.com/ -https://yenzsocks.com/pages/our-socks -https://yhotie.com/ -https://youngandtheinvested.com/ -https://zaimy24.com.ua/ -https://zawajco.blogspot.com/2019/01/Girls-Photo-numbers-Sons-of-the-UAE.html?m=1 -https://zionmedicinals.com/ -https://zoihospitals.com/ \ No newline at end of file From aeea6a8e7932821497923b649c2f1ecbe32aa461 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 23 Jun 2021 14:58:44 -0700 Subject: [PATCH 08/71] use render.js for standalone --- .../report/html/html-report-assets.js | 18 +- .../report/html/renderer/common/render.js | 37 +++ .../html/renderer/generated/standalone.js | 223 ++++++++++-------- .../report/html/renderer/standalone.js | 26 +- .../report/html/report-template.html | 3 +- lighthouse-core/runner.js | 4 +- 6 files changed, 178 insertions(+), 133 deletions(-) create mode 100644 lighthouse-core/report/html/renderer/common/render.js diff --git a/lighthouse-core/report/html/html-report-assets.js b/lighthouse-core/report/html/html-report-assets.js index af2ef7c8afcb..c365a708d47b 100644 --- a/lighthouse-core/report/html/html-report-assets.js +++ b/lighthouse-core/report/html/html-report-assets.js @@ -8,23 +8,7 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8'); -const REPORT_JAVASCRIPT = [ - fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), -].join(';\n'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/renderer/generated/standalone.js', 'utf8'); const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8'); diff --git a/lighthouse-core/report/html/renderer/common/render.js b/lighthouse-core/report/html/renderer/common/render.js new file mode 100644 index 000000000000..1280d6f827d6 --- /dev/null +++ b/lighthouse-core/report/html/renderer/common/render.js @@ -0,0 +1,37 @@ +/** + * @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 './dom.js'; +// TODO: should Logger be part of the public interface? or just for standalone? +// import {Logger} from './logger.js'; +import {ReportRenderer} from './report-renderer.js'; +import {ReportUIFeatures} from './report-ui-features.js'; + +// OR: we could take an options objec +/** + * @typedef RenderOptions + * @property {LH.Result} lhr + * @property {Element} containerEl Parent element to render the report into. + */ + + +// TODO: we could instead return an Element (not appending to the dom), +// and replace `containerEl` with an options `document: Document` property. + +/** + * @param {RenderOptions} opts + */ +export function renderLighthouseReport(opts) { + const dom = new DOM(opts.containerEl.ownerDocument); + const renderer = new ReportRenderer(dom); + renderer.renderReport(opts.lhr, opts.containerEl); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(opts.lhr); +} diff --git a/lighthouse-core/report/html/renderer/generated/standalone.js b/lighthouse-core/report/html/renderer/generated/standalone.js index d9c35ebe3373..40a3cb58c216 100644 --- a/lighthouse-core/report/html/renderer/generated/standalone.js +++ b/lighthouse-core/report/html/renderer/generated/standalone.js @@ -879,82 +879,6 @@ } } - /** - * @license - * Copyright 2017 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. - */ - - /** - * Logs messages via a UI butter. - */ - class Logger { - /** - * @param {Element} element - */ - constructor(element) { - this.el = element; - this._id = undefined; - } - - /** - * Shows a butter bar. - * @param {string} msg The message to show. - * @param {boolean=} autoHide True to hide the message after a duration. - * Default is true. - */ - log(msg, autoHide = true) { - this._id && clearTimeout(this._id); - - this.el.textContent = msg; - this.el.classList.add('show'); - if (autoHide) { - this._id = setTimeout(_ => { - this.el.classList.remove('show'); - }, 7000); - } - } - - /** - * @param {string} msg - */ - warn(msg) { - this.log('Warning: ' + msg); - } - - /** - * @param {string} msg - */ - error(msg) { - this.log(msg); - - // Rethrow to make sure it's auditable as an error, but in a setTimeout so page - // recovers gracefully and user can try loading a report again. - setTimeout(_ => { - throw new Error(msg); - }, 0); - } - - /** - * Explicitly hides the butter bar. - */ - hide() { - this._id && clearTimeout(this._id); - this.el.classList.remove('show'); - } - } - /** * @license * Copyright 2017 The Lighthouse Authors. All Rights Reserved. @@ -4944,43 +4868,150 @@ * 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. */ - function __initLighthouseReport__() { - const dom = new DOM(document); - const renderer = new ReportRenderer(dom); + // OR: we could take an options objec + /** + * @typedef RenderOptions + * @property {LH.Result} lhr + * @property {Element} containerEl Parent element to render the report into. + */ + - const container = document.querySelector('main'); - renderer.renderReport(window.__LIGHTHOUSE_JSON__, container); + // TODO: we could instead return an Element (not appending to the dom), + // and replace `containerEl` with an options `document: Document` property. + + /** + * @param {RenderOptions} opts + */ + function renderLighthouseReport(opts) { + const dom = new DOM(opts.containerEl.ownerDocument); + const renderer = new ReportRenderer(dom); + renderer.renderReport(opts.lhr, opts.containerEl); // Hook in JS features and page-level event listeners after the report // is in the document. const features = new ReportUIFeatures(dom); - features.initFeatures(window.__LIGHTHOUSE_JSON__); + features.initFeatures(opts.lhr); } - if (document.readyState === 'loading') { - window.addEventListener('DOMContentLoaded', __initLighthouseReport__); - } else { - __initLighthouseReport__(); - } + /** + * @license + * Copyright 2017 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. + */ - document.addEventListener('lh-analytics', e => { - if (window.ga) { - ga(e.detail.cmd, e.detail.fields); + /** + * Logs messages via a UI butter. + */ + class Logger { + /** + * @param {Element} element + */ + constructor(element) { + this.el = element; + this._id = undefined; } + + /** + * Shows a butter bar. + * @param {string} msg The message to show. + * @param {boolean=} autoHide True to hide the message after a duration. + * Default is true. + */ + log(msg, autoHide = true) { + this._id && clearTimeout(this._id); + + this.el.textContent = msg; + this.el.classList.add('show'); + if (autoHide) { + this._id = setTimeout(_ => { + this.el.classList.remove('show'); + }, 7000); + } + } + + /** + * @param {string} msg + */ + warn(msg) { + this.log('Warning: ' + msg); + } + + /** + * @param {string} msg + */ + error(msg) { + this.log(msg); + + // Rethrow to make sure it's auditable as an error, but in a setTimeout so page + // recovers gracefully and user can try loading a report again. + setTimeout(_ => { + throw new Error(msg); + }, 0); + } + + /** + * Explicitly hides the butter bar. + */ + hide() { + this._id && clearTimeout(this._id); + this.el.classList.remove('show'); + } + } + + /** + * @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. + */ + + function __initLighthouseReport__() { + const mainEl = document.querySelector('main'); + if (!mainEl) return; + + /** @type {LH.ReportResult} */ + // @ts-expect-error + const lhr = window.__LIGHTHOUSE_JSON__; + renderLighthouseReport({ + lhr, + containerEl: mainEl, + }); + } + + __initLighthouseReport__(); + + document.addEventListener('lh-analytics', /** @param {Event} e */ e => { + // @ts-expect-error + if (window.ga) ga(e.detail.cmd, e.detail.fields); }); - document.addEventListener('lh-log', e => { - const logger = new Logger(document.querySelector('#lh-log')); + document.addEventListener('lh-log', /** @param {Event} e */ e => { + const el = document.querySelector('#lh-log'); + if (!el) return; + + const logger = new Logger(el); + // @ts-expect-error + const detail = e.detail; - switch (e.detail.cmd) { + switch (detail.cmd) { case 'log': - logger.log(e.detail.msg); + logger.log(detail.msg); break; case 'warn': - logger.warn(e.detail.msg); + logger.warn(detail.msg); break; case 'error': - logger.error(e.detail.msg); + logger.error(detail.msg); break; case 'hide': logger.hide(); diff --git a/lighthouse-core/report/html/renderer/standalone.js b/lighthouse-core/report/html/renderer/standalone.js index 943c0ede1e75..221b8de796a9 100644 --- a/lighthouse-core/report/html/renderer/standalone.js +++ b/lighthouse-core/report/html/renderer/standalone.js @@ -7,31 +7,23 @@ /* global document window ga */ -import {DOM} from './common/dom.js'; +import {renderLighthouseReport} from './common/render.js'; import {Logger} from './common/logger.js'; -import {ReportRenderer} from './common/report-renderer.js'; -import {ReportUIFeatures} from './common/report-ui-features.js'; function __initLighthouseReport__() { - const dom = new DOM(document); - const renderer = new ReportRenderer(dom); - const container = dom.find('main', document); + const mainEl = document.querySelector('main'); + if (!mainEl) return; + /** @type {LH.ReportResult} */ // @ts-expect-error const lhr = window.__LIGHTHOUSE_JSON__; - renderer.renderReport(lhr, container); - - // Hook in JS features and page-level event listeners after the report - // is in the document. - const features = new ReportUIFeatures(dom); - features.initFeatures(lhr); + renderLighthouseReport({ + lhr, + containerEl: mainEl, + }); } -if (document.readyState === 'loading') { - window.addEventListener('DOMContentLoaded', __initLighthouseReport__); -} else { - __initLighthouseReport__(); -} +__initLighthouseReport__(); document.addEventListener('lh-analytics', /** @param {Event} e */ e => { // @ts-expect-error diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html index 4bce64f6eee4..e7cd816d40c0 100644 --- a/lighthouse-core/report/html/report-template.html +++ b/lighthouse-core/report/html/report-template.html @@ -31,11 +31,10 @@
+ - - diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index 6095ae27bc45..48f0d02c855e 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -170,7 +170,9 @@ class Runner { } // Build report if in local dev env so we don't have to run a watch command. - if (settings.output === 'html' && !global.isDevtools && !global.isLightrider && + const forHtml = settings.output === 'html' || + (Array.isArray(settings.output) && settings.output.includes('html')); + if (forHtml && !global.isDevtools && !global.isLightrider && fs.existsSync('dist') && fs.existsSync('.git')) { // Prevent bundling. const buildReportPath = '../build/build-report.js'; From f34a0123b39c5097a783a1eacebb6a86a3a106f2 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 23 Jun 2021 15:21:14 -0700 Subject: [PATCH 09/71] tweak --- lighthouse-core/report/html/renderer/common/dom.js | 1 + lighthouse-core/report/html/renderer/common/render.js | 5 +++++ lighthouse-core/report/html/renderer/psi.js | 1 + 3 files changed, 7 insertions(+) diff --git a/lighthouse-core/report/html/renderer/common/dom.js b/lighthouse-core/report/html/renderer/common/dom.js index 695ba01194d3..f7fa91ba4217 100644 --- a/lighthouse-core/report/html/renderer/common/dom.js +++ b/lighthouse-core/report/html/renderer/common/dom.js @@ -129,6 +129,7 @@ export class DOM { * Resets the "stamped" state of the templates. */ resetTemplates() { + // TODO: this should only act on `templateContext` this.findAll('template[data-stamped]', this._document).forEach(t => { t.removeAttribute('data-stamped'); }); diff --git a/lighthouse-core/report/html/renderer/common/render.js b/lighthouse-core/report/html/renderer/common/render.js index 1280d6f827d6..ce4f888095bb 100644 --- a/lighthouse-core/report/html/renderer/common/render.js +++ b/lighthouse-core/report/html/renderer/common/render.js @@ -21,13 +21,18 @@ import {ReportUIFeatures} from './report-ui-features.js'; // TODO: we could instead return an Element (not appending to the dom), // and replace `containerEl` with an options `document: Document` property. +// oh, and `templateContext` ... /** * @param {RenderOptions} opts */ export function renderLighthouseReport(opts) { const dom = new DOM(opts.containerEl.ownerDocument); + // Assume fresh styles needed on every call, so mark all template styles as unused. + dom.resetTemplates(); + const renderer = new ReportRenderer(dom); + // if (opts.templateContext) renderer.setTemplateContext(opts.templateContext); renderer.renderReport(opts.lhr, opts.containerEl); // Hook in JS features and page-level event listeners after the report diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js index 20c5ab635a31..f0b7049b114d 100644 --- a/lighthouse-core/report/html/renderer/psi.js +++ b/lighthouse-core/report/html/renderer/psi.js @@ -21,6 +21,7 @@ */ 'use strict'; +// import {renderLighthouseReport} from './common/render.js'; import {DetailsRenderer} from './common/details-renderer.js'; import {DOM} from './common/dom.js'; import {ElementScreenshotRenderer} from './common/element-screenshot-renderer.js'; From d24381f7bc488570d072c0f4c7f06c052b30890b Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 23 Jun 2021 17:31:47 -0700 Subject: [PATCH 10/71] fix tests. defer common entry point. --- .gitignore | 2 + build/build-dt-report-resources.js | 4 +- build/build-report.js | 5 +- build/readme.md | 2 +- clients/devtools-report-assets.js | 2 +- docs/releasing.md | 1 - .../report/html/renderer/bundle.js | 16 + .../report/html/renderer/common/dom.js | 1 - .../report/html/renderer/common/file-namer.js | 2 - .../report/html/renderer/common/index.js | 22 - .../report/html/renderer/common/render.js | 42 - .../html/renderer/generated/standalone.js | 5022 ----------------- lighthouse-core/report/html/renderer/psi.js | 1 - .../report/html/renderer/standalone.js | 24 +- lighthouse-core/scripts/copy-util-commonjs.sh | 2 +- .../scripts/i18n/collect-strings.js | 2 +- lighthouse-core/scripts/open-devtools.sh | 1 - lighthouse-core/scripts/roll-to-devtools.sh | 17 +- .../test/lib/page-functions-test.js | 2 +- .../html/renderer/category-renderer-test.js | 12 +- .../renderer/crc-details-renderer-test.js | 10 +- .../html/renderer/details-renderer-test.js | 15 +- .../test/report/html/renderer/dom-test.js | 6 +- .../element-screenshot-renderer-test.js | 8 +- .../test/report/html/renderer/i18n-test.js | 4 +- .../performance-category-renderer-test.js | 14 +- .../test/report/html/renderer/psi-test.js | 18 +- .../renderer/pwa-category-renderer-test.js | 12 +- .../html/renderer/report-renderer-test.js | 22 +- .../html/renderer/report-ui-features-test.js | 22 +- .../html/renderer/snippet-renderer-test.js | 8 +- .../html/renderer/text-encoding-test.js | 2 +- .../test/report/html/renderer/util-test.js | 4 +- lighthouse-treemap/app/src/util.js | 2 +- lighthouse-treemap/types/treemap.d.ts | 8 +- .../app/src/viewer-ui-features.js | 2 +- lighthouse-viewer/types/viewer.d.ts | 10 +- package.json | 5 +- tsconfig.json | 1 + 39 files changed, 146 insertions(+), 5209 deletions(-) create mode 100644 lighthouse-core/report/html/renderer/bundle.js delete mode 100644 lighthouse-core/report/html/renderer/common/index.js delete mode 100644 lighthouse-core/report/html/renderer/common/render.js delete mode 100644 lighthouse-core/report/html/renderer/generated/standalone.js diff --git a/.gitignore b/.gitignore index 25096b00309f..6e08471ce4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ last-run-results.html *.artifacts.log *.ctc.json +lighthouse-core/report/html/renderer/generated + !lighthouse-core/test/results/artifacts/*.trace.json !lighthouse-core/test/results/artifacts/*.devtoolslog.json !lighthouse-core/test/fixtures/artifacts/**/*.trace.json diff --git a/build/build-dt-report-resources.js b/build/build-dt-report-resources.js index 60194567198d..6a2b34b4d2db 100644 --- a/build/build-dt-report-resources.js +++ b/build/build-dt-report-resources.js @@ -28,11 +28,11 @@ function writeFile(name, content) { fs.rmdirSync(distDir, {recursive: true}); fs.mkdirSync(distDir); -writeFile('report.js', htmlReportAssets.REPORT_JAVASCRIPT); // TODO remove +writeFile('report.js', htmlReportAssets.REPORT_JAVASCRIPT); writeFile('report.css', htmlReportAssets.REPORT_CSS); writeFile('template.html', htmlReportAssets.REPORT_TEMPLATE); writeFile('templates.html', htmlReportAssets.REPORT_TEMPLATES); -writeFile('report.d.ts', 'export {}'); // TODO remove +writeFile('report.d.ts', 'export {}'); writeFile('report-generator.d.ts', 'export {}'); const pathToReportAssets = require.resolve('../clients/devtools-report-assets.js'); diff --git a/build/build-report.js b/build/build-report.js index 5b9e0c7f373b..6efb427e56f2 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -32,20 +32,19 @@ async function buildStandaloneReport() { async function buildEsModulesBundle() { const bundle = await rollup.rollup({ - input: 'lighthouse-core/report/html/renderer/common/index.js', + input: 'lighthouse-core/report/html/renderer/bundle.js', plugins: [ commonjs(), ], }); await bundle.write({ - file: 'dist/report.mjs', + file: 'dist/report.js', format: 'esm', }); } buildStandaloneReport(); -// TODO buildPsiReport(); ? buildEsModulesBundle(); module.exports = { diff --git a/build/readme.md b/build/readme.md index c8900c9c6751..164f423e2d41 100644 --- a/build/readme.md +++ b/build/readme.md @@ -15,7 +15,7 @@ Additionally, there are build processes for: To build the devtools files and roll them into a local checkout of Chromium: ```sh -yarn build-devtools && yarn devtools +yarn devtools ``` diff --git a/clients/devtools-report-assets.js b/clients/devtools-report-assets.js index 39ff340677e3..9f962af142d2 100644 --- a/clients/devtools-report-assets.js +++ b/clients/devtools-report-assets.js @@ -25,7 +25,7 @@ module.exports = { return cachedResources.get('third_party/lighthouse/report-assets/report.css'); }, get REPORT_JAVASCRIPT() { - return cachedResources.get('third_party/lighthouse/report/standalone.js'); + return cachedResources.get('third_party/lighthouse/report-assets/report.js'); }, get REPORT_TEMPLATE() { return cachedResources.get('third_party/lighthouse/report-assets/template.html'); diff --git a/docs/releasing.md b/docs/releasing.md index 3f4578516ba7..87d6c7f32e52 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -154,7 +154,6 @@ echo "Complete the _Release publicity_ tasks documented above" ```sh git checkout vx.x.x # Checkout the specific version. -yarn build-devtools yarn devtools ~/src/devtools/devtools-frontend cd ~/src/devtools/devtools-frontend diff --git a/lighthouse-core/report/html/renderer/bundle.js b/lighthouse-core/report/html/renderer/bundle.js new file mode 100644 index 000000000000..d35043cf2c3f --- /dev/null +++ b/lighthouse-core/report/html/renderer/bundle.js @@ -0,0 +1,16 @@ +/** + * @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'; + +// This file is used to generate a bundle that can be imported +// into an esmodules codebase to render the lighthouse report. +// Currently, embedders must handle some boilerplate themselves (like standalone.js) +// until we work out a common rendering interface. +// See: https://github.com/GoogleChrome/lighthouse/pull/12623 + +export {DOM} from './common/dom.js'; +export {ReportRenderer} from './common/report-renderer.js'; +export {ReportUIFeatures} from './common/report-ui-features.js'; diff --git a/lighthouse-core/report/html/renderer/common/dom.js b/lighthouse-core/report/html/renderer/common/dom.js index f7fa91ba4217..695ba01194d3 100644 --- a/lighthouse-core/report/html/renderer/common/dom.js +++ b/lighthouse-core/report/html/renderer/common/dom.js @@ -129,7 +129,6 @@ export class DOM { * Resets the "stamped" state of the templates. */ resetTemplates() { - // TODO: this should only act on `templateContext` this.findAll('template[data-stamped]', this._document).forEach(t => { t.removeAttribute('data-stamped'); }); diff --git a/lighthouse-core/report/html/renderer/common/file-namer.js b/lighthouse-core/report/html/renderer/common/file-namer.js index 09a92a22a797..fab3e537a975 100644 --- a/lighthouse-core/report/html/renderer/common/file-namer.js +++ b/lighthouse-core/report/html/renderer/common/file-namer.js @@ -5,6 +5,4 @@ */ 'use strict'; -// export * from '../../../lib/file-namer.js'; - export {getFilenamePrefix} from '../../../../lib/file-namer.js'; diff --git a/lighthouse-core/report/html/renderer/common/index.js b/lighthouse-core/report/html/renderer/common/index.js deleted file mode 100644 index dedf2058365c..000000000000 --- a/lighthouse-core/report/html/renderer/common/index.js +++ /dev/null @@ -1,22 +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'; - -export {CategoryRenderer} from './category-renderer.js'; -export {CriticalRequestChainRenderer} from './crc-details-renderer.js'; -export {DetailsRenderer} from './details-renderer.js'; -export {DOM} from './dom.js'; -export {ElementScreenshotRenderer} from './element-screenshot-renderer.js'; -export {getFilenamePrefix} from './file-namer.js'; -export {I18n} from './i18n.js'; -export {Logger} from './logger.js'; -export {PerformanceCategoryRenderer} from './performance-category-renderer.js'; -export {PwaCategoryRenderer} from './pwa-category-renderer.js'; -export {ReportRenderer} from './report-renderer.js'; -export {ReportUIFeatures} from './report-ui-features.js'; -export {SnippetRenderer} from './snippet-renderer.js'; -export {TextEncoding} from './text-encoding.js'; -export {Util} from './util.js'; diff --git a/lighthouse-core/report/html/renderer/common/render.js b/lighthouse-core/report/html/renderer/common/render.js deleted file mode 100644 index ce4f888095bb..000000000000 --- a/lighthouse-core/report/html/renderer/common/render.js +++ /dev/null @@ -1,42 +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'; - -import {DOM} from './dom.js'; -// TODO: should Logger be part of the public interface? or just for standalone? -// import {Logger} from './logger.js'; -import {ReportRenderer} from './report-renderer.js'; -import {ReportUIFeatures} from './report-ui-features.js'; - -// OR: we could take an options objec -/** - * @typedef RenderOptions - * @property {LH.Result} lhr - * @property {Element} containerEl Parent element to render the report into. - */ - - -// TODO: we could instead return an Element (not appending to the dom), -// and replace `containerEl` with an options `document: Document` property. -// oh, and `templateContext` ... - -/** - * @param {RenderOptions} opts - */ -export function renderLighthouseReport(opts) { - const dom = new DOM(opts.containerEl.ownerDocument); - // Assume fresh styles needed on every call, so mark all template styles as unused. - dom.resetTemplates(); - - const renderer = new ReportRenderer(dom); - // if (opts.templateContext) renderer.setTemplateContext(opts.templateContext); - renderer.renderReport(opts.lhr, opts.containerEl); - - // Hook in JS features and page-level event listeners after the report - // is in the document. - const features = new ReportUIFeatures(dom); - features.initFeatures(opts.lhr); -} diff --git a/lighthouse-core/report/html/renderer/generated/standalone.js b/lighthouse-core/report/html/renderer/generated/standalone.js deleted file mode 100644 index 40a3cb58c216..000000000000 --- a/lighthouse-core/report/html/renderer/generated/standalone.js +++ /dev/null @@ -1,5022 +0,0 @@ -(function () { - 'use strict'; - - /** - * @license - * Copyright 2017 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. - */ - - /** @template T @typedef {import('./i18n').I18n} I18n */ - - const ELLIPSIS = '\u2026'; - const NBSP = '\xa0'; - const PASS_THRESHOLD = 0.9; - const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,'; - - const RATINGS = { - PASS: {label: 'pass', minScore: PASS_THRESHOLD}, - AVERAGE: {label: 'average', minScore: 0.5}, - FAIL: {label: 'fail'}, - ERROR: {label: 'error'}, - }; - - // 25 most used tld plus one domains (aka public suffixes) from http archive. - // @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212 - // The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes - const listOfTlds = [ - 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv', - 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on', - ]; - - class Util { - static get PASS_THRESHOLD() { - return PASS_THRESHOLD; - } - - static get MS_DISPLAY_VALUE() { - return `%10d${NBSP}ms`; - } - - /** - * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer. - * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be - * compatible with current renderer. - * The LHR passed in is not mutated. - * TODO(team): we all agree the LHR shape change is technical debt we should fix - * @param {LH.Result} result - * @return {LH.ReportResult} - */ - static prepareReportResult(result) { - // If any mutations happen to the report within the renderers, we want the original object untouched - const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result))); - - // If LHR is older (≤3.0.3), it has no locale setting. Set default. - if (!clone.configSettings.locale) { - clone.configSettings.locale = 'en'; - } - if (!clone.configSettings.formFactor) { - // @ts-expect-error fallback handling for emulatedFormFactor - clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor; - } - - for (const audit of Object.values(clone.audits)) { - // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions) - // into 'notApplicable' (LHR ≥4.0). - // @ts-expect-error tsc rightly flags that these values shouldn't occur. - // eslint-disable-next-line max-len - if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') { - audit.scoreDisplayMode = 'notApplicable'; - } - - if (audit.details) { - // Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0) - // into 'debugdata' (LHR ≥5.0). - // @ts-expect-error tsc rightly flags that these values shouldn't occur. - if (audit.details.type === undefined || audit.details.type === 'diagnostic') { - // @ts-expect-error details is of type never. - audit.details.type = 'debugdata'; - } - - // Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0). - if (audit.details.type === 'filmstrip') { - for (const screenshot of audit.details.items) { - if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) { - screenshot.data = SCREENSHOT_PREFIX + screenshot.data; - } - } - } - } - } - - // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group) - if (typeof clone.categories !== 'object') throw new Error('No categories provided.'); - - /** @type {Map>} */ - const relevantAuditToMetricsMap = new Map(); - - for (const category of Object.values(clone.categories)) { - // Make basic lookup table for relevantAudits - category.auditRefs.forEach(metricRef => { - if (!metricRef.relevantAudits) return; - metricRef.relevantAudits.forEach(auditId => { - const arr = relevantAuditToMetricsMap.get(auditId) || []; - arr.push(metricRef); - relevantAuditToMetricsMap.set(auditId, arr); - }); - }); - - category.auditRefs.forEach(auditRef => { - const result = clone.audits[auditRef.id]; - auditRef.result = result; - - // Attach any relevantMetric auditRefs - if (relevantAuditToMetricsMap.has(auditRef.id)) { - auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id); - } - - // attach the stackpacks to the auditRef object - if (clone.stackPacks) { - clone.stackPacks.forEach(pack => { - if (pack.descriptions[auditRef.id]) { - auditRef.stackPacks = auditRef.stackPacks || []; - auditRef.stackPacks.push({ - title: pack.title, - iconDataURL: pack.iconDataURL, - description: pack.descriptions[auditRef.id], - }); - } - }); - } - }); - } - - return clone; - } - - /** - * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed" - * sections of the report. - * - * @param {{score: (number|null), scoreDisplayMode: string}} audit - * @return {boolean} - */ - static showAsPassed(audit) { - switch (audit.scoreDisplayMode) { - case 'manual': - case 'notApplicable': - return true; - case 'error': - case 'informative': - return false; - case 'numeric': - case 'binary': - default: - return Number(audit.score) >= RATINGS.PASS.minScore; - } - } - - /** - * Convert a score to a rating label. - * @param {number|null} score - * @param {string=} scoreDisplayMode - * @return {string} - */ - static calculateRating(score, scoreDisplayMode) { - // Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error' - if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') { - return RATINGS.PASS.label; - } else if (scoreDisplayMode === 'error') { - return RATINGS.ERROR.label; - } else if (score === null) { - return RATINGS.FAIL.label; - } - - // At this point, we're rating a standard binary/numeric audit - let rating = RATINGS.FAIL.label; - if (score >= RATINGS.PASS.minScore) { - rating = RATINGS.PASS.label; - } else if (score >= RATINGS.AVERAGE.minScore) { - rating = RATINGS.AVERAGE.label; - } - return rating; - } - - /** - * Split a string by markdown code spans (enclosed in `backticks`), splitting - * into segments that were enclosed in backticks (marked as `isCode === true`) - * and those that outside the backticks (`isCode === false`). - * @param {string} text - * @return {Array<{isCode: true, text: string}|{isCode: false, text: string}>} - */ - static splitMarkdownCodeSpans(text) { - /** @type {Array<{isCode: true, text: string}|{isCode: false, text: string}>} */ - const segments = []; - - // Split on backticked code spans. - const parts = text.split(/`(.*?)`/g); - for (let i = 0; i < parts.length; i ++) { - const text = parts[i]; - - // Empty strings are an artifact of splitting, not meaningful. - if (!text) continue; - - // Alternates between plain text and code segments. - const isCode = i % 2 !== 0; - segments.push({ - isCode, - text, - }); - } - - return segments; - } - - /** - * Split a string on markdown links (e.g. [some link](https://...)) into - * segments of plain text that weren't part of a link (marked as - * `isLink === false`), and segments with text content and a URL that did make - * up a link (marked as `isLink === true`). - * @param {string} text - * @return {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} - */ - static splitMarkdownLink(text) { - /** @type {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} */ - const segments = []; - - const parts = text.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g); - while (parts.length) { - // Shift off the same number of elements as the pre-split and capture groups. - const [preambleText, linkText, linkHref] = parts.splice(0, 3); - - if (preambleText) { // Skip empty text as it's an artifact of splitting, not meaningful. - segments.push({ - isLink: false, - text: preambleText, - }); - } - - // Append link if there are any. - if (linkText && linkHref) { - segments.push({ - isLink: true, - text: linkText, - linkHref, - }); - } - } - - return segments; - } - - /** - * @param {URL} parsedUrl - * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options - * @return {string} - */ - static getURLDisplayName(parsedUrl, options) { - // Closure optional properties aren't optional in tsc, so fallback needs undefined values. - options = options || {numPathParts: undefined, preserveQuery: undefined, - preserveHost: undefined}; - const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2; - const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true; - const preserveHost = options.preserveHost || false; - - let name; - - if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') { - // Handle 'about:*' and 'data:*' URLs specially since they have no path. - name = parsedUrl.href; - } else { - name = parsedUrl.pathname; - const parts = name.split('/').filter(part => part.length); - if (numPathParts && parts.length > numPathParts) { - name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/'); - } - - if (preserveHost) { - name = `${parsedUrl.host}/${name.replace(/^\//, '')}`; - } - if (preserveQuery) { - name = `${name}${parsedUrl.search}`; - } - } - - const MAX_LENGTH = 64; - // Always elide hexadecimal hash - name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`); - // Also elide other hash-like mixed-case strings - name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g, - `$1${ELLIPSIS}`); - // Also elide long number sequences - name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`); - // Merge any adjacent ellipses - name = name.replace(/\u2026+/g, ELLIPSIS); - - // Elide query params first - if (name.length > MAX_LENGTH && name.includes('?')) { - // Try to leave the first query parameter intact - name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`); - - // Remove it all if it's still too long - if (name.length > MAX_LENGTH) { - name = name.replace(/\?.*/, `?${ELLIPSIS}`); - } - } - - // Elide too long names next - if (name.length > MAX_LENGTH) { - const dotIndex = name.lastIndexOf('.'); - if (dotIndex >= 0) { - name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) + - // Show file extension - `${ELLIPSIS}${name.slice(dotIndex)}`; - } else { - name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS; - } - } - - return name; - } - - /** - * Split a URL into a file, hostname and origin for easy display. - * @param {string} url - * @return {{file: string, hostname: string, origin: string}} - */ - static parseURL(url) { - const parsedUrl = new URL(url); - return { - file: Util.getURLDisplayName(parsedUrl), - hostname: parsedUrl.hostname, - origin: parsedUrl.origin, - }; - } - - /** - * @param {string|URL} value - * @return {!URL} - */ - static createOrReturnURL(value) { - if (value instanceof URL) { - return value; - } - - return new URL(value); - } - - /** - * Gets the tld of a domain - * - * @param {string} hostname - * @return {string} tld - */ - static getTld(hostname) { - const tlds = hostname.split('.').slice(-2); - - if (!listOfTlds.includes(tlds[0])) { - return `.${tlds[tlds.length - 1]}`; - } - - return `.${tlds.join('.')}`; - } - - /** - * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). - * @param {string|URL} url hostname or URL object - * @returns {string} - */ - static getRootDomain(url) { - const hostname = Util.createOrReturnURL(url).hostname; - const tld = Util.getTld(hostname); - - // tld is .com or .co.uk which means we means that length is 1 to big - // .com => 2 & .co.uk => 3 - const splitTld = tld.split('.'); - - // get TLD + root domain - return hostname.split('.').slice(-splitTld.length).join('.'); - } - - /** - * @param {LH.Config.Settings} settings - * @return {!Array<{name: string, description: string}>} - */ - static getEnvironmentDisplayValues(settings) { - const emulationDesc = Util.getEmulationDescriptions(settings); - - return [ - { - name: Util.i18n.strings.runtimeSettingsDevice, - description: emulationDesc.deviceEmulation, - }, - { - name: Util.i18n.strings.runtimeSettingsNetworkThrottling, - description: emulationDesc.networkThrottling, - }, - { - name: Util.i18n.strings.runtimeSettingsCPUThrottling, - description: emulationDesc.cpuThrottling, - }, - ]; - } - - /** - * @param {LH.Config.Settings} settings - * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}} - */ - static getEmulationDescriptions(settings) { - let cpuThrottling; - let networkThrottling; - - const throttling = settings.throttling; - - switch (settings.throttlingMethod) { - case 'provided': - cpuThrottling = Util.i18n.strings.throttlingProvided; - networkThrottling = Util.i18n.strings.throttlingProvided; - break; - case 'devtools': { - const {cpuSlowdownMultiplier, requestLatencyMs} = throttling; - cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`; - networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` + - `${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` + - `${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`; - break; - } - case 'simulate': { - const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling; - cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`; - networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` + - `${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`; - break; - } - default: - cpuThrottling = Util.i18n.strings.runtimeUnknown; - networkThrottling = Util.i18n.strings.runtimeUnknown; - } - - // TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796 - const deviceEmulation = { - mobile: Util.i18n.strings.runtimeMobileEmulation, - desktop: Util.i18n.strings.runtimeDesktopEmulation, - }[settings.formFactor] || Util.i18n.strings.runtimeNoEmulation; - - return { - deviceEmulation, - cpuThrottling, - networkThrottling, - }; - } - - /** - * Returns only lines that are near a message, or the first few lines if there are - * no line messages. - * @param {LH.Audit.Details.SnippetValue['lines']} lines - * @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages - * @param {number} surroundingLineCount Number of lines to include before and after - * the message. If this is e.g. 2 this function might return 5 lines. - */ - static filterRelevantLines(lines, lineMessages, surroundingLineCount) { - if (lineMessages.length === 0) { - // no lines with messages, just return the first bunch of lines - return lines.slice(0, surroundingLineCount * 2 + 1); - } - - const minGapSize = 3; - const lineNumbersToKeep = new Set(); - // Sort messages so we can check lineNumbersToKeep to see how big the gap to - // the previous line is. - lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0)); - lineMessages.forEach(({lineNumber}) => { - let firstSurroundingLineNumber = lineNumber - surroundingLineCount; - let lastSurroundingLineNumber = lineNumber + surroundingLineCount; - - while (firstSurroundingLineNumber < 1) { - // make sure we still show (surroundingLineCount * 2 + 1) lines in total - firstSurroundingLineNumber++; - lastSurroundingLineNumber++; - } - // If only a few lines would be omitted normally then we prefer to include - // extra lines to avoid the tiny gap - if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) { - firstSurroundingLineNumber -= minGapSize; - } - for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) { - const surroundingLineNumber = i; - lineNumbersToKeep.add(surroundingLineNumber); - } - }); - - return lines.filter(line => lineNumbersToKeep.has(line.lineNumber)); - } - - /** - * @param {string} categoryId - */ - static isPluginCategory(categoryId) { - return categoryId.startsWith('lighthouse-plugin-'); - } - } - - /** - * Some parts of the report renderer require data found on the LHR. Instead of wiring it - * through, we have this global. - * @type {LH.ReportResult | null} - */ - Util.reportJson = null; - - /** - * An always-increasing counter for making unique SVG ID suffixes. - */ - Util.getUniqueSuffix = (() => { - let svgSuffix = 0; - return function() { - return svgSuffix++; - }; - })(); - - /** @type {I18n} */ - // @ts-expect-error: Is set in report renderer. - Util.i18n = null; - - /** - * Report-renderer-specific strings. - */ - Util.UIStrings = { - /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */ - varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.', - /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */ - calculatorLink: 'See calculator.', - /** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */ - showRelevantAudits: 'Show audits relevant to:', - /** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */ - opportunityResourceColumnLabel: 'Opportunity', - /** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */ - opportunitySavingsColumnLabel: 'Estimated Savings', - - /** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */ - errorMissingAuditInfo: 'Report error: no audit information', - /** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */ - errorLabel: 'Error!', - /** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */ - warningHeader: 'Warnings: ', - /** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */ - warningAuditsGroupTitle: 'Passed audits but with warnings', - /** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ - passedAuditsGroupTitle: 'Passed audits', - /** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ - notApplicableAuditsGroupTitle: 'Not applicable', - /** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */ - manualAuditsGroupTitle: 'Additional items to manually check', - - /** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */ - toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:', - - /** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */ - crcInitialNavigation: 'Initial Navigation', - /** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */ - crcLongestDurationLabel: 'Maximum critical path latency:', - - /** Label for button that shows all lines of the snippet when clicked */ - snippetExpandButtonLabel: 'Expand snippet', - /** Label for button that only shows a few lines of the snippet when clicked */ - snippetCollapseButtonLabel: 'Collapse snippet', - - /** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */ - lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.', - /** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */ - labDataTitle: 'Lab Data', - - /** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */ - thirdPartyResourcesLabel: 'Show 3rd-party resources', - /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */ - viewTreemapLabel: 'View Treemap', - - /** Option in a dropdown menu that opens a small, summary report in a print dialog. */ - dropdownPrintSummary: 'Print Summary', - /** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */ - dropdownPrintExpanded: 'Print Expanded', - /** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */ - dropdownCopyJSON: 'Copy JSON', - /** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */ - dropdownSaveHTML: 'Save as HTML', - /** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */ - dropdownSaveJSON: 'Save as JSON', - /** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */ - dropdownViewer: 'Open in Viewer', - /** Option in a dropdown menu that saves the current report as a new GitHub Gist. */ - dropdownSaveGist: 'Save as Gist', - /** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */ - dropdownDarkTheme: 'Toggle Dark Theme', - - /** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */ - runtimeSettingsTitle: 'Runtime Settings', - /** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */ - runtimeSettingsUrl: 'URL', - /** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */ - runtimeSettingsFetchTime: 'Fetch Time', - /** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */ - runtimeSettingsDevice: 'Device', - /** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */ - runtimeSettingsNetworkThrottling: 'Network throttling', - /** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/ - runtimeSettingsCPUThrottling: 'CPU throttling', - /** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */ - runtimeSettingsChannel: 'Channel', - /** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */ - runtimeSettingsUA: 'User agent (host)', - /** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */ - runtimeSettingsUANetwork: 'User agent (network)', - /** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */ - runtimeSettingsBenchmark: 'CPU/Memory Power', - /** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */ - runtimeSettingsAxeVersion: 'Axe version', - - /** Label for button to create an issue against the Lighthouse GitHub project. */ - footerIssue: 'File an issue', - - /** Descriptive explanation for emulation setting when no device emulation is set. */ - runtimeNoEmulation: 'No emulation', - /** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */ - runtimeMobileEmulation: 'Emulated Moto G4', - /** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */ - runtimeDesktopEmulation: 'Emulated Desktop', - /** Descriptive explanation for a runtime setting that is set to an unknown value. */ - runtimeUnknown: 'Unknown', - - /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */ - throttlingProvided: 'Provided by environment', - }; - - /** - * @license - * Copyright 2017 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. - */ - - class DOM { - /** - * @param {Document} document - */ - constructor(document) { - /** @type {Document} */ - this._document = document; - /** @type {string} */ - this._lighthouseChannel = 'unknown'; - } - - /** - * @template {string} T - * @param {T} name - * @param {string=} className - * @param {Object=} attrs Attribute key/val pairs. - * Note: if an attribute key has an undefined value, this method does not - * set the attribute on the node. - * @return {HTMLElementByTagName[T]} - */ - createElement(name, className, attrs = {}) { - const element = this._document.createElement(name); - if (className) { - element.className = className; - } - Object.keys(attrs).forEach(key => { - const value = attrs[key]; - if (typeof value !== 'undefined') { - element.setAttribute(key, value); - } - }); - return element; - } - - /** - * @param {string} namespaceURI - * @param {string} name - * @param {string=} className - * @param {Object=} attrs Attribute key/val pairs. - * Note: if an attribute key has an undefined value, this method does not - * set the attribute on the node. - * @return {Element} - */ - createElementNS(namespaceURI, name, className, attrs = {}) { - const element = this._document.createElementNS(namespaceURI, name); - if (className) { - element.className = className; - } - Object.keys(attrs).forEach(key => { - const value = attrs[key]; - if (typeof value !== 'undefined') { - element.setAttribute(key, value); - } - }); - return element; - } - - /** - * @return {!DocumentFragment} - */ - createFragment() { - return this._document.createDocumentFragment(); - } - - /** - * @template {string} T - * @param {Element} parentElem - * @param {T} elementName - * @param {string=} className - * @param {Object=} attrs Attribute key/val pairs. - * Note: if an attribute key has an undefined value, this method does not - * set the attribute on the node. - * @return {HTMLElementByTagName[T]} - */ - createChildOf(parentElem, elementName, className, attrs) { - const element = this.createElement(elementName, className, attrs); - parentElem.appendChild(element); - return element; - } - - /** - * @param {string} selector - * @param {ParentNode} context - * @return {!DocumentFragment} A clone of the template content. - * @throws {Error} - */ - cloneTemplate(selector, context) { - const template = /** @type {?HTMLTemplateElement} */ (context.querySelector(selector)); - if (!template) { - throw new Error(`Template not found: template${selector}`); - } - - const clone = this._document.importNode(template.content, true); - - // Prevent duplicate styles in the DOM. After a template has been stamped - // for the first time, remove the clone's styles so they're not re-added. - if (template.hasAttribute('data-stamped')) { - this.findAll('style', clone).forEach(style => style.remove()); - } - template.setAttribute('data-stamped', 'true'); - - return clone; - } - - /** - * Resets the "stamped" state of the templates. - */ - resetTemplates() { - this.findAll('template[data-stamped]', this._document).forEach(t => { - t.removeAttribute('data-stamped'); - }); - } - - /** - * @param {string} text - * @return {Element} - */ - convertMarkdownLinkSnippets(text) { - const element = this.createElement('span'); - - for (const segment of Util.splitMarkdownLink(text)) { - if (!segment.isLink) { - // Plain text segment. - element.appendChild(this._document.createTextNode(segment.text)); - continue; - } - - // Otherwise, append any links found. - const url = new URL(segment.linkHref); - - const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev']; - if (DOCS_ORIGINS.includes(url.origin)) { - url.searchParams.set('utm_source', 'lighthouse'); - url.searchParams.set('utm_medium', this._lighthouseChannel); - } - - const a = this.createElement('a'); - a.rel = 'noopener'; - a.target = '_blank'; - a.textContent = segment.text; - a.href = url.href; - element.appendChild(a); - } - - return element; - } - - /** - * @param {string} markdownText - * @return {Element} - */ - convertMarkdownCodeSnippets(markdownText) { - const element = this.createElement('span'); - - for (const segment of Util.splitMarkdownCodeSpans(markdownText)) { - if (segment.isCode) { - const pre = this.createElement('code'); - pre.textContent = segment.text; - element.appendChild(pre); - } else { - element.appendChild(this._document.createTextNode(segment.text)); - } - } - - return element; - } - - /** - * The channel to use for UTM data when rendering links to the documentation. - * @param {string} lighthouseChannel - */ - setLighthouseChannel(lighthouseChannel) { - this._lighthouseChannel = lighthouseChannel; - } - - /** - * @return {Document} - */ - document() { - return this._document; - } - - /** - * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this - * @return {boolean} - */ - isDevTools() { - return !!this._document.querySelector('.lh-devtools'); - } - - /** - * Guaranteed context.querySelector. Always returns an element or throws if - * nothing matches query. - * @template {string} T - * @param {T} query - * @param {ParentNode} context - * @return {ParseSelector} - */ - find(query, context) { - const result = context.querySelector(query); - if (result === null) { - throw new Error(`query ${query} not found`); - } - - // Because we control the report layout and templates, use the simpler - // `typed-query-selector` types that don't require differentiating between - // e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011 - return /** @type {ParseSelector} */ (result); - } - - /** - * Helper for context.querySelectorAll. Returns an Array instead of a NodeList. - * @template {string} T - * @param {T} query - * @param {ParentNode} context - */ - findAll(query, context) { - const elements = Array.from(context.querySelectorAll(query)); - return elements; - } - } - - /** - * @license - * Copyright 2017 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. - */ - - class CategoryRenderer { - /** - * @param {DOM} dom - * @param {DetailsRenderer} detailsRenderer - */ - constructor(dom, detailsRenderer) { - /** @type {DOM} */ - this.dom = dom; - /** @type {DetailsRenderer} */ - this.detailsRenderer = detailsRenderer; - /** @type {ParentNode} */ - this.templateContext = this.dom.document(); - - this.detailsRenderer.setTemplateContext(this.templateContext); - } - - /** - * Display info per top-level clump. Define on class to avoid race with Util init. - */ - get _clumpTitles() { - return { - warning: Util.i18n.strings.warningAuditsGroupTitle, - manual: Util.i18n.strings.manualAuditsGroupTitle, - passed: Util.i18n.strings.passedAuditsGroupTitle, - notApplicable: Util.i18n.strings.notApplicableAuditsGroupTitle, - }; - } - - /** - * @param {LH.ReportResult.AuditRef} audit - * @return {Element} - */ - renderAudit(audit) { - const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext); - return this.populateAuditValues(audit, tmpl); - } - - /** - * Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity - * @param {LH.ReportResult.AuditRef} audit - * @param {DocumentFragment} tmpl - * @return {!Element} - */ - populateAuditValues(audit, tmpl) { - const strings = Util.i18n.strings; - const auditEl = this.dom.find('.lh-audit', tmpl); - auditEl.id = audit.result.id; - const scoreDisplayMode = audit.result.scoreDisplayMode; - - if (audit.result.displayValue) { - this.dom.find('.lh-audit__display-text', auditEl).textContent = audit.result.displayValue; - } - - const titleEl = this.dom.find('.lh-audit__title', auditEl); - titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title)); - const descEl = this.dom.find('.lh-audit__description', auditEl); - descEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description)); - - for (const relevantMetric of audit.relevantMetrics || []) { - const adornEl = this.dom.createChildOf(descEl, 'span', 'lh-audit__adorn', { - title: `Relevant to ${relevantMetric.result.title}`, - }); - adornEl.textContent = relevantMetric.acronym || relevantMetric.id; - } - - if (audit.stackPacks) { - audit.stackPacks.forEach(pack => { - const packElm = this.dom.createElement('div'); - packElm.classList.add('lh-audit__stackpack'); - - const packElmImg = this.dom.createElement('img'); - packElmImg.classList.add('lh-audit__stackpack__img'); - packElmImg.src = pack.iconDataURL; - packElmImg.alt = pack.title; - packElm.appendChild(packElmImg); - - packElm.appendChild(this.dom.convertMarkdownLinkSnippets(pack.description)); - - this.dom.find('.lh-audit__stackpacks', auditEl) - .appendChild(packElm); - }); - } - - const header = this.dom.find('details', auditEl); - if (audit.result.details) { - const elem = this.detailsRenderer.render(audit.result.details); - if (elem) { - elem.classList.add('lh-details'); - header.appendChild(elem); - } - } - - // Add chevron SVG to the end of the summary - this.dom.find('.lh-chevron-container', auditEl).appendChild(this._createChevron()); - this._setRatingClass(auditEl, audit.result.score, scoreDisplayMode); - - if (audit.result.scoreDisplayMode === 'error') { - auditEl.classList.add(`lh-audit--error`); - const textEl = this.dom.find('.lh-audit__display-text', auditEl); - textEl.textContent = strings.errorLabel; - textEl.classList.add('tooltip-boundary'); - const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error'); - tooltip.textContent = audit.result.errorMessage || strings.errorMissingAuditInfo; - } else if (audit.result.explanation) { - const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation'); - explEl.textContent = audit.result.explanation; - } - const warnings = audit.result.warnings; - if (!warnings || warnings.length === 0) return auditEl; - - // Add list of warnings or singular warning - const summaryEl = this.dom.find('summary', header); - 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(''))); - } else { - const warningsUl = this.dom.createChildOf(warningsEl, 'ul'); - for (const warning of warnings) { - const item = this.dom.createChildOf(warningsUl, 'li'); - item.textContent = warning; - } - } - return auditEl; - } - - /** - * @return {Element} - */ - _createChevron() { - const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext); - const chevronEl = this.dom.find('svg.lh-chevron', chevronTmpl); - return chevronEl; - } - - /** - * @param {Element} element DOM node to populate with values. - * @param {number|null} score - * @param {string} scoreDisplayMode - * @return {!Element} - */ - _setRatingClass(element, score, scoreDisplayMode) { - const rating = Util.calculateRating(score, scoreDisplayMode); - element.classList.add(`lh-audit--${scoreDisplayMode.toLowerCase()}`); - if (scoreDisplayMode !== 'informative') { - element.classList.add(`lh-audit--${rating}`); - } - return element; - } - - /** - * @param {LH.ReportResult.Category} category - * @param {Record} groupDefinitions - * @return {DocumentFragment} - */ - renderCategoryHeader(category, groupDefinitions) { - const tmpl = this.dom.cloneTemplate('#tmpl-lh-category-header', this.templateContext); - - const gaugeContainerEl = this.dom.find('.lh-score__gauge', tmpl); - const gaugeEl = this.renderScoreGauge(category, groupDefinitions); - gaugeContainerEl.appendChild(gaugeEl); - - if (category.description) { - const descEl = this.dom.convertMarkdownLinkSnippets(category.description); - this.dom.find('.lh-category-header__description', tmpl).appendChild(descEl); - } - - return tmpl; - } - - /** - * Renders the group container for a group of audits. Individual audit elements can be added - * directly to the returned element. - * @param {LH.Result.ReportGroup} group - * @return {Element} - */ - renderAuditGroup(group) { - const groupEl = this.dom.createElement('div', 'lh-audit-group'); - - const auditGroupHeader = this.dom.createElement('div', 'lh-audit-group__header'); - - this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title') - .textContent = group.title; - if (group.description) { - const descriptionEl = this.dom.convertMarkdownLinkSnippets(group.description); - descriptionEl.classList.add('lh-audit-group__description'); - auditGroupHeader.appendChild(descriptionEl); - } - groupEl.appendChild(auditGroupHeader); - - return groupEl; - } - - /** - * Takes an array of auditRefs, groups them if requested, then returns an - * array of audit and audit-group elements. - * @param {Array} auditRefs - * @param {Object} groupDefinitions - * @return {Array} - */ - _renderGroupedAudits(auditRefs, groupDefinitions) { - // Audits grouped by their group (or under notAGroup). - /** @type {Map>} */ - const grouped = new Map(); - - // Add audits without a group first so they will appear first. - const notAGroup = 'NotAGroup'; - grouped.set(notAGroup, []); - - for (const auditRef of auditRefs) { - const groupId = auditRef.group || notAGroup; - const groupAuditRefs = grouped.get(groupId) || []; - groupAuditRefs.push(auditRef); - grouped.set(groupId, groupAuditRefs); - } - - /** @type {Array} */ - const auditElements = []; - - for (const [groupId, groupAuditRefs] of grouped) { - if (groupId === notAGroup) { - // Push not-grouped audits individually. - for (const auditRef of groupAuditRefs) { - auditElements.push(this.renderAudit(auditRef)); - } - continue; - } - - // Push grouped audits as a group. - const groupDef = groupDefinitions[groupId]; - const auditGroupElem = this.renderAuditGroup(groupDef); - for (const auditRef of groupAuditRefs) { - auditGroupElem.appendChild(this.renderAudit(auditRef)); - } - auditGroupElem.classList.add(`lh-audit-group--${groupId}`); - auditElements.push(auditGroupElem); - } - - return auditElements; - } - - /** - * Take a set of audits, group them if they have groups, then render in a top-level - * clump that can't be expanded/collapsed. - * @param {Array} auditRefs - * @param {Object} groupDefinitions - * @return {Element} - */ - renderUnexpandableClump(auditRefs, groupDefinitions) { - const clumpElement = this.dom.createElement('div'); - const elements = this._renderGroupedAudits(auditRefs, groupDefinitions); - elements.forEach(elem => clumpElement.appendChild(elem)); - return clumpElement; - } - - /** - * Take a set of audits and render in a top-level, expandable clump that starts - * in a collapsed state. - * @param {Exclude} clumpId - * @param {{auditRefs: Array, description?: string}} clumpOpts - * @return {!Element} - */ - renderClump(clumpId, {auditRefs, description}) { - const clumpTmpl = this.dom.cloneTemplate('#tmpl-lh-clump', this.templateContext); - const clumpElement = this.dom.find('.lh-clump', clumpTmpl); - - if (clumpId === 'warning') { - clumpElement.setAttribute('open', ''); - } - - const summaryInnerEl = this.dom.find('div.lh-audit-group__summary', clumpElement); - summaryInnerEl.appendChild(this._createChevron()); - - const headerEl = this.dom.find('.lh-audit-group__header', clumpElement); - const title = this._clumpTitles[clumpId]; - this.dom.find('.lh-audit-group__title', headerEl).textContent = title; - if (description) { - const descriptionEl = this.dom.convertMarkdownLinkSnippets(description); - descriptionEl.classList.add('lh-audit-group__description'); - headerEl.appendChild(descriptionEl); - } - - const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement); - itemCountEl.textContent = `(${auditRefs.length})`; - - // Add all audit results to the clump. - const auditElements = auditRefs.map(this.renderAudit.bind(this)); - clumpElement.append(...auditElements); - - clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`); - return clumpElement; - } - - /** - * @param {ParentNode} context - */ - setTemplateContext(context) { - this.templateContext = context; - this.detailsRenderer.setTemplateContext(context); - } - - /** - * @param {LH.ReportResult.Category} category - * @param {Record} groupDefinitions - * @return {DocumentFragment} - */ - renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars - const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge', this.templateContext); - const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl); - wrapper.href = `#${category.id}`; - - if (Util.isPluginCategory(category.id)) { - wrapper.classList.add('lh-gauge__wrapper--plugin'); - } - - // Cast `null` to 0 - const numericScore = Number(category.score); - const gauge = this.dom.find('.lh-gauge', tmpl); - const gaugeArc = this.dom.find('circle.lh-gauge-arc', gauge); - - if (gaugeArc) this._setGaugeArc(gaugeArc, numericScore); - - const scoreOutOf100 = Math.round(numericScore * 100); - const percentageEl = this.dom.find('div.lh-gauge__percentage', tmpl); - percentageEl.textContent = scoreOutOf100.toString(); - if (category.score === null) { - percentageEl.textContent = '?'; - percentageEl.title = Util.i18n.strings.errorLabel; - } - - // Render a numerical score if the category has applicable audits, or no audits whatsoever. - if (category.auditRefs.length === 0 || this.hasApplicableAudits(category)) { - wrapper.classList.add(`lh-gauge__wrapper--${Util.calculateRating(category.score)}`); - } else { - wrapper.classList.add(`lh-gauge__wrapper--not-applicable`); - percentageEl.textContent = '-'; - percentageEl.title = Util.i18n.strings.notApplicableAuditsGroupTitle; - } - - this.dom.find('.lh-gauge__label', tmpl).textContent = category.title; - return tmpl; - } - - /** - * Returns true if an LH category has any non-"notApplicable" audits. - * @param {LH.ReportResult.Category} category - * @return {boolean} - */ - hasApplicableAudits(category) { - return category.auditRefs.some(ref => ref.result.scoreDisplayMode !== 'notApplicable'); - } - - /** - * Define the score arc of the gauge - * Credit to xgad for the original technique: https://codepen.io/xgad/post/svg-radial-progress-meters - * @param {SVGCircleElement} arcElem - * @param {number} percent - */ - _setGaugeArc(arcElem, percent) { - const circumferencePx = 2 * Math.PI * Number(arcElem.getAttribute('r')); - // The rounded linecap of the stroke extends the arc past its start and end. - // First, we tweak the -90deg rotation to start exactly at the top of the circle. - const strokeWidthPx = Number(arcElem.getAttribute('stroke-width')); - const rotationalAdjustmentPercent = 0.25 * strokeWidthPx / circumferencePx; - arcElem.style.transform = `rotate(${-90 + rotationalAdjustmentPercent * 360}deg)`; - - // Then, we terminate the line a little early as well. - let arcLengthPx = percent * circumferencePx - strokeWidthPx / 2; - // Special cases. No dot for 0, and full ring if 100 - if (percent === 0) arcElem.style.opacity = '0'; - if (percent === 1) arcLengthPx = circumferencePx; - - arcElem.style.strokeDasharray = `${Math.max(arcLengthPx, 0)} ${circumferencePx}`; - } - - /** - * @param {LH.ReportResult.AuditRef} audit - * @return {boolean} - */ - _auditHasWarning(audit) { - return Boolean(audit.result.warnings && audit.result.warnings.length); - } - - /** - * Returns the id of the top-level clump to put this audit in. - * @param {LH.ReportResult.AuditRef} auditRef - * @return {TopLevelClumpId} - */ - _getClumpIdForAuditRef(auditRef) { - const scoreDisplayMode = auditRef.result.scoreDisplayMode; - if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') { - return scoreDisplayMode; - } - - if (Util.showAsPassed(auditRef.result)) { - if (this._auditHasWarning(auditRef)) { - return 'warning'; - } else { - return 'passed'; - } - } else { - return 'failed'; - } - } - - /** - * Renders a set of top level sections (clumps), under a status of failed, warning, - * manual, passed, or notApplicable. The result ends up something like: - * - * failed clump - * ├── audit 1 (w/o group) - * ├── audit 2 (w/o group) - * ├── audit group - * | ├── audit 3 - * | └── audit 4 - * └── audit group - * ├── audit 5 - * └── audit 6 - * other clump (e.g. 'manual') - * ├── audit 1 - * ├── audit 2 - * ├── … - * ⋮ - * @param {LH.ReportResult.Category} category - * @param {Object} [groupDefinitions] - * @return {Element} - */ - render(category, groupDefinitions = {}) { - const element = this.dom.createElement('div', 'lh-category'); - this.createPermalinkSpan(element, category.id); - element.appendChild(this.renderCategoryHeader(category, groupDefinitions)); - - // Top level clumps for audits, in order they will appear in the report. - /** @type {Map>} */ - const clumps = new Map(); - clumps.set('failed', []); - clumps.set('warning', []); - clumps.set('manual', []); - clumps.set('passed', []); - clumps.set('notApplicable', []); - - // Sort audits into clumps. - for (const auditRef of category.auditRefs) { - const clumpId = this._getClumpIdForAuditRef(auditRef); - const clump = /** @type {Array} */ (clumps.get(clumpId)); // already defined - clump.push(auditRef); - clumps.set(clumpId, clump); - } - - // Render each clump. - for (const [clumpId, auditRefs] of clumps) { - if (auditRefs.length === 0) continue; - - if (clumpId === 'failed') { - const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions); - clumpElem.classList.add(`lh-clump--failed`); - element.appendChild(clumpElem); - continue; - } - - const description = clumpId === 'manual' ? category.manualDescription : undefined; - const clumpElem = this.renderClump(clumpId, {auditRefs, description}); - element.appendChild(clumpElem); - } - - return element; - } - - /** - * Create a non-semantic span used for hash navigation of categories - * @param {Element} element - * @param {string} id - */ - createPermalinkSpan(element, id) { - const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink'); - permalinkEl.id = id; - } - } - - /** - * @license - * Copyright 2017 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. - */ - - class CriticalRequestChainRenderer { - /** - * Create render context for critical-request-chain tree display. - * @param {LH.Audit.SimpleCriticalRequestNode} tree - * @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}} - */ - static initTree(tree) { - let startTime = 0; - const rootNodes = Object.keys(tree); - if (rootNodes.length > 0) { - const node = tree[rootNodes[0]]; - startTime = node.request.startTime; - } - - return {tree, startTime, transferSize: 0}; - } - - /** - * Helper to create context for each critical-request-chain node based on its - * parent. Calculates if this node is the last child, whether it has any - * children itself and what the tree looks like all the way back up to the root, - * so the tree markers can be drawn correctly. - * @param {LH.Audit.SimpleCriticalRequestNode} parent - * @param {string} id - * @param {number} startTime - * @param {number} transferSize - * @param {Array=} treeMarkers - * @param {boolean=} parentIsLastChild - * @return {CRCSegment} - */ - static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) { - const node = parent[id]; - const siblings = Object.keys(parent); - const isLastChild = siblings.indexOf(id) === (siblings.length - 1); - const hasChildren = !!node.children && Object.keys(node.children).length > 0; - - // Copy the tree markers so that we don't change by reference. - const newTreeMarkers = Array.isArray(treeMarkers) ? treeMarkers.slice(0) : []; - - // Add on the new entry. - if (typeof parentIsLastChild !== 'undefined') { - newTreeMarkers.push(!parentIsLastChild); - } - - return { - node, - isLastChild, - hasChildren, - startTime, - transferSize: transferSize + node.request.transferSize, - treeMarkers: newTreeMarkers, - }; - } - - /** - * Creates the DOM for a tree segment. - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {CRCSegment} segment - * @param {DetailsRenderer} detailsRenderer - * @return {Node} - */ - static createChainNode(dom, tmpl, segment, detailsRenderer) { - const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl); - - // Hovering over request shows full URL. - dom.find('.crc-node', chainsEl).setAttribute('title', segment.node.request.url); - - const treeMarkeEl = dom.find('.crc-node__tree-marker', chainsEl); - - // Construct lines and add spacers for sub requests. - segment.treeMarkers.forEach(separator => { - if (separator) { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert')); - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); - } else { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker')); - } - }); - - if (segment.isLastChild) { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker up-right')); - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); - } else { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert-right')); - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); - } - - if (segment.hasChildren) { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker horiz-down')); - } else { - treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right')); - } - - // Fill in url, host, and request size information. - const url = segment.node.request.url; - const linkEl = detailsRenderer.renderTextURL(url); - const treevalEl = dom.find('.crc-node__tree-value', chainsEl); - treevalEl.appendChild(linkEl); - - if (!segment.hasChildren) { - const {startTime, endTime, transferSize} = segment.node.request; - const span = dom.createElement('span', 'crc-node__chain-duration'); - span.textContent = ' - ' + Util.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', '; - const span2 = dom.createElement('span', 'crc-node__chain-duration'); - span2.textContent = Util.i18n.formatBytesToKiB(transferSize, 0.01); - - treevalEl.appendChild(span); - treevalEl.appendChild(span2); - } - - return chainsEl; - } - - /** - * Recursively builds a tree from segments. - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {CRCSegment} segment - * @param {Element} elem Parent element. - * @param {LH.Audit.Details.CriticalRequestChain} details - * @param {DetailsRenderer} detailsRenderer - */ - static buildTree(dom, tmpl, segment, elem, details, detailsRenderer) { - elem.appendChild(CRCRenderer.createChainNode(dom, tmpl, segment, detailsRenderer)); - if (segment.node.children) { - for (const key of Object.keys(segment.node.children)) { - const childSegment = CRCRenderer.createSegment(segment.node.children, key, - segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild); - CRCRenderer.buildTree(dom, tmpl, childSegment, elem, details, detailsRenderer); - } - } - } - - /** - * @param {DOM} dom - * @param {ParentNode} templateContext - * @param {LH.Audit.Details.CriticalRequestChain} details - * @param {DetailsRenderer} detailsRenderer - * @return {Element} - */ - static render(dom, templateContext, details, detailsRenderer) { - const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext); - const containerEl = dom.find('.lh-crc', tmpl); - - // Fill in top summary. - dom.find('.crc-initial-nav', tmpl).textContent = Util.i18n.strings.crcInitialNavigation; - dom.find('.lh-crc__longest_duration_label', tmpl).textContent = - Util.i18n.strings.crcLongestDurationLabel; - dom.find('.lh-crc__longest_duration', tmpl).textContent = - Util.i18n.formatMilliseconds(details.longestChain.duration); - - // Construct visual tree. - const root = CRCRenderer.initTree(details.chains); - for (const key of Object.keys(root.tree)) { - const segment = CRCRenderer.createSegment(root.tree, key, root.startTime, root.transferSize); - CRCRenderer.buildTree(dom, tmpl, segment, containerEl, details, detailsRenderer); - } - - return dom.find('.lh-crc-container', tmpl); - } - } - - // Alias b/c the name is really long. - const CRCRenderer = CriticalRequestChainRenderer; - - /** @typedef {{ - node: LH.Audit.SimpleCriticalRequestNode[string], - isLastChild: boolean, - hasChildren: boolean, - startTime: number, - transferSize: number, - treeMarkers: Array - }} CRCSegment - */ - - /** - * @license Copyright 2019 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. - */ - - /** @enum {number} */ - const LineVisibility = { - /** Show regardless of whether the snippet is collapsed or expanded */ - ALWAYS: 0, - WHEN_COLLAPSED: 1, - WHEN_EXPANDED: 2, - }; - - /** @enum {number} */ - const LineContentType = { - /** A line of content */ - CONTENT_NORMAL: 0, - /** A line of content that's emphasized by setting the CSS background color */ - CONTENT_HIGHLIGHTED: 1, - /** Use when some lines are hidden, shows the "..." placeholder */ - PLACEHOLDER: 2, - /** A message about a line of content or the snippet in general */ - MESSAGE: 3, - }; - - /** @typedef {{ - content: string; - lineNumber: string | number; - contentType: LineContentType; - truncated?: boolean; - visibility?: LineVisibility; - }} LineDetails */ - - const classNamesByContentType = { - [LineContentType.CONTENT_NORMAL]: ['lh-snippet__line--content'], - [LineContentType.CONTENT_HIGHLIGHTED]: [ - 'lh-snippet__line--content', - 'lh-snippet__line--content-highlighted', - ], - [LineContentType.PLACEHOLDER]: ['lh-snippet__line--placeholder'], - [LineContentType.MESSAGE]: ['lh-snippet__line--message'], - }; - - /** - * @param {LH.Audit.Details.SnippetValue['lines']} lines - * @param {number} lineNumber - * @return {{line?: LH.Audit.Details.SnippetValue['lines'][0], previousLine?: LH.Audit.Details.SnippetValue['lines'][0]}} - */ - function getLineAndPreviousLine(lines, lineNumber) { - return { - line: lines.find(l => l.lineNumber === lineNumber), - previousLine: lines.find(l => l.lineNumber === lineNumber - 1), - }; - } - - /** - * @param {LH.Audit.Details.SnippetValue["lineMessages"]} messages - * @param {number} lineNumber - */ - function getMessagesForLineNumber(messages, lineNumber) { - return messages.filter(h => h.lineNumber === lineNumber); - } - - /** - * @param {LH.Audit.Details.SnippetValue} details - * @return {LH.Audit.Details.SnippetValue['lines']} - */ - function getLinesWhenCollapsed(details) { - const SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED = 2; - return Util.filterRelevantLines( - details.lines, - details.lineMessages, - SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED - ); - } - - /** - * Render snippet of text with line numbers and annotations. - * By default we only show a few lines around each annotation and the user - * can click "Expand snippet" to show more. - * Content lines with annotations are highlighted. - */ - class SnippetRenderer { - /** - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {LH.Audit.Details.SnippetValue} details - * @param {DetailsRenderer} detailsRenderer - * @param {function} toggleExpandedFn - * @return {DocumentFragment} - */ - static renderHeader(dom, tmpl, details, detailsRenderer, toggleExpandedFn) { - const linesWhenCollapsed = getLinesWhenCollapsed(details); - const canExpand = linesWhenCollapsed.length < details.lines.length; - - const header = dom.cloneTemplate('#tmpl-lh-snippet__header', tmpl); - dom.find('.lh-snippet__title', header).textContent = details.title; - - const { - snippetCollapseButtonLabel, - snippetExpandButtonLabel, - } = Util.i18n.strings; - dom.find( - '.lh-snippet__btn-label-collapse', - header - ).textContent = snippetCollapseButtonLabel; - dom.find( - '.lh-snippet__btn-label-expand', - header - ).textContent = snippetExpandButtonLabel; - - const toggleExpandButton = dom.find('.lh-snippet__toggle-expand', header); - // If we're already showing all the available lines of the snippet, we don't need an - // expand/collapse button and can remove it from the DOM. - // If we leave the button in though, wire up the click listener to toggle visibility! - if (!canExpand) { - toggleExpandButton.remove(); - } else { - toggleExpandButton.addEventListener('click', () => toggleExpandedFn()); - } - - // We only show the source node of the snippet in DevTools because then the user can - // access the full element detail. Just being able to see the outer HTML isn't very useful. - if (details.node && dom.isDevTools()) { - const nodeContainer = dom.find('.lh-snippet__node', header); - nodeContainer.appendChild(detailsRenderer.renderNode(details.node)); - } - - return header; - } - - /** - * Renders a line (text content, message, or placeholder) as a DOM element. - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {LineDetails} lineDetails - * @return {Element} - */ - static renderSnippetLine( - dom, - tmpl, - {content, lineNumber, truncated, contentType, visibility} - ) { - const clonedTemplate = dom.cloneTemplate('#tmpl-lh-snippet__line', tmpl); - const contentLine = dom.find('.lh-snippet__line', clonedTemplate); - const {classList} = contentLine; - - classNamesByContentType[contentType].forEach(typeClass => - classList.add(typeClass) - ); - - if (visibility === LineVisibility.WHEN_COLLAPSED) { - classList.add('lh-snippet__show-if-collapsed'); - } else if (visibility === LineVisibility.WHEN_EXPANDED) { - classList.add('lh-snippet__show-if-expanded'); - } - - const lineContent = content + (truncated ? '…' : ''); - const lineContentEl = dom.find('.lh-snippet__line code', contentLine); - if (contentType === LineContentType.MESSAGE) { - lineContentEl.appendChild(dom.convertMarkdownLinkSnippets(lineContent)); - } else { - lineContentEl.textContent = lineContent; - } - - dom.find( - '.lh-snippet__line-number', - contentLine - ).textContent = lineNumber.toString(); - - return contentLine; - } - - /** - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {{message: string}} message - * @return {Element} - */ - static renderMessage(dom, tmpl, message) { - return SnippetRenderer.renderSnippetLine(dom, tmpl, { - lineNumber: ' ', - content: message.message, - contentType: LineContentType.MESSAGE, - }); - } - - /** - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {LineVisibility} visibility - * @return {Element} - */ - static renderOmittedLinesPlaceholder(dom, tmpl, visibility) { - return SnippetRenderer.renderSnippetLine(dom, tmpl, { - lineNumber: '…', - content: '', - visibility, - contentType: LineContentType.PLACEHOLDER, - }); - } - - /** - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {LH.Audit.Details.SnippetValue} details - * @return {DocumentFragment} - */ - static renderSnippetContent(dom, tmpl, details) { - const template = dom.cloneTemplate('#tmpl-lh-snippet__content', tmpl); - const snippetEl = dom.find('.lh-snippet__snippet-inner', template); - - // First render messages that don't belong to specific lines - details.generalMessages.forEach(m => - snippetEl.append(SnippetRenderer.renderMessage(dom, tmpl, m)) - ); - // Then render the lines and their messages, as well as placeholders where lines are omitted - snippetEl.append(SnippetRenderer.renderSnippetLines(dom, tmpl, details)); - - return template; - } - - /** - * @param {DOM} dom - * @param {DocumentFragment} tmpl - * @param {LH.Audit.Details.SnippetValue} details - * @return {DocumentFragment} - */ - static renderSnippetLines(dom, tmpl, details) { - const {lineMessages, generalMessages, lineCount, lines} = details; - const linesWhenCollapsed = getLinesWhenCollapsed(details); - const hasOnlyGeneralMessages = - generalMessages.length > 0 && lineMessages.length === 0; - - const lineContainer = dom.createFragment(); - - // When a line is not shown in the collapsed state we try to see if we also need an - // omitted lines placeholder for the expanded state, rather than rendering two separate - // placeholders. - let hasPendingOmittedLinesPlaceholderForCollapsedState = false; - - for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { - const {line, previousLine} = getLineAndPreviousLine(lines, lineNumber); - const { - line: lineWhenCollapsed, - previousLine: previousLineWhenCollapsed, - } = getLineAndPreviousLine(linesWhenCollapsed, lineNumber); - - const showLineWhenCollapsed = !!lineWhenCollapsed; - const showPreviousLineWhenCollapsed = !!previousLineWhenCollapsed; - - // If we went from showing lines in the collapsed state to not showing them - // we need to render a placeholder - if (showPreviousLineWhenCollapsed && !showLineWhenCollapsed) { - hasPendingOmittedLinesPlaceholderForCollapsedState = true; - } - // If we are back to lines being visible in the collapsed and the placeholder - // hasn't been rendered yet then render it now - if ( - showLineWhenCollapsed && - hasPendingOmittedLinesPlaceholderForCollapsedState - ) { - lineContainer.append( - SnippetRenderer.renderOmittedLinesPlaceholder( - dom, - tmpl, - LineVisibility.WHEN_COLLAPSED - ) - ); - hasPendingOmittedLinesPlaceholderForCollapsedState = false; - } - - // Render omitted lines placeholder if we have not already rendered one for this gap - const isFirstOmittedLineWhenExpanded = !line && !!previousLine; - const isFirstLineOverallAndIsOmittedWhenExpanded = - !line && lineNumber === 1; - if ( - isFirstOmittedLineWhenExpanded || - isFirstLineOverallAndIsOmittedWhenExpanded - ) { - // In the collapsed state we don't show omitted lines placeholders around - // the edges of the snippet - const hasRenderedAllLinesVisibleWhenCollapsed = !linesWhenCollapsed.some( - l => l.lineNumber > lineNumber - ); - const onlyShowWhenExpanded = - hasRenderedAllLinesVisibleWhenCollapsed || lineNumber === 1; - lineContainer.append( - SnippetRenderer.renderOmittedLinesPlaceholder( - dom, - tmpl, - onlyShowWhenExpanded - ? LineVisibility.WHEN_EXPANDED - : LineVisibility.ALWAYS - ) - ); - hasPendingOmittedLinesPlaceholderForCollapsedState = false; - } - - if (!line) { - // Can't render the line if we don't know its content (instead we've rendered a placeholder) - continue; - } - - // Now render the line and any messages - const messages = getMessagesForLineNumber(lineMessages, lineNumber); - const highlightLine = messages.length > 0 || hasOnlyGeneralMessages; - const contentLineDetails = Object.assign({}, line, { - contentType: highlightLine - ? LineContentType.CONTENT_HIGHLIGHTED - : LineContentType.CONTENT_NORMAL, - visibility: lineWhenCollapsed - ? LineVisibility.ALWAYS - : LineVisibility.WHEN_EXPANDED, - }); - lineContainer.append( - SnippetRenderer.renderSnippetLine(dom, tmpl, contentLineDetails) - ); - - messages.forEach(message => { - lineContainer.append(SnippetRenderer.renderMessage(dom, tmpl, message)); - }); - } - - return lineContainer; - } - - /** - * @param {DOM} dom - * @param {ParentNode} templateContext - * @param {LH.Audit.Details.SnippetValue} details - * @param {DetailsRenderer} detailsRenderer - * @return {!Element} - */ - static render(dom, templateContext, details, detailsRenderer) { - const tmpl = dom.cloneTemplate('#tmpl-lh-snippet', templateContext); - const snippetEl = dom.find('.lh-snippet', tmpl); - - const header = SnippetRenderer.renderHeader( - dom, - tmpl, - details, - detailsRenderer, - () => snippetEl.classList.toggle('lh-snippet--expanded') - ); - const content = SnippetRenderer.renderSnippetContent(dom, tmpl, details); - snippetEl.append(header, content); - - return snippetEl; - } - } - - /** - * @license Copyright 2020 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. - */ - - /** - * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot - * @param {LH.Artifacts.Rect} rect - * @return {boolean} - */ - function screenshotOverlapsRect(screenshot, rect) { - return rect.left <= screenshot.width && - 0 <= rect.right && - rect.top <= screenshot.height && - 0 <= rect.bottom; - } - - /** - * @param {number} value - * @param {number} min - * @param {number} max - */ - function clamp(value, min, max) { - if (value < min) return min; - if (value > max) return max; - return value; - } - - /** - * @param {Rect} rect - */ - function getRectCenterPoint(rect) { - return { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2, - }; - } - - class ElementScreenshotRenderer { - /** - * Given the location of an element and the sizes of the preview and screenshot, - * compute the absolute positions (in screenshot coordinate scale) of the screenshot content - * and the highlighted rect around the element. - * @param {Rect} elementRectSC - * @param {Size} elementPreviewSizeSC - * @param {Size} screenshotSize - */ - static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) { - const elementRectCenter = getRectCenterPoint(elementRectSC); - - // Try to center clipped region. - const screenshotLeftVisibleEdge = clamp( - elementRectCenter.x - elementPreviewSizeSC.width / 2, - 0, screenshotSize.width - elementPreviewSizeSC.width - ); - const screenshotTopVisisbleEdge = clamp( - elementRectCenter.y - elementPreviewSizeSC.height / 2, - 0, screenshotSize.height - elementPreviewSizeSC.height - ); - - return { - screenshot: { - left: screenshotLeftVisibleEdge, - top: screenshotTopVisisbleEdge, - }, - clip: { - left: elementRectSC.left - screenshotLeftVisibleEdge, - top: elementRectSC.top - screenshotTopVisisbleEdge, - }, - }; - } - - /** - * Render a clipPath SVG element to assist marking the element's rect. - * The elementRect and previewSize are in screenshot coordinate scale. - * @param {DOM} dom - * @param {HTMLElement} maskEl - * @param {{left: number, top: number}} positionClip - * @param {LH.Artifacts.Rect} elementRect - * @param {Size} elementPreviewSize - */ - static renderClipPathInScreenshot(dom, maskEl, positionClip, elementRect, elementPreviewSize) { - const clipPathEl = dom.find('clipPath', maskEl); - const clipId = `clip-${Util.getUniqueSuffix()}`; - clipPathEl.id = clipId; - maskEl.style.clipPath = `url(#${clipId})`; - - // Normalize values between 0-1. - const top = positionClip.top / elementPreviewSize.height; - const bottom = top + elementRect.height / elementPreviewSize.height; - const left = positionClip.left / elementPreviewSize.width; - const right = left + elementRect.width / elementPreviewSize.width; - - const polygonsPoints = [ - `0,0 1,0 1,${top} 0,${top}`, - `0,${bottom} 1,${bottom} 1,1 0,1`, - `0,${top} ${left},${top} ${left},${bottom} 0,${bottom}`, - `${right},${top} 1,${top} 1,${bottom} ${right},${bottom}`, - ]; - for (const points of polygonsPoints) { - clipPathEl.append(dom.createElementNS( - 'http://www.w3.org/2000/svg', 'polygon', undefined, {points})); - } - } - - /** - * Called by report renderer. Defines a css variable used by any element screenshots - * in the provided report element. - * Allows for multiple Lighthouse reports to be rendered on the page, each with their - * own full page screenshot. - * @param {HTMLElement} el - * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot - */ - static installFullPageScreenshot(el, screenshot) { - el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`); - } - - /** - * Installs the lightbox elements and wires up click listeners to all .lh-element-screenshot elements. - * @param {InstallOverlayFeatureParams} opts - */ - static installOverlayFeature(opts) { - const {dom, reportEl, overlayContainerEl, templateContext, 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); - - // Add a single listener to the provided element to handle all clicks within (event delegation). - reportEl.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. - const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot')); - if (!el) return; - - const overlay = dom.createElement('div', 'lh-element-screenshot__overlay'); - overlayContainerEl.append(overlay); - - // The newly-added overlay has the dimensions we need. - const maxLightboxSize = { - width: overlay.clientWidth * 0.95, - height: overlay.clientHeight * 0.80, - }; - - const elementRectSC = { - width: Number(el.dataset['rectWidth']), - height: Number(el.dataset['rectHeight']), - left: Number(el.dataset['rectLeft']), - right: Number(el.dataset['rectLeft']) + Number(el.dataset['rectWidth']), - top: Number(el.dataset['rectTop']), - bottom: Number(el.dataset['rectTop']) + Number(el.dataset['rectHeight']), - }; - const screenshotElement = ElementScreenshotRenderer.render( - dom, - templateContext, - fullPageScreenshot.screenshot, - elementRectSC, - maxLightboxSize - ); - - // This would be unexpected here. - // When `screenshotElement` is `null`, there is also no thumbnail element for the user to have clicked to make it this far. - if (!screenshotElement) { - overlay.remove(); - return; - } - overlay.appendChild(screenshotElement); - overlay.addEventListener('click', () => overlay.remove()); - }); - } - - /** - * Given the size of the element in the screenshot and the total available size of our preview container, - * compute the factor by which we need to zoom out to view the entire element with context. - * @param {LH.Artifacts.Rect} elementRectSC - * @param {Size} renderContainerSizeDC - * @return {number} - */ - static _computeZoomFactor(elementRectSC, renderContainerSizeDC) { - const targetClipToViewportRatio = 0.75; - const zoomRatioXY = { - x: renderContainerSizeDC.width / elementRectSC.width, - y: renderContainerSizeDC.height / elementRectSC.height, - }; - const zoomFactor = targetClipToViewportRatio * Math.min(zoomRatioXY.x, zoomRatioXY.y); - return Math.min(1, zoomFactor); - } - - /** - * Renders an element with surrounding context from the full page screenshot. - * Used to render both the thumbnail preview in details tables and the full-page screenshot in the lightbox. - * Returns null if element rect is outside screenshot bounds. - * @param {DOM} dom - * @param {ParentNode} templateContext - * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot - * @param {LH.Artifacts.Rect} elementRectSC Region of screenshot to highlight. - * @param {Size} maxRenderSizeDC e.g. maxThumbnailSize or maxLightboxSize. - * @return {Element|null} - */ - static render(dom, templateContext, screenshot, elementRectSC, maxRenderSizeDC) { - if (!screenshotOverlapsRect(screenshot, elementRectSC)) { - return null; - } - - const tmpl = dom.cloneTemplate('#tmpl-lh-element-screenshot', templateContext); - const containerEl = dom.find('div.lh-element-screenshot', tmpl); - - containerEl.dataset['rectWidth'] = elementRectSC.width.toString(); - containerEl.dataset['rectHeight'] = elementRectSC.height.toString(); - containerEl.dataset['rectLeft'] = elementRectSC.left.toString(); - containerEl.dataset['rectTop'] = elementRectSC.top.toString(); - - // Zoom out when highlighted region takes up most of the viewport. - // This provides more context for where on the page this element is. - const zoomFactor = this._computeZoomFactor(elementRectSC, maxRenderSizeDC); - - const elementPreviewSizeSC = { - width: maxRenderSizeDC.width / zoomFactor, - height: maxRenderSizeDC.height / zoomFactor, - }; - elementPreviewSizeSC.width = Math.min(screenshot.width, elementPreviewSizeSC.width); - /* This preview size is either the size of the thumbnail or size of the Lightbox */ - const elementPreviewSizeDC = { - width: elementPreviewSizeSC.width * zoomFactor, - height: elementPreviewSizeSC.height * zoomFactor, - }; - - const positions = ElementScreenshotRenderer.getScreenshotPositions( - elementRectSC, - elementPreviewSizeSC, - {width: screenshot.width, height: screenshot.height} - ); - - const contentEl = dom.find('div.lh-element-screenshot__content', containerEl); - contentEl.style.top = `-${elementPreviewSizeDC.height}px`; - - const imageEl = dom.find('div.lh-element-screenshot__image', containerEl); - imageEl.style.width = elementPreviewSizeDC.width + 'px'; - imageEl.style.height = elementPreviewSizeDC.height + 'px'; - - imageEl.style.backgroundPositionY = -(positions.screenshot.top * zoomFactor) + 'px'; - imageEl.style.backgroundPositionX = -(positions.screenshot.left * zoomFactor) + 'px'; - imageEl.style.backgroundSize = - `${screenshot.width * zoomFactor}px ${screenshot.height * zoomFactor}px`; - - const markerEl = dom.find('div.lh-element-screenshot__element-marker', containerEl); - markerEl.style.width = elementRectSC.width * zoomFactor + 'px'; - markerEl.style.height = elementRectSC.height * zoomFactor + 'px'; - markerEl.style.left = positions.clip.left * zoomFactor + 'px'; - markerEl.style.top = positions.clip.top * zoomFactor + 'px'; - - const maskEl = dom.find('div.lh-element-screenshot__mask', containerEl); - maskEl.style.width = elementPreviewSizeDC.width + 'px'; - maskEl.style.height = elementPreviewSizeDC.height + 'px'; - - ElementScreenshotRenderer.renderClipPathInScreenshot( - dom, - maskEl, - positions.clip, - elementRectSC, - elementPreviewSizeSC - ); - - return containerEl; - } - } - - /** - * @license - * Copyright 2017 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. - */ - - const URL_PREFIXES = ['http://', 'https://', 'data:']; - - class DetailsRenderer { - /** - * @param {DOM} dom - * @param {{fullPageScreenshot?: LH.Artifacts.FullPageScreenshot}} [options] - */ - constructor(dom, options = {}) { - this._dom = dom; - this._fullPageScreenshot = options.fullPageScreenshot; - - /** @type {ParentNode} */ - this._templateContext; // eslint-disable-line no-unused-expressions - } - - /** - * @param {ParentNode} context - */ - setTemplateContext(context) { - this._templateContext = context; - } - - /** - * @param {AuditDetails} details - * @return {Element|null} - */ - render(details) { - switch (details.type) { - case 'filmstrip': - return this._renderFilmstrip(details); - case 'list': - return this._renderList(details); - case 'table': - return this._renderTable(details); - case 'criticalrequestchain': - return CriticalRequestChainRenderer.render(this._dom, this._templateContext, details, this); - case 'opportunity': - return this._renderTable(details); - - // Internal-only details, not for rendering. - case 'screenshot': - case 'debugdata': - case 'full-page-screenshot': - case 'treemap-data': - return null; - - default: { - // @ts-expect-error tsc thinks this is unreachable, but be forward compatible - // with new unexpected detail types. - return this._renderUnknown(details.type, details); - } - } - } - - /** - * @param {{value: number, granularity?: number}} details - * @return {Element} - */ - _renderBytes(details) { - // TODO: handle displayUnit once we have something other than 'kb' - // Note that 'kb' is historical and actually represents KiB. - const value = Util.i18n.formatBytesToKiB(details.value, details.granularity); - const textEl = this._renderText(value); - textEl.title = Util.i18n.formatBytes(details.value); - return textEl; - } - - /** - * @param {{value: number, granularity?: number, displayUnit?: string}} details - * @return {Element} - */ - _renderMilliseconds(details) { - let value = Util.i18n.formatMilliseconds(details.value, details.granularity); - if (details.displayUnit === 'duration') { - value = Util.i18n.formatDuration(details.value); - } - - return this._renderText(value); - } - - /** - * @param {string} text - * @return {HTMLElement} - */ - renderTextURL(text) { - const url = text; - - let displayedPath; - let displayedHost; - let title; - try { - const parsed = Util.parseURL(url); - displayedPath = parsed.file === '/' ? parsed.origin : parsed.file; - displayedHost = parsed.file === '/' || parsed.hostname === '' ? '' : `(${parsed.hostname})`; - title = url; - } catch (e) { - displayedPath = url; - } - - const element = this._dom.createElement('div', 'lh-text__url'); - element.appendChild(this._renderLink({text: displayedPath, url})); - - if (displayedHost) { - const hostElem = this._renderText(displayedHost); - hostElem.classList.add('lh-text__url-host'); - element.appendChild(hostElem); - } - - if (title) { - element.title = url; - // set the url on the element's dataset which we use to check 3rd party origins - element.dataset.url = url; - } - return element; - } - - /** - * @param {{text: string, url: string}} details - * @return {HTMLElement} - */ - _renderLink(details) { - const allowedProtocols = ['https:', 'http:']; - let url; - try { - url = new URL(details.url); - } catch (_) {} - - if (!url || !allowedProtocols.includes(url.protocol)) { - // Fall back to just the link text if invalid or protocol not allowed. - const element = this._renderText(details.text); - element.classList.add('lh-link'); - return element; - } - - const a = this._dom.createElement('a'); - a.rel = 'noopener'; - a.target = '_blank'; - a.textContent = details.text; - a.href = url.href; - a.classList.add('lh-link'); - return a; - } - - /** - * @param {string} text - * @return {HTMLDivElement} - */ - _renderText(text) { - const element = this._dom.createElement('div', 'lh-text'); - element.textContent = text; - return element; - } - - /** - * @param {{value: number, granularity?: number}} details - * @return {Element} - */ - _renderNumeric(details) { - const value = Util.i18n.formatNumber(details.value, details.granularity); - const element = this._dom.createElement('div', 'lh-numeric'); - element.textContent = value; - return element; - } - - /** - * Create small thumbnail with scaled down image asset. - * @param {string} details - * @return {Element} - */ - _renderThumbnail(details) { - const element = this._dom.createElement('img', 'lh-thumbnail'); - const strValue = details; - element.src = strValue; - element.title = strValue; - element.alt = ''; - return element; - } - - /** - * @param {string} type - * @param {*} value - */ - _renderUnknown(type, value) { - // eslint-disable-next-line no-console - console.error(`Unknown details type: ${type}`, value); - const element = this._dom.createElement('details', 'lh-unknown'); - this._dom.createChildOf(element, 'summary').textContent = - `We don't know how to render audit details of type \`${type}\`. ` + - 'The Lighthouse version that collected this data is likely newer than the Lighthouse ' + - 'version of the report renderer. Expand for the raw JSON.'; - this._dom.createChildOf(element, 'pre').textContent = JSON.stringify(value, null, 2); - return element; - } - - /** - * Render a details item value for embedding in a table. Renders the value - * based on the heading's valueType, unless the value itself has a `type` - * property to override it. - * @param {TableItemValue} value - * @param {LH.Audit.Details.OpportunityColumnHeading} heading - * @return {Element|null} - */ - _renderTableValue(value, heading) { - if (value === undefined || value === null) { - return null; - } - - // First deal with the possible object forms of value. - if (typeof value === 'object') { - // The value's type overrides the heading's for this column. - switch (value.type) { - case 'code': { - return this._renderCode(value.value); - } - case 'link': { - return this._renderLink(value); - } - case 'node': { - return this.renderNode(value); - } - case 'numeric': { - return this._renderNumeric(value); - } - case 'source-location': { - return this.renderSourceLocation(value); - } - case 'url': { - return this.renderTextURL(value.value); - } - default: { - return this._renderUnknown(value.type, value); - } - } - } - - // Next, deal with primitives. - switch (heading.valueType) { - case 'bytes': { - const numValue = Number(value); - return this._renderBytes({value: numValue, granularity: heading.granularity}); - } - case 'code': { - const strValue = String(value); - return this._renderCode(strValue); - } - case 'ms': { - const msValue = { - value: Number(value), - granularity: heading.granularity, - displayUnit: heading.displayUnit, - }; - return this._renderMilliseconds(msValue); - } - case 'numeric': { - const numValue = Number(value); - return this._renderNumeric({value: numValue, granularity: heading.granularity}); - } - case 'text': { - const strValue = String(value); - return this._renderText(strValue); - } - case 'thumbnail': { - const strValue = String(value); - return this._renderThumbnail(strValue); - } - case 'timespanMs': { - const numValue = Number(value); - return this._renderMilliseconds({value: numValue}); - } - case 'url': { - const strValue = String(value); - if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) { - return this.renderTextURL(strValue); - } else { - // Fall back to
 rendering if not actually a URL.
-            return this._renderCode(strValue);
-          }
-        }
-        default: {
-          return this._renderUnknown(heading.valueType, value);
-        }
-      }
-    }
-
-    /**
-     * Get the headings of a table-like details object, converted into the
-     * OpportunityColumnHeading type until we have all details use the same
-     * heading format.
-     * @param {Table|OpportunityTable} tableLike
-     * @return {OpportunityTable['headings']}
-     */
-    _getCanonicalizedHeadingsFromTable(tableLike) {
-      if (tableLike.type === 'opportunity') {
-        return tableLike.headings;
-      }
-
-      return tableLike.headings.map(heading => this._getCanonicalizedHeading(heading));
-    }
-
-    /**
-     * Get the headings of a table-like details object, converted into the
-     * OpportunityColumnHeading type until we have all details use the same
-     * heading format.
-     * @param {Table['headings'][number]} heading
-     * @return {OpportunityTable['headings'][number]}
-     */
-    _getCanonicalizedHeading(heading) {
-      let subItemsHeading;
-      if (heading.subItemsHeading) {
-        subItemsHeading = this._getCanonicalizedsubItemsHeading(heading.subItemsHeading, heading);
-      }
-
-      return {
-        key: heading.key,
-        valueType: heading.itemType,
-        subItemsHeading,
-        label: heading.text,
-        displayUnit: heading.displayUnit,
-        granularity: heading.granularity,
-      };
-    }
-
-    /**
-     * @param {Exclude} subItemsHeading
-     * @param {LH.Audit.Details.TableColumnHeading} parentHeading
-     * @return {LH.Audit.Details.OpportunityColumnHeading['subItemsHeading']}
-     */
-    _getCanonicalizedsubItemsHeading(subItemsHeading, parentHeading) {
-      // Low-friction way to prevent commiting a falsy key (which is never allowed for
-      // a subItemsHeading) from passing in CI.
-      if (!subItemsHeading.key) {
-        // eslint-disable-next-line no-console
-        console.warn('key should not be null');
-      }
-
-      return {
-        key: subItemsHeading.key || '',
-        valueType: subItemsHeading.itemType || parentHeading.itemType,
-        granularity: subItemsHeading.granularity || parentHeading.granularity,
-        displayUnit: subItemsHeading.displayUnit || parentHeading.displayUnit,
-      };
-    }
-
-    /**
-     * Returns a new heading where the values are defined first by `heading.subItemsHeading`,
-     * and secondly by `heading`. If there is no subItemsHeading, returns null, which will
-     * be rendered as an empty column.
-     * @param {LH.Audit.Details.OpportunityColumnHeading} heading
-     * @return {LH.Audit.Details.OpportunityColumnHeading | null}
-     */
-    _getDerivedsubItemsHeading(heading) {
-      if (!heading.subItemsHeading) return null;
-      return {
-        key: heading.subItemsHeading.key || '',
-        valueType: heading.subItemsHeading.valueType || heading.valueType,
-        granularity: heading.subItemsHeading.granularity || heading.granularity,
-        displayUnit: heading.subItemsHeading.displayUnit || heading.displayUnit,
-        label: '',
-      };
-    }
-
-    /**
-     * @param {TableItem} item
-     * @param {(LH.Audit.Details.OpportunityColumnHeading | null)[]} headings
-     */
-    _renderTableRow(item, headings) {
-      const rowElem = this._dom.createElement('tr');
-
-      for (const heading of headings) {
-        // Empty cell if no heading or heading key for this column.
-        if (!heading || !heading.key) {
-          this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
-          continue;
-        }
-
-        const value = item[heading.key];
-        let valueElement;
-        if (value !== undefined && value !== null) {
-          valueElement = this._renderTableValue(value, heading);
-        }
-
-        if (valueElement) {
-          const classes = `lh-table-column--${heading.valueType}`;
-          this._dom.createChildOf(rowElem, 'td', classes).appendChild(valueElement);
-        } else {
-          // Empty cell is rendered for a column if:
-          // - the pair is null
-          // - the heading key is null
-          // - the value is undefined/null
-          this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
-        }
-      }
-
-      return rowElem;
-    }
-
-    /**
-     * Renders one or more rows from a details table item. A single table item can
-     * expand into multiple rows, if there is a subItemsHeading.
-     * @param {TableItem} item
-     * @param {LH.Audit.Details.OpportunityColumnHeading[]} headings
-     */
-    _renderTableRowsFromItem(item, headings) {
-      const fragment = this._dom.createFragment();
-      fragment.append(this._renderTableRow(item, headings));
-
-      if (!item.subItems) return fragment;
-
-      const subItemsHeadings = headings.map(this._getDerivedsubItemsHeading);
-      if (!subItemsHeadings.some(Boolean)) return fragment;
-
-      for (const subItem of item.subItems.items) {
-        const rowEl = this._renderTableRow(subItem, subItemsHeadings);
-        rowEl.classList.add('lh-sub-item-row');
-        fragment.append(rowEl);
-      }
-
-      return fragment;
-    }
-
-    /**
-     * @param {OpportunityTable|Table} details
-     * @return {Element}
-     */
-    _renderTable(details) {
-      if (!details.items.length) return this._dom.createElement('span');
-
-      const tableElem = this._dom.createElement('table', 'lh-table');
-      const theadElem = this._dom.createChildOf(tableElem, 'thead');
-      const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
-
-      const headings = this._getCanonicalizedHeadingsFromTable(details);
-
-      for (const heading of headings) {
-        const valueType = heading.valueType || 'text';
-        const classes = `lh-table-column--${valueType}`;
-        const labelEl = this._dom.createElement('div', 'lh-text');
-        labelEl.textContent = heading.label;
-        this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl);
-      }
-
-      const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
-      let even = true;
-      for (const item of details.items) {
-        const rowsFragment = this._renderTableRowsFromItem(item, headings);
-        for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
-          // For zebra styling.
-          rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
-        }
-        even = !even;
-        tbodyElem.append(rowsFragment);
-      }
-
-      return tableElem;
-    }
-
-    /**
-     * @param {LH.Audit.Details.List} details
-     * @return {Element}
-     */
-    _renderList(details) {
-      const listContainer = this._dom.createElement('div', 'lh-list');
-
-      details.items.forEach(item => {
-        const snippetEl = SnippetRenderer.render(this._dom, this._templateContext, item, this);
-        listContainer.appendChild(snippetEl);
-      });
-
-      return listContainer;
-    }
-
-    /**
-     * @param {LH.Audit.Details.NodeValue} item
-     * @return {Element}
-     */
-    renderNode(item) {
-      const element = this._dom.createElement('span', 'lh-node');
-      if (item.nodeLabel) {
-        const nodeLabelEl = this._dom.createElement('div');
-        nodeLabelEl.textContent = item.nodeLabel;
-        element.appendChild(nodeLabelEl);
-      }
-      if (item.snippet) {
-        const snippetEl = this._dom.createElement('div');
-        snippetEl.classList.add('lh-node__snippet');
-        snippetEl.textContent = item.snippet;
-        element.appendChild(snippetEl);
-      }
-      if (item.selector) {
-        element.title = item.selector;
-      }
-      if (item.path) element.setAttribute('data-path', item.path);
-      if (item.selector) element.setAttribute('data-selector', item.selector);
-      if (item.snippet) element.setAttribute('data-snippet', item.snippet);
-
-      if (!this._fullPageScreenshot) return element;
-
-      const rect = item.lhId && this._fullPageScreenshot.nodes[item.lhId];
-      if (!rect || rect.width === 0 || rect.height === 0) return element;
-
-      const maxThumbnailSize = {width: 147, height: 100};
-      const elementScreenshot = ElementScreenshotRenderer.render(
-        this._dom,
-        this._templateContext,
-        this._fullPageScreenshot.screenshot,
-        rect,
-        maxThumbnailSize
-      );
-      if (elementScreenshot) element.prepend(elementScreenshot);
-
-      return element;
-    }
-
-    /**
-     * @param {LH.Audit.Details.SourceLocationValue} item
-     * @return {Element|null}
-     * @protected
-     */
-    renderSourceLocation(item) {
-      if (!item.url) {
-        return null;
-      }
-
-      // Lines are shown as one-indexed.
-      const generatedLocation = `${item.url}:${item.line + 1}:${item.column}`;
-      let sourceMappedOriginalLocation;
-      if (item.original) {
-        const file = item.original.file || '';
-        sourceMappedOriginalLocation = `${file}:${item.original.line + 1}:${item.original.column}`;
-      }
-
-      // We render slightly differently based on presence of source map and provenance of URL.
-      let element;
-      if (item.urlProvider === 'network' && sourceMappedOriginalLocation) {
-        element = this._renderLink({
-          url: item.url,
-          text: sourceMappedOriginalLocation,
-        });
-        element.title = `maps to generated location ${generatedLocation}`;
-      } else if (item.urlProvider === 'network' && !sourceMappedOriginalLocation) {
-        element = this.renderTextURL(item.url);
-        this._dom.find('.lh-link', element).textContent += `:${item.line + 1}:${item.column}`;
-      } else if (item.urlProvider === 'comment' && sourceMappedOriginalLocation) {
-        element = this._renderText(`${sourceMappedOriginalLocation} (from source map)`);
-        element.title = `${generatedLocation} (from sourceURL)`;
-      } else if (item.urlProvider === 'comment' && !sourceMappedOriginalLocation) {
-        element = this._renderText(`${generatedLocation} (from sourceURL)`);
-      } else {
-        return null;
-      }
-
-      element.classList.add('lh-source-location');
-      element.setAttribute('data-source-url', item.url);
-      // DevTools expects zero-indexed lines.
-      element.setAttribute('data-source-line', String(item.line));
-      element.setAttribute('data-source-column', String(item.column));
-
-      return element;
-    }
-
-    /**
-     * @param {LH.Audit.Details.Filmstrip} details
-     * @return {Element}
-     */
-    _renderFilmstrip(details) {
-      const filmstripEl = this._dom.createElement('div', 'lh-filmstrip');
-
-      for (const thumbnail of details.items) {
-        const frameEl = this._dom.createChildOf(filmstripEl, 'div', 'lh-filmstrip__frame');
-        this._dom.createChildOf(frameEl, 'img', 'lh-filmstrip__thumbnail', {
-          src: thumbnail.data,
-          alt: `Screenshot`,
-        });
-      }
-      return filmstripEl;
-    }
-
-    /**
-     * @param {string} text
-     * @return {Element}
-     */
-    _renderCode(text) {
-      const pre = this._dom.createElement('pre', 'lh-code');
-      pre.textContent = text;
-      return pre;
-    }
-  }
-
-  /**
-   * @license Copyright 2020 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.
-   */
-
-  // Not named `NBSP` because that creates a duplicate identifier (util.js).
-  const NBSP2 = '\xa0';
-  const KiB = 1024;
-  const MiB = KiB * KiB;
-
-  /**
-   * @template T
-   */
-  class I18n {
-    /**
-     * @param {LH.Locale} locale
-     * @param {T} strings
-     */
-    constructor(locale, strings) {
-      // When testing, use a locale with more exciting numeric formatting.
-      if (locale === 'en-XA') locale = 'de';
-
-      this._numberDateLocale = locale;
-      this._numberFormatter = new Intl.NumberFormat(locale);
-      this._percentFormatter = new Intl.NumberFormat(locale, {style: 'percent'});
-      this._strings = strings;
-    }
-
-    get strings() {
-      return this._strings;
-    }
-
-    /**
-     * Format number.
-     * @param {number} number
-     * @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
-     * @return {string}
-     */
-    formatNumber(number, granularity = 0.1) {
-      const coarseValue = Math.round(number / granularity) * granularity;
-      return this._numberFormatter.format(coarseValue);
-    }
-
-    /**
-     * Format percent.
-     * @param {number} number 0–1
-     * @return {string}
-     */
-    formatPercent(number) {
-      return this._percentFormatter.format(number);
-    }
-
-    /**
-     * @param {number} size
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
-     * @return {string}
-     */
-    formatBytesToKiB(size, granularity = 0.1) {
-      const formatter = this._byteFormatterForGranularity(granularity);
-      const kbs = formatter.format(Math.round(size / 1024 / granularity) * granularity);
-      return `${kbs}${NBSP2}KiB`;
-    }
-
-    /**
-     * @param {number} size
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
-     * @return {string}
-     */
-    formatBytesToMiB(size, granularity = 0.1) {
-      const formatter = this._byteFormatterForGranularity(granularity);
-      const kbs = formatter.format(Math.round(size / (1024 ** 2) / granularity) * granularity);
-      return `${kbs}${NBSP2}MiB`;
-    }
-
-    /**
-     * @param {number} size
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
-     * @return {string}
-     */
-    formatBytes(size, granularity = 1) {
-      const formatter = this._byteFormatterForGranularity(granularity);
-      const kbs = formatter.format(Math.round(size / granularity) * granularity);
-      return `${kbs}${NBSP2}bytes`;
-    }
-
-    /**
-     * @param {number} size
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
-     * @return {string}
-     */
-    formatBytesWithBestUnit(size, granularity = 0.1) {
-      if (size >= MiB) return this.formatBytesToMiB(size, granularity);
-      if (size >= KiB) return this.formatBytesToKiB(size, granularity);
-      return this.formatNumber(size, granularity) + '\xa0B';
-    }
-
-    /**
-     * Format bytes with a constant number of fractional digits, i.e for a granularity of 0.1, 10 becomes '10.0'
-     * @param {number} granularity Controls how coarse the displayed value is
-     * @return {Intl.NumberFormat}
-     */
-    _byteFormatterForGranularity(granularity) {
-      // assume any granularity above 1 will not contain fractional parts, i.e. will never be 1.5
-      let numberOfFractionDigits = 0;
-      if (granularity < 1) {
-        numberOfFractionDigits = -Math.floor(Math.log10(granularity));
-      }
-
-      return new Intl.NumberFormat(this._numberDateLocale, {
-        ...this._numberFormatter.resolvedOptions(),
-        maximumFractionDigits: numberOfFractionDigits,
-        minimumFractionDigits: numberOfFractionDigits,
-      });
-    }
-
-    /**
-     * @param {number} ms
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
-     * @return {string}
-     */
-    formatMilliseconds(ms, granularity = 10) {
-      const coarseTime = Math.round(ms / granularity) * granularity;
-      return coarseTime === 0
-        ? `${this._numberFormatter.format(0)}${NBSP2}ms`
-        : `${this._numberFormatter.format(coarseTime)}${NBSP2}ms`;
-    }
-
-    /**
-     * @param {number} ms
-     * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
-     * @return {string}
-     */
-    formatSeconds(ms, granularity = 0.1) {
-      const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
-      return `${this._numberFormatter.format(coarseTime)}${NBSP2}s`;
-    }
-
-    /**
-     * Format time.
-     * @param {string} date
-     * @return {string}
-     */
-    formatDateTime(date) {
-      /** @type {Intl.DateTimeFormatOptions} */
-      const options = {
-        month: 'short', day: 'numeric', year: 'numeric',
-        hour: 'numeric', minute: 'numeric', timeZoneName: 'short',
-      };
-
-      // Force UTC if runtime timezone could not be detected.
-      // See https://github.com/GoogleChrome/lighthouse/issues/1056
-      // and https://github.com/GoogleChrome/lighthouse/pull/9822
-      let formatter;
-      try {
-        formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
-      } catch (err) {
-        options.timeZone = 'UTC';
-        formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
-      }
-
-      return formatter.format(new Date(date));
-    }
-
-    /**
-     * Converts a time in milliseconds into a duration string, i.e. `1d 2h 13m 52s`
-     * @param {number} timeInMilliseconds
-     * @return {string}
-     */
-    formatDuration(timeInMilliseconds) {
-      let timeInSeconds = timeInMilliseconds / 1000;
-      if (Math.round(timeInSeconds) === 0) {
-        return 'None';
-      }
-
-      /** @type {Array} */
-      const parts = [];
-      /** @type {Record} */
-      const unitLabels = {
-        d: 60 * 60 * 24,
-        h: 60 * 60,
-        m: 60,
-        s: 1,
-      };
-
-      Object.keys(unitLabels).forEach(label => {
-        const unit = unitLabels[label];
-        const numberOfUnits = Math.floor(timeInSeconds / unit);
-        if (numberOfUnits > 0) {
-          timeInSeconds -= numberOfUnits * unit;
-          parts.push(`${numberOfUnits}\xa0${label}`);
-        }
-      });
-
-      return parts.join(' ');
-    }
-  }
-
-  /**
-   * @license
-   * Copyright 2018 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.
-   */
-
-  class PerformanceCategoryRenderer extends CategoryRenderer {
-    /**
-     * @param {LH.ReportResult.AuditRef} audit
-     * @return {!Element}
-     */
-    _renderMetric(audit) {
-      const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
-      const element = this.dom.find('.lh-metric', tmpl);
-      element.id = audit.result.id;
-      const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
-      element.classList.add(`lh-metric--${rating}`);
-
-      const titleEl = this.dom.find('.lh-metric__title', tmpl);
-      titleEl.textContent = audit.result.title;
-
-      const valueEl = this.dom.find('.lh-metric__value', tmpl);
-      valueEl.textContent = audit.result.displayValue || '';
-
-      const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
-      descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
-
-      if (audit.result.scoreDisplayMode === 'error') {
-        descriptionEl.textContent = '';
-        valueEl.textContent = 'Error!';
-        const tooltip = this.dom.createChildOf(descriptionEl, 'span');
-        tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
-      }
-
-      return element;
-    }
-
-    /**
-     * @param {LH.ReportResult.AuditRef} audit
-     * @param {number} scale
-     * @return {!Element}
-     */
-    _renderOpportunity(audit, scale) {
-      const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
-      const element = this.populateAuditValues(audit, oppTmpl);
-      element.id = audit.result.id;
-
-      if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
-        return element;
-      }
-      const details = audit.result.details;
-      if (details.type !== 'opportunity') {
-        return element;
-      }
-
-      // Overwrite the displayValue with opportunity's wastedMs
-      // TODO: normalize this to one tagName.
-      const displayEl =
-        this.dom.find('span.lh-audit__display-text, div.lh-audit__display-text', element);
-      const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
-      this.dom.find('div.lh-sparkline__bar', element).style.width = sparklineWidthPct;
-      displayEl.textContent = Util.i18n.formatSeconds(details.overallSavingsMs, 0.01);
-
-      // Set [title] tooltips
-      if (audit.result.displayValue) {
-        const displayValue = audit.result.displayValue;
-        this.dom.find('div.lh-load-opportunity__sparkline', element).title = displayValue;
-        displayEl.title = displayValue;
-      }
-
-      return element;
-    }
-
-    /**
-     * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
-     * Opportunities with an error won't have a details object, so MIN_VALUE is returned to keep any
-     * erroring opportunities last in sort order.
-     * @param {LH.ReportResult.AuditRef} audit
-     * @return {number}
-     */
-    _getWastedMs(audit) {
-      if (audit.result.details && audit.result.details.type === 'opportunity') {
-        const details = audit.result.details;
-        if (typeof details.overallSavingsMs !== 'number') {
-          throw new Error('non-opportunity details passed to _getWastedMs');
-        }
-        return details.overallSavingsMs;
-      } else {
-        return Number.MIN_VALUE;
-      }
-    }
-
-    /**
-     * Get a link to the interactive scoring calculator with the metric values.
-     * @param {LH.ReportResult.AuditRef[]} auditRefs
-     * @return {string}
-     */
-    _getScoringCalculatorHref(auditRefs) {
-      // TODO: filter by !!acronym when dropping renderer support of v7 LHRs.
-      const metrics = auditRefs.filter(audit => audit.group === 'metrics');
-      const fci = auditRefs.find(audit => audit.id === 'first-cpu-idle');
-      const fmp = auditRefs.find(audit => audit.id === 'first-meaningful-paint');
-      if (fci) metrics.push(fci);
-      if (fmp) metrics.push(fmp);
-
-      /**
-       * Clamp figure to 2 decimal places
-       * @param {number} val
-       * @return {number}
-       */
-      const clampTo2Decimals = val => Math.round(val * 100) / 100;
-
-      const metricPairs = metrics.map(audit => {
-        let value;
-        if (typeof audit.result.numericValue === 'number') {
-          value = audit.id === 'cumulative-layout-shift' ?
-            clampTo2Decimals(audit.result.numericValue) :
-            Math.round(audit.result.numericValue);
-          value = value.toString();
-        } else {
-          value = 'null';
-        }
-        return [audit.acronym || audit.id, value];
-      });
-      const paramPairs = [...metricPairs];
-
-      if (Util.reportJson) {
-        paramPairs.push(['device', Util.reportJson.configSettings.formFactor]);
-        paramPairs.push(['version', Util.reportJson.lighthouseVersion]);
-      }
-
-      const params = new URLSearchParams(paramPairs);
-      const url = new URL('https://googlechrome.github.io/lighthouse/scorecalc/');
-      url.hash = params.toString();
-      return url.href;
-    }
-
-    /**
-     * @param {LH.ReportResult.Category} category
-     * @param {Object} groups
-     * @param {'PSI'=} environment 'PSI' and undefined are the only valid values
-     * @return {Element}
-     * @override
-     */
-    render(category, groups, environment) {
-      const strings = Util.i18n.strings;
-      const element = this.dom.createElement('div', 'lh-category');
-      if (environment === 'PSI') {
-        const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
-        gaugeEl.appendChild(this.renderScoreGauge(category, groups));
-        element.appendChild(gaugeEl);
-      } else {
-        this.createPermalinkSpan(element, category.id);
-        element.appendChild(this.renderCategoryHeader(category, groups));
-      }
-
-      // Metrics.
-      const metricAuditsEl = this.renderAuditGroup(groups.metrics);
-
-      // Metric descriptions toggle.
-      const toggleTmpl = this.dom.cloneTemplate('#tmpl-lh-metrics-toggle', this.templateContext);
-      const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
-      metricAuditsEl.append(..._toggleEl.childNodes);
-
-      const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
-      const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
-
-      metricAudits.forEach(item => {
-        metricsBoxesEl.appendChild(this._renderMetric(item));
-      });
-
-      const estValuesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics__disclaimer');
-      const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
-      estValuesEl.appendChild(disclaimerEl);
-
-      // Add link to score calculator.
-      const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
-      calculatorLink.target = '_blank';
-      calculatorLink.textContent = strings.calculatorLink;
-      calculatorLink.href = this._getScoringCalculatorHref(category.auditRefs);
-
-
-      metricAuditsEl.classList.add('lh-audit-group--metrics');
-      element.appendChild(metricAuditsEl);
-
-      // Filmstrip
-      const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
-      const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
-      const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
-      if (thumbnailResult && thumbnailResult.details) {
-        timelineEl.id = thumbnailResult.id;
-        const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
-        filmstripEl && timelineEl.appendChild(filmstripEl);
-      }
-
-      // Opportunities
-      const opportunityAudits = category.auditRefs
-          .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
-          .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
-
-
-      const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
-      // TODO: only add if there are opportunities & diagnostics rendered.
-      if (filterableMetrics.length) {
-        this.renderMetricAuditFilter(filterableMetrics, element);
-      }
-
-      if (opportunityAudits.length) {
-        // Scale the sparklines relative to savings, minimum 2s to not overstate small savings
-        const minimumScale = 2000;
-        const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
-        const maxWaste = Math.max(...wastedMsValues);
-        const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
-        const groupEl = this.renderAuditGroup(groups['load-opportunities']);
-        const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
-
-        this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
-          strings.opportunityResourceColumnLabel;
-        this.dom.find('.lh-load-opportunity__col--two', tmpl).textContent =
-          strings.opportunitySavingsColumnLabel;
-
-        const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
-        groupEl.appendChild(headerEl);
-        opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
-        groupEl.classList.add('lh-audit-group--load-opportunities');
-        element.appendChild(groupEl);
-      }
-
-      // Diagnostics
-      const diagnosticAudits = category.auditRefs
-          .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
-          .sort((a, b) => {
-            const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
-            const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
-            return scoreA - scoreB;
-          });
-
-      if (diagnosticAudits.length) {
-        const groupEl = this.renderAuditGroup(groups['diagnostics']);
-        diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
-        groupEl.classList.add('lh-audit-group--diagnostics');
-        element.appendChild(groupEl);
-      }
-
-      // Passed audits
-      const passedAudits = category.auditRefs
-          .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
-              Util.showAsPassed(audit.result));
-
-      if (!passedAudits.length) return element;
-
-      const clumpOpts = {
-        auditRefs: passedAudits,
-        groupDefinitions: groups,
-      };
-      const passedElem = this.renderClump('passed', clumpOpts);
-      element.appendChild(passedElem);
-
-      // Budgets
-      /** @type {Array} */
-      const budgetTableEls = [];
-      ['performance-budget', 'timing-budget'].forEach((id) => {
-        const audit = category.auditRefs.find(audit => audit.id === id);
-        if (audit && audit.result.details) {
-          const table = this.detailsRenderer.render(audit.result.details);
-          if (table) {
-            table.id = id;
-            table.classList.add('lh-audit');
-            budgetTableEls.push(table);
-          }
-        }
-      });
-      if (budgetTableEls.length > 0) {
-        const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
-        budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
-        budgetsGroupEl.classList.add('lh-audit-group--budgets');
-        element.appendChild(budgetsGroupEl);
-      }
-
-      return element;
-    }
-
-    /**
-     * Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
-     * @param {LH.ReportResult.AuditRef[]} filterableMetrics
-     * @param {HTMLDivElement} categoryEl
-     */
-    renderMetricAuditFilter(filterableMetrics, categoryEl) {
-      const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
-      const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
-      textEl.textContent = Util.i18n.strings.showRelevantAudits;
-
-      const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
-        ({acronym: 'All'}),
-        ...filterableMetrics,
-      ]);
-      for (const metric of filterChoices) {
-        const elemId = `metric-${metric.acronym}`;
-        const radioEl = this.dom.createChildOf(metricFilterEl, 'input', 'lh-metricfilter__radio', {
-          type: 'radio',
-          name: 'metricsfilter',
-          id: elemId,
-        });
-
-        const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', {
-          for: elemId,
-          title: metric.result && metric.result.title,
-        });
-        labelEl.textContent = metric.acronym || metric.id;
-
-        if (metric.acronym === 'All') {
-          radioEl.checked = true;
-          labelEl.classList.add('lh-metricfilter__label--active');
-        }
-        categoryEl.append(metricFilterEl);
-
-        // Toggle class/hidden state based on filter choice.
-        radioEl.addEventListener('input', _ => {
-          for (const elem of categoryEl.querySelectorAll('label.lh-metricfilter__label')) {
-            elem.classList.toggle('lh-metricfilter__label--active', elem.htmlFor === elemId);
-          }
-          categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
-
-          for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
-            if (metric.acronym === 'All') {
-              perfAuditEl.hidden = false;
-              continue;
-            }
-
-            perfAuditEl.hidden = true;
-            if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
-              perfAuditEl.hidden = false;
-            }
-          }
-
-          // Hide groups/clumps if all child audits are also hidden.
-          const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
-          for (const groupEl of groupEls) {
-            groupEl.hidden = false;
-            const childEls = Array.from(groupEl.querySelectorAll('div.lh-audit'));
-            const areAllHidden = !!childEls.length && childEls.every(auditEl => auditEl.hidden);
-            groupEl.hidden = areAllHidden;
-          }
-        });
-      }
-    }
-  }
-
-  /**
-   * @license
-   * Copyright 2018 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.
-   */
-
-  class PwaCategoryRenderer extends CategoryRenderer {
-    /**
-     * @param {LH.ReportResult.Category} category
-     * @param {Object} [groupDefinitions]
-     * @return {Element}
-     */
-    render(category, groupDefinitions = {}) {
-      const categoryElem = this.dom.createElement('div', 'lh-category');
-      this.createPermalinkSpan(categoryElem, category.id);
-      categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions));
-
-      const auditRefs = category.auditRefs;
-
-      // Regular audits aren't split up into pass/fail/notApplicable clumps, they're
-      // all put in a top-level clump that isn't expandable/collapsible.
-      const regularAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode !== 'manual');
-      const auditsElem = this._renderAudits(regularAuditRefs, groupDefinitions);
-      categoryElem.appendChild(auditsElem);
-
-      // Manual audits are still in a manual clump.
-      const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
-      const manualElem = this.renderClump('manual',
-        {auditRefs: manualAuditRefs, description: category.manualDescription});
-      categoryElem.appendChild(manualElem);
-
-      return categoryElem;
-    }
-
-    /**
-     * @param {LH.ReportResult.Category} category
-     * @param {Record} groupDefinitions
-     * @return {DocumentFragment}
-     */
-    renderScoreGauge(category, groupDefinitions) {
-      // Defer to parent-gauge style if category error.
-      if (category.score === null) {
-        return super.renderScoreGauge(category, groupDefinitions);
-      }
-
-      const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge--pwa', this.templateContext);
-      const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
-      wrapper.href = `#${category.id}`;
-
-      // Correct IDs in case multiple instances end up in the page.
-      const svgRoot = tmpl.querySelector('svg');
-      if (!svgRoot) throw new Error('no SVG element found in PWA score gauge template');
-      PwaCategoryRenderer._makeSvgReferencesUnique(svgRoot);
-
-      const allGroups = this._getGroupIds(category.auditRefs);
-      const passingGroupIds = this._getPassingGroupIds(category.auditRefs);
-
-      if (passingGroupIds.size === allGroups.size) {
-        wrapper.classList.add('lh-badged--all');
-      } else {
-        for (const passingGroupId of passingGroupIds) {
-          wrapper.classList.add(`lh-badged--${passingGroupId}`);
-        }
-      }
-
-      this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
-      wrapper.title = this._getGaugeTooltip(category.auditRefs, groupDefinitions);
-      return tmpl;
-    }
-
-    /**
-     * Returns the group IDs found in auditRefs.
-     * @param {Array} auditRefs
-     * @return {!Set}
-     */
-    _getGroupIds(auditRefs) {
-      const groupIds = auditRefs.map(ref => ref.group).filter(/** @return {g is string} */ g => !!g);
-      return new Set(groupIds);
-    }
-
-    /**
-     * Returns the group IDs whose audits are all considered passing.
-     * @param {Array} auditRefs
-     * @return {Set}
-     */
-    _getPassingGroupIds(auditRefs) {
-      const uniqueGroupIds = this._getGroupIds(auditRefs);
-
-      // Remove any that have a failing audit.
-      for (const auditRef of auditRefs) {
-        if (!Util.showAsPassed(auditRef.result) && auditRef.group) {
-          uniqueGroupIds.delete(auditRef.group);
-        }
-      }
-
-      return uniqueGroupIds;
-    }
-
-    /**
-     * Returns a tooltip string summarizing group pass rates.
-     * @param {Array} auditRefs
-     * @param {Record} groupDefinitions
-     * @return {string}
-     */
-    _getGaugeTooltip(auditRefs, groupDefinitions) {
-      const groupIds = this._getGroupIds(auditRefs);
-
-      const tips = [];
-      for (const groupId of groupIds) {
-        const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId);
-        const auditCount = groupAuditRefs.length;
-        const passedCount = groupAuditRefs.filter(ref => Util.showAsPassed(ref.result)).length;
-
-        const title = groupDefinitions[groupId].title;
-        tips.push(`${title}: ${passedCount}/${auditCount}`);
-      }
-
-      return tips.join(', ');
-    }
-
-    /**
-     * Render non-manual audits in groups, giving a badge to any group that has
-     * all passing audits.
-     * @param {Array} auditRefs
-     * @param {Object} groupDefinitions
-     * @return {Element}
-     */
-    _renderAudits(auditRefs, groupDefinitions) {
-      const auditsElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
-
-      // Add a 'badged' class to group if all audits in that group pass.
-      const passsingGroupIds = this._getPassingGroupIds(auditRefs);
-      for (const groupId of passsingGroupIds) {
-        const groupElem = this.dom.find(`.lh-audit-group--${groupId}`, auditsElem);
-        groupElem.classList.add('lh-badged');
-      }
-
-      return auditsElem;
-    }
-
-    /**
-     * Alters SVG id references so multiple instances of an SVG element can coexist
-     * in a single page. If `svgRoot` has a `` block, gives all elements defined
-     * in it unique ids, then updates id references (``,
-     * `fill="url(#...)"`) to the altered ids in all descendents of `svgRoot`.
-     * @param {SVGElement} svgRoot
-     */
-    static _makeSvgReferencesUnique(svgRoot) {
-      const defsEl = svgRoot.querySelector('defs');
-      if (!defsEl) return;
-
-      const idSuffix = Util.getUniqueSuffix();
-      const elementsToUpdate = defsEl.querySelectorAll('[id]');
-      for (const el of elementsToUpdate) {
-        const oldId = el.id;
-        const newId = `${oldId}-${idSuffix}`;
-        el.id = newId;
-
-        // Update all s.
-        const useEls = svgRoot.querySelectorAll(`use[href="#${oldId}"]`);
-        for (const useEl of useEls) {
-          useEl.setAttribute('href', `#${newId}`);
-        }
-
-        // Update all fill="url(#...)"s.
-        const fillEls = svgRoot.querySelectorAll(`[fill="url(#${oldId})"]`);
-        for (const fillEl of fillEls) {
-          fillEl.setAttribute('fill', `url(#${newId})`);
-        }
-      }
-    }
-  }
-
-  /**
-   * @license
-   * Copyright 2017 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.
-   */
-
-  class ReportRenderer {
-    /**
-     * @param {DOM} dom
-     */
-    constructor(dom) {
-      /** @type {DOM} */
-      this._dom = dom;
-      /** @type {ParentNode} */
-      this._templateContext = this._dom.document();
-    }
-
-    /**
-     * @param {LH.Result} result
-     * @param {Element} container Parent element to render the report into.
-     * @return {!Element}
-     */
-    renderReport(result, container) {
-      this._dom.setLighthouseChannel(result.configSettings.channel || 'unknown');
-
-      const report = Util.prepareReportResult(result);
-
-      container.textContent = ''; // Remove previous report.
-      container.appendChild(this._renderReport(report));
-
-      return container;
-    }
-
-    /**
-     * Define a custom element for  to be extracted from. For example:
-     *     this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
-     * @param {ParentNode} context
-     */
-    setTemplateContext(context) {
-      this._templateContext = context;
-    }
-
-    /**
-     * @param {LH.ReportResult} report
-     * @return {DocumentFragment}
-     */
-    _renderReportTopbar(report) {
-      const el = this._dom.cloneTemplate('#tmpl-lh-topbar', this._templateContext);
-      const metadataUrl = this._dom.find('a.lh-topbar__url', el);
-      metadataUrl.href = metadataUrl.textContent = report.finalUrl;
-      metadataUrl.title = report.finalUrl;
-      return el;
-    }
-
-    /**
-     * @return {DocumentFragment}
-     */
-    _renderReportHeader() {
-      const el = this._dom.cloneTemplate('#tmpl-lh-heading', this._templateContext);
-      const domFragment = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext);
-      const placeholder = this._dom.find('.lh-scores-wrapper-placeholder', el);
-      placeholder.replaceWith(domFragment);
-      return el;
-    }
-
-    /**
-     * @param {LH.ReportResult} report
-     * @return {DocumentFragment}
-     */
-    _renderReportFooter(report) {
-      const footer = this._dom.cloneTemplate('#tmpl-lh-footer', this._templateContext);
-
-      const env = this._dom.find('.lh-env__items', footer);
-      env.id = 'runtime-settings';
-      this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
-
-      const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
-      const runtimeValues = [
-        {name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
-        {name: Util.i18n.strings.runtimeSettingsFetchTime,
-          description: Util.i18n.formatDateTime(report.fetchTime)},
-        ...envValues,
-        {name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
-        {name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
-        {name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
-          report.environment.networkUserAgent},
-        {name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
-          report.environment.benchmarkIndex.toFixed(0)},
-      ];
-      if (report.environment.credits && report.environment.credits['axe-core']) {
-        runtimeValues.push({
-          name: Util.i18n.strings.runtimeSettingsAxeVersion,
-          description: report.environment.credits['axe-core'],
-        });
-      }
-
-      for (const runtime of runtimeValues) {
-        if (!runtime.description) continue;
-
-        const item = this._dom.cloneTemplate('#tmpl-lh-env__items', env);
-        this._dom.find('.lh-env__name', item).textContent = runtime.name;
-        this._dom.find('.lh-env__description', item).textContent = runtime.description;
-        env.appendChild(item);
-      }
-
-      this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
-      this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
-      return footer;
-    }
-
-    /**
-     * Returns a div with a list of top-level warnings, or an empty div if no warnings.
-     * @param {LH.ReportResult} report
-     * @return {Node}
-     */
-    _renderReportWarnings(report) {
-      if (!report.runWarnings || report.runWarnings.length === 0) {
-        return this._dom.createElement('div');
-      }
-
-      const container = this._dom.cloneTemplate('#tmpl-lh-warnings--toplevel', this._templateContext);
-      const message = this._dom.find('.lh-warnings__msg', container);
-      message.textContent = Util.i18n.strings.toplevelWarningsMessage;
-
-      const warnings = this._dom.find('ul', container);
-      for (const warningString of report.runWarnings) {
-        const warning = warnings.appendChild(this._dom.createElement('li'));
-        warning.appendChild(this._dom.convertMarkdownLinkSnippets(warningString));
-      }
-
-      return container;
-    }
-
-    /**
-     * @param {LH.ReportResult} report
-     * @param {CategoryRenderer} categoryRenderer
-     * @param {Record} specificCategoryRenderers
-     * @return {!DocumentFragment[]}
-     */
-    _renderScoreGauges(report, categoryRenderer, specificCategoryRenderers) {
-      // Group gauges in this order: default, pwa, plugins.
-      const defaultGauges = [];
-      const customGauges = []; // PWA.
-      const pluginGauges = [];
-
-      for (const category of Object.values(report.categories)) {
-        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
-        const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
-
-        if (Util.isPluginCategory(category.id)) {
-          pluginGauges.push(categoryGauge);
-        } else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
-          // The renderer for default categories is just the default CategoryRenderer.
-          // If the functions are equal, then renderer is an instance of CategoryRenderer.
-          // For example, the PWA category uses PwaCategoryRenderer, which overrides
-          // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
-          // in the customGauges bucket.
-          defaultGauges.push(categoryGauge);
-        } else {
-          customGauges.push(categoryGauge);
-        }
-      }
-
-      return [...defaultGauges, ...customGauges, ...pluginGauges];
-    }
-
-    /**
-     * @param {LH.ReportResult} report
-     * @return {!DocumentFragment}
-     */
-    _renderReport(report) {
-      const i18n = new I18n(report.configSettings.locale, {
-        // Set missing renderer strings to default (english) values.
-        ...Util.UIStrings,
-        ...report.i18n.rendererFormattedStrings,
-      });
-      Util.i18n = i18n;
-      Util.reportJson = report;
-
-      const fullPageScreenshot =
-        report.audits['full-page-screenshot'] && report.audits['full-page-screenshot'].details &&
-        report.audits['full-page-screenshot'].details.type === 'full-page-screenshot' ?
-        report.audits['full-page-screenshot'].details : undefined;
-      const detailsRenderer = new DetailsRenderer(this._dom, {
-        fullPageScreenshot,
-      });
-
-      const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
-      categoryRenderer.setTemplateContext(this._templateContext);
-
-      /** @type {Record} */
-      const specificCategoryRenderers = {
-        performance: new PerformanceCategoryRenderer(this._dom, detailsRenderer),
-        pwa: new PwaCategoryRenderer(this._dom, detailsRenderer),
-      };
-      Object.values(specificCategoryRenderers).forEach(renderer => {
-        renderer.setTemplateContext(this._templateContext);
-      });
-
-      const headerContainer = this._dom.createElement('div');
-      headerContainer.appendChild(this._renderReportHeader());
-
-      const reportContainer = this._dom.createElement('div', 'lh-container');
-      const reportSection = this._dom.createElement('div', 'lh-report');
-      reportSection.appendChild(this._renderReportWarnings(report));
-
-      let scoreHeader;
-      const isSoloCategory = Object.keys(report.categories).length === 1;
-      if (!isSoloCategory) {
-        scoreHeader = this._dom.createElement('div', 'lh-scores-header');
-      } else {
-        headerContainer.classList.add('lh-header--solo-category');
-      }
-
-      if (scoreHeader) {
-        const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
-        const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
-        scoreHeader.append(
-          ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
-        scoresContainer.appendChild(scoreHeader);
-        scoresContainer.appendChild(scoreScale);
-
-        const stickyHeader = this._dom.createElement('div', 'lh-sticky-header');
-        stickyHeader.append(
-          ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
-        reportContainer.appendChild(stickyHeader);
-      }
-
-      const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories'));
-      for (const category of Object.values(report.categories)) {
-        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
-        // .lh-category-wrapper is full-width and provides horizontal rules between categories.
-        // .lh-category within has the max-width: var(--report-width);
-        const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
-        wrapper.appendChild(renderer.render(category, report.categoryGroups));
-      }
-
-      const reportFragment = this._dom.createFragment();
-      const topbarDocumentFragment = this._renderReportTopbar(report);
-
-      reportFragment.appendChild(topbarDocumentFragment);
-      reportFragment.appendChild(reportContainer);
-      reportContainer.appendChild(headerContainer);
-      reportContainer.appendChild(reportSection);
-      reportSection.appendChild(this._renderReportFooter(report));
-
-      if (fullPageScreenshot) {
-        ElementScreenshotRenderer.installFullPageScreenshot(
-          reportContainer, fullPageScreenshot.screenshot);
-      }
-
-      return reportFragment;
-    }
-  }
-
-  /**
-   * @license Copyright 2017 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.
-   */
-
-  /**
-   * @fileoverview
-   * @suppress {reportUnknownTypes}
-   */
-
-  /**
-   * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
-   * Date/time uses the local timezone, however Node has unreliable ICU
-   * support, so we must construct a YYYY-MM-DD date format manually. :/
-   * @param {{finalUrl: string, fetchTime: string}} lhr
-   * @return {string}
-   */
-  function getFilenamePrefix(lhr) {
-    const hostname = new URL(lhr.finalUrl).hostname;
-    const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
-
-    const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
-    const dateParts = date.toLocaleDateString('en-US', {
-      year: 'numeric', month: '2-digit', day: '2-digit',
-    }).split('/');
-    // @ts-expect-error - parts exists
-    dateParts.unshift(dateParts.pop());
-    const dateStr = dateParts.join('-');
-
-    const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
-    // replace characters that are unfriendly to filenames
-    return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
-  }
-
-  var fileNamer = {getFilenamePrefix};
-  var fileNamer_1 = fileNamer.getFilenamePrefix;
-
-  /**
-   * @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.
-   */
-
-  /* global btoa atob window CompressionStream Response */
-
-  const btoa_ = typeof btoa !== 'undefined' ?
-    btoa :
-    /** @param {string} str */
-    (str) => Buffer.from(str).toString('base64');
-  const atob_ = typeof atob !== 'undefined' ?
-    atob :
-    /** @param {string} str */
-    (str) => Buffer.from(str, 'base64').toString();
-
-  /**
-   * Takes an UTF-8 string and returns a base64 encoded string.
-   * If gzip is true, the UTF-8 bytes are gzipped before base64'd, using
-   * CompressionStream (currently only in Chrome), falling back to pako
-   * (which is only used to encode in our Node tests).
-   * @param {string} string
-   * @param {{gzip: boolean}} options
-   * @return {Promise}
-   */
-  async function toBase64(string, options) {
-    let bytes = new TextEncoder().encode(string);
-
-    if (options.gzip) {
-      if (typeof CompressionStream !== 'undefined') {
-        const cs = new CompressionStream('gzip');
-        const writer = cs.writable.getWriter();
-        writer.write(bytes);
-        writer.close();
-        const compAb = await new Response(cs.readable).arrayBuffer();
-        bytes = new Uint8Array(compAb);
-      } else {
-        /** @type {import('pako')=} */
-        const pako = window.pako;
-        bytes = pako.gzip(string);
-      }
-    }
-
-    let binaryString = '';
-    // This is ~25% faster than building the string one character at a time.
-    // https://jsbench.me/2gkoxazvjl
-    const chunkSize = 5000;
-    for (let i = 0; i < bytes.length; i += chunkSize) {
-      binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
-    }
-    return btoa_(binaryString);
-  }
-
-  /**
-   * @param {string} encoded
-   * @param {{gzip: boolean}} options
-   * @return {string}
-   */
-  function fromBase64(encoded, options) {
-    const binaryString = atob_(encoded);
-    const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
-
-    if (options.gzip) {
-      /** @type {import('pako')=} */
-      const pako = window.pako;
-      return pako.ungzip(bytes, {to: 'string'});
-    } else {
-      return new TextDecoder().decode(bytes);
-    }
-  }
-
-  const TextEncoding = {toBase64, fromBase64};
-
-  /**
-   * @license
-   * Copyright 2017 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.
-   */
-
-  /**
-   * @param {HTMLTableElement} tableEl
-   * @return {Array}
-   */
-  function getTableRows(tableEl) {
-    return Array.from(tableEl.tBodies[0].rows);
-  }
-
-  function getAppsOrigin() {
-    const isVercel = window.location.host.endsWith('.vercel.app');
-    const isDev = new URLSearchParams(window.location.search).has('dev');
-
-    if (isVercel) return `https://${window.location.host}/gh-pages`;
-    if (isDev) return 'http://localhost:8000';
-    return 'https://googlechrome.github.io/lighthouse';
-  }
-
-  class ReportUIFeatures {
-    /**
-     * @param {DOM} dom
-     */
-    constructor(dom) {
-      /** @type {LH.Result} */
-      this.json; // eslint-disable-line no-unused-expressions
-      /** @type {DOM} */
-      this._dom = dom;
-      /** @type {Document} */
-      this._document = this._dom.document();
-      /** @type {ParentNode} */
-      this._templateContext = this._dom.document();
-      /** @type {DropDown} */
-      this._dropDown = new DropDown(this._dom);
-      /** @type {boolean} */
-      this._copyAttempt = false;
-      /** @type {HTMLElement} */
-      this.topbarEl; // eslint-disable-line no-unused-expressions
-      /** @type {HTMLElement} */
-      this.scoreScaleEl; // eslint-disable-line no-unused-expressions
-      /** @type {HTMLElement} */
-      this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
-      /** @type {HTMLElement} */
-      this.highlightEl; // eslint-disable-line no-unused-expressions
-
-      this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
-      this.onCopy = this.onCopy.bind(this);
-      this.onDropDownMenuClick = this.onDropDownMenuClick.bind(this);
-      this.onKeyUp = this.onKeyUp.bind(this);
-      this.collapseAllDetails = this.collapseAllDetails.bind(this);
-      this.expandAllDetails = this.expandAllDetails.bind(this);
-      this._toggleDarkTheme = this._toggleDarkTheme.bind(this);
-      this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
-    }
-
-    /**
-     * Adds tools button, print, and other functionality to the report. The method
-     * should be called whenever the report needs to be re-rendered.
-     * @param {LH.Result} report
-     */
-    initFeatures(report) {
-      this.json = report;
-
-      this._setupMediaQueryListeners();
-      this._dropDown.setup(this.onDropDownMenuClick);
-      this._setupThirdPartyFilter();
-      this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document));
-      this._setUpCollapseDetailsAfterPrinting();
-      this._resetUIState();
-      this._document.addEventListener('keyup', this.onKeyUp);
-      this._document.addEventListener('copy', this.onCopy);
-
-      const topbarLogo = this._dom.find('.lh-topbar__logo', this._document);
-      topbarLogo.addEventListener('click', () => this._toggleDarkTheme());
-
-      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) {
-        turnOffTheLights = true;
-      }
-
-      // Fireworks!
-      // To get fireworks you need 100 scores in all core categories, except PWA (because going the PWA route is discretionary).
-      const fireworksRequiredCategoryIds = ['performance', 'accessibility', 'best-practices', 'seo'];
-      const scoresAll100 = fireworksRequiredCategoryIds.every(id => {
-        const cat = report.categories[id];
-        return cat && cat.score === 1;
-      });
-      if (scoresAll100) {
-        turnOffTheLights = true;
-        this._enableFireworks();
-      }
-
-      if (turnOffTheLights) {
-        this._toggleDarkTheme(true);
-      }
-
-      // There is only a sticky header when at least 2 categories are present.
-      if (Object.keys(this.json.categories).length >= 2) {
-        this._setupStickyHeaderElements();
-        const containerEl = this._dom.find('.lh-container', this._document);
-        const elToAddScrollListener = this._getScrollParent(containerEl);
-        elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
-
-        // Use ResizeObserver where available.
-        // TODO: there is an issue with incorrect position numbers and, as a result, performance
-        // issues due to layout thrashing.
-        // See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
-        // For now, limit to DevTools.
-        if (this._dom.isDevTools()) {
-          const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
-          resizeObserver.observe(containerEl);
-        } else {
-          window.addEventListener('resize', this._updateStickyHeaderOnScroll);
-        }
-      }
-
-      // Show the metric descriptions by default when there is an error.
-      const hasMetricError = report.categories.performance && report.categories.performance.auditRefs
-        .some(audit => Boolean(audit.group === 'metrics' && report.audits[audit.id].errorMessage));
-      if (hasMetricError) {
-        const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document);
-        toggleInputEl.checked = true;
-      }
-
-      const showTreemapApp =
-        this.json.audits['script-treemap-data'] && this.json.audits['script-treemap-data'].details;
-      if (showTreemapApp) {
-        this.addButton({
-          text: Util.i18n.strings.viewTreemapLabel,
-          icon: 'treemap',
-          onClick: () => ReportUIFeatures.openTreemap(this.json),
-        });
-      }
-
-      // Fill in all i18n data.
-      for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
-        // 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 i18nAttr = /** @type {keyof LH.I18NRendererStrings} */ (node.getAttribute('data-i18n'));
-        node.textContent = Util.i18n.strings[i18nAttr];
-      }
-    }
-
-    /**
-     * Define a custom element for  to be extracted from. For example:
-     *     this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
-     * @param {ParentNode} context
-     */
-    setTemplateContext(context) {
-      this._templateContext = context;
-    }
-
-    /**
-     * @param {{container?: Element, 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;
-
-      let buttonsEl = containerEl.querySelector('.lh-buttons');
-      if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons');
-
-      const classes = [
-        'lh-button',
-      ];
-      if (opts.icon) {
-        classes.push('report-icon');
-        classes.push(`report-icon--${opts.icon}`);
-      }
-      const buttonEl = this._dom.createChildOf(buttonsEl, 'button', classes.join(' '));
-      buttonEl.textContent = opts.text;
-      buttonEl.addEventListener('click', opts.onClick);
-      return buttonEl;
-    }
-
-    /**
-     * Finds the first scrollable ancestor of `element`. Falls back to the document.
-     * @param {Element} element
-     * @return {Node}
-     */
-    _getScrollParent(element) {
-      const {overflowY} = window.getComputedStyle(element);
-      const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';
-
-      if (isScrollable) {
-        return element;
-      }
-
-      if (element.parentElement) {
-        return this._getScrollParent(element.parentElement);
-      }
-
-      return document;
-    }
-
-    _enableFireworks() {
-      const scoresContainer = this._dom.find('.lh-scores-container', this._document);
-      scoresContainer.classList.add('score100');
-      scoresContainer.addEventListener('click', _ => {
-        scoresContainer.classList.toggle('fireworks-paused');
-      });
-    }
-
-    /**
-     * Fires a custom DOM event on target.
-     * @param {string} name Name of the event.
-     * @param {Node=} target DOM node to fire the event on.
-     * @param {*=} detail Custom data to include.
-     */
-    _fireEventOn(name, target = this._document, detail) {
-      const event = new CustomEvent(name, detail ? {detail} : undefined);
-      target.dispatchEvent(event);
-    }
-
-    _setupMediaQueryListeners() {
-      const mediaQuery = self.matchMedia('(max-width: 500px)');
-      mediaQuery.addListener(this.onMediaQueryChange);
-      // Ensure the handler is called on init
-      this.onMediaQueryChange(mediaQuery);
-    }
-
-    /**
-     * Handle media query change events.
-     * @param {MediaQueryList|MediaQueryListEvent} mql
-     */
-    onMediaQueryChange(mql) {
-      const root = this._dom.find('.lh-root', this._document);
-      root.classList.toggle('lh-narrow', mql.matches);
-    }
-
-    _setupThirdPartyFilter() {
-      // Some audits should not display the third party filter option.
-      const thirdPartyFilterAuditExclusions = [
-        // These audits deal explicitly with third party resources.
-        'uses-rel-preconnect',
-        'third-party-facades',
-      ];
-      // Some audits should hide third party by default.
-      const thirdPartyFilterAuditHideByDefault = [
-        // Only first party resources are actionable.
-        'legacy-javascript',
-      ];
-
-      // Get all tables with a text url column.
-      const tables = Array.from(this._document.querySelectorAll('table.lh-table'));
-      const tablesWithUrls = tables
-        .filter(el =>
-          el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
-        .filter(el => {
-          const containingAudit = el.closest('.lh-audit');
-          if (!containingAudit) throw new Error('.lh-table not within audit');
-          return !thirdPartyFilterAuditExclusions.includes(containingAudit.id);
-        });
-
-      tablesWithUrls.forEach((tableEl, index) => {
-        const rowEls = getTableRows(tableEl);
-        const thirdPartyRows = this._getThirdPartyRows(rowEls, this.json.finalUrl);
-
-        // create input box
-        const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._templateContext);
-        const filterInput = this._dom.find('input', filterTemplate);
-        const id = `lh-3p-filter-label--${index}`;
-
-        filterInput.id = id;
-        filterInput.addEventListener('change', e => {
-          const shouldHideThirdParty = e.target instanceof HTMLInputElement && !e.target.checked;
-          let even = true;
-          let rowEl = rowEls[0];
-          while (rowEl) {
-            const shouldHide = shouldHideThirdParty && thirdPartyRows.includes(rowEl);
-
-            // Iterate subsequent associated sub item rows.
-            do {
-              rowEl.classList.toggle('lh-row--hidden', shouldHide);
-              // Adjust for zebra styling.
-              rowEl.classList.toggle('lh-row--even', !shouldHide && even);
-              rowEl.classList.toggle('lh-row--odd', !shouldHide && !even);
-
-              rowEl = /** @type {HTMLElement} */ (rowEl.nextElementSibling);
-            } while (rowEl && rowEl.classList.contains('lh-sub-item-row'));
-
-            if (!shouldHide) even = !even;
-          }
-        });
-
-        this._dom.find('label', filterTemplate).setAttribute('for', id);
-        this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
-            `${thirdPartyRows.length}`;
-        this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
-            Util.i18n.strings.thirdPartyResourcesLabel;
-
-        const allThirdParty = thirdPartyRows.length === rowEls.length;
-        const allFirstParty = !thirdPartyRows.length;
-
-        // If all or none of the rows are 3rd party, disable the checkbox.
-        if (allThirdParty || allFirstParty) {
-          filterInput.disabled = true;
-          filterInput.checked = allThirdParty;
-        }
-
-        // Add checkbox to the DOM.
-        if (!tableEl.parentNode) return; // Keep tsc happy.
-        tableEl.parentNode.insertBefore(filterTemplate, tableEl);
-
-        // Hide third-party rows for some audits by default.
-        const containingAudit = tableEl.closest('.lh-audit');
-        if (!containingAudit) throw new Error('.lh-table not within audit');
-        if (thirdPartyFilterAuditHideByDefault.includes(containingAudit.id) && !allThirdParty) {
-          filterInput.click();
-        }
-      });
-    }
-
-    /**
-     * @param {Element} el
-     */
-    _setupElementScreenshotOverlay(el) {
-      const fullPageScreenshot =
-        this.json.audits['full-page-screenshot'] &&
-        this.json.audits['full-page-screenshot'].details &&
-        this.json.audits['full-page-screenshot'].details.type === 'full-page-screenshot' &&
-        this.json.audits['full-page-screenshot'].details;
-      if (!fullPageScreenshot) return;
-
-      ElementScreenshotRenderer.installOverlayFeature({
-        dom: this._dom,
-        reportEl: el,
-        overlayContainerEl: el,
-        templateContext: this._templateContext,
-        fullPageScreenshot,
-      });
-    }
-
-    /**
-     * From a table with URL entries, finds the rows containing third-party URLs
-     * and returns them.
-     * @param {HTMLElement[]} rowEls
-     * @param {string} finalUrl
-     * @return {Array}
-     */
-    _getThirdPartyRows(rowEls, finalUrl) {
-      /** @type {Array} */
-      const thirdPartyRows = [];
-      const finalUrlRootDomain = Util.getRootDomain(finalUrl);
-
-      for (const rowEl of rowEls) {
-        if (rowEl.classList.contains('lh-sub-item-row')) continue;
-
-        const urlItem = rowEl.querySelector('div.lh-text__url');
-        if (!urlItem) continue;
-
-        const datasetUrl = urlItem.dataset.url;
-        if (!datasetUrl) continue;
-        const isThirdParty = Util.getRootDomain(datasetUrl) !== finalUrlRootDomain;
-        if (!isThirdParty) continue;
-
-        thirdPartyRows.push(rowEl);
-      }
-
-      return thirdPartyRows;
-    }
-
-    _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);
-
-      // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
-      this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
-    }
-
-    /**
-     * Handle copy events.
-     * @param {ClipboardEvent} e
-     */
-    onCopy(e) {
-      // Only handle copy button presses (e.g. ignore the user copying page text).
-      if (this._copyAttempt && e.clipboardData) {
-        // We want to write our own data to the clipboard, not the user's text selection.
-        e.preventDefault();
-        e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
-
-        this._fireEventOn('lh-log', this._document, {
-          cmd: 'log', msg: 'Report JSON copied to clipboard',
-        });
-      }
-
-      this._copyAttempt = false;
-    }
-
-    /**
-     * Copies the report JSON to the clipboard (if supported by the browser).
-     */
-    onCopyButtonClick() {
-      this._fireEventOn('lh-analytics', this._document, {
-        cmd: 'send',
-        fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
-      });
-
-      try {
-        if (this._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._document.execCommand('copy')) {
-            this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
-
-            this._fireEventOn('lh-log', this._document, {
-              cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
-            });
-          }
-        }
-      } catch (/** @type {Error} */ e) {
-        this._copyAttempt = false;
-        this._fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
-      }
-    }
-
-    /**
-     * Resets the state of page before capturing the page for export.
-     * When the user opens the exported HTML page, certain UI elements should
-     * be in their closed state (not opened) and the templates should be unstamped.
-     */
-    _resetUIState() {
-      this._dropDown.close();
-      this._dom.resetTemplates();
-    }
-
-    /**
-     * Handler for tool button.
-     * @param {Event} e
-     */
-    onDropDownMenuClick(e) {
-      e.preventDefault();
-
-      const el = /** @type {?Element} */ (e.target);
-
-      if (!el || !el.hasAttribute('data-action')) {
-        return;
-      }
-
-      switch (el.getAttribute('data-action')) {
-        case 'copy':
-          this.onCopyButtonClick();
-          break;
-        case 'print-summary':
-          this.collapseAllDetails();
-          this._print();
-          break;
-        case 'print-expanded':
-          this.expandAllDetails();
-          this._print();
-          break;
-        case 'save-json': {
-          const jsonStr = JSON.stringify(this.json, null, 2);
-          this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
-          break;
-        }
-        case 'save-html': {
-          const htmlStr = this.getReportHtml();
-          try {
-            this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
-          } catch (/** @type {Error} */ e) {
-            this._fireEventOn('lh-log', this._document, {
-              cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
-            });
-          }
-          break;
-        }
-        case 'open-viewer': {
-          ReportUIFeatures.openTabAndSendJsonReportToViewer(this.json);
-          break;
-        }
-        case 'save-gist': {
-          this.saveAsGist();
-          break;
-        }
-        case 'toggle-dark': {
-          this._toggleDarkTheme();
-          break;
-        }
-      }
-
-      this._dropDown.close();
-    }
-
-    _print() {
-      self.print();
-    }
-
-    /**
-     * Keyup handler for the document.
-     * @param {KeyboardEvent} e
-     */
-    onKeyUp(e) {
-      // Ctrl+P - Expands audit details when user prints via keyboard shortcut.
-      if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) {
-        this._dropDown.close();
-      }
-    }
-
-    /**
-     * The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly.
-     * @param {LH.Result} json
-     * @protected
-     */
-    static computeWindowNameSuffix(json) {
-      // @ts-ignore - If this is a v2 LHR, use old `generatedTime`.
-      const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
-      const fetchTime = json.fetchTime || fallbackFetchTime;
-      return `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
-    }
-
-    /**
-     * Opens a new tab to the online viewer and sends the local page's JSON results
-     * to the online viewer using postMessage.
-     * @param {LH.Result} json
-     * @protected
-     */
-    static openTabAndSendJsonReportToViewer(json) {
-      const windowName = 'viewer-' + this.computeWindowNameSuffix(json);
-      const url = getAppsOrigin() + '/viewer/';
-      ReportUIFeatures.openTabAndSendData({lhr: json}, url, windowName);
-    }
-
-    /**
-     * Opens a new tab to the treemap app and sends the JSON results using URL.fragment
-     * @param {LH.Result} json
-     */
-    static openTreemap(json) {
-      const treemapData = json.audits['script-treemap-data'].details;
-      if (!treemapData) {
-        throw new Error('no script treemap data found');
-      }
-
-      /** @type {LH.Treemap.Options} */
-      const treemapOptions = {
-        lhr: {
-          requestedUrl: json.requestedUrl,
-          finalUrl: json.finalUrl,
-          audits: {
-            'script-treemap-data': json.audits['script-treemap-data'],
-          },
-          configSettings: {
-            locale: json.configSettings.locale,
-          },
-        },
-      };
-      const url = getAppsOrigin() + '/treemap/';
-      const windowName = 'treemap-' + this.computeWindowNameSuffix(json);
-
-      ReportUIFeatures.openTabWithUrlData(treemapOptions, url, windowName);
-    }
-
-    /**
-     * Opens a new tab to an external page and sends data using postMessage.
-     * @param {{lhr: LH.Result} | LH.Treemap.Options} data
-     * @param {string} url
-     * @param {string} windowName
-     * @protected
-     */
-    static openTabAndSendData(data, url, windowName) {
-      const origin = new URL(url).origin;
-      // Chrome doesn't allow us to immediately postMessage to a popup right
-      // after it's created. Normally, we could also listen for the popup window's
-      // load event, however it is cross-domain and won't fire. Instead, listen
-      // for a message from the target app saying "I'm open".
-      window.addEventListener('message', function msgHandler(messageEvent) {
-        if (messageEvent.origin !== origin) {
-          return;
-        }
-        if (popup && messageEvent.data.opened) {
-          popup.postMessage(data, origin);
-          window.removeEventListener('message', msgHandler);
-        }
-      });
-
-      const popup = window.open(url, windowName);
-    }
-
-    /**
-     * Opens a new tab to an external page and sends data via base64 encoded url params.
-     * @param {{lhr: LH.Result} | LH.Treemap.Options} data
-     * @param {string} url_
-     * @param {string} windowName
-     * @protected
-     */
-    static async openTabWithUrlData(data, url_, windowName) {
-      const url = new URL(url_);
-      const gzip = Boolean(window.CompressionStream);
-      url.hash = await TextEncoding.toBase64(JSON.stringify(data), {
-        gzip,
-      });
-      if (gzip) url.searchParams.set('gzip', '1');
-      window.open(url.toString(), windowName);
-    }
-
-    /**
-     * Expands all audit `
`. - * Ideally, a print stylesheet could take care of this, but CSS has no way to - * open a `
` element. - */ - expandAllDetails() { - const details = this._dom.findAll('.lh-categories details', this._document); - details.map(detail => detail.open = true); - } - - /** - * Collapses all audit `
`. - * open a `
` element. - */ - collapseAllDetails() { - const details = this._dom.findAll('.lh-categories details', this._document); - details.map(detail => detail.open = false); - } - - /** - * Sets up listeners to collapse audit `
` when the user closes the - * print dialog, all `
` are collapsed. - */ - _setUpCollapseDetailsAfterPrinting() { - // FF and IE implement these old events. - if ('onbeforeprint' in self) { - self.addEventListener('afterprint', this.collapseAllDetails); - } else { - // Note: FF implements both window.onbeforeprint and media listeners. However, - // it doesn't matchMedia doesn't fire when matching 'print'. - self.matchMedia('print').addListener(mql => { - if (mql.matches) { - this.expandAllDetails(); - } else { - this.collapseAllDetails(); - } - }); - } - } - - /** - * Returns the html that recreates this report. - * @return {string} - * @protected - */ - getReportHtml() { - this._resetUIState(); - return this._document.documentElement.outerHTML; - } - - /** - * Save json as a gist. Unimplemented in base UI features. - * @protected - */ - saveAsGist() { - throw new Error('Cannot save as gist from base report'); - } - - /** - * Downloads a file (blob) using a[download]. - * @param {Blob|File} blob The file to save. - * @private - */ - _saveFile(blob) { - const filename = fileNamer_1({ - finalUrl: this.json.finalUrl, - fetchTime: this.json.fetchTime, - }); - - const ext = blob.type.match('json') ? '.json' : '.html'; - const href = URL.createObjectURL(blob); - - const a = this._dom.createElement('a'); - a.download = `${filename}${ext}`; - a.href = href; - this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM. - a.click(); - - // cleanup. - this._document.body.removeChild(a); - setTimeout(_ => URL.revokeObjectURL(href), 500); - } - - /** - * @private - * @param {boolean} [force] - */ - _toggleDarkTheme(force) { - const el = this._dom.find('.lh-vars', this._document); - // 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 - if (typeof force === 'undefined') { - el.classList.toggle('dark'); - } else { - el.classList.toggle('dark', force); - } - } - - _updateStickyHeaderOnScroll() { - // Show sticky header when the score scale begins to go underneath the topbar. - const topbarBottom = this.topbarEl.getBoundingClientRect().bottom; - const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top; - const showStickyHeader = topbarBottom >= scoreScaleTop; - - // 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 categoriesAboveTheMiddle = - categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0); - const highlightIndex = - categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0; - - // Category order matches gauge order in sticky header. - const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper'); - const gaugeToHighlight = gaugeWrapperEls[highlightIndex]; - const origin = gaugeWrapperEls[0].getBoundingClientRect().left; - const offset = gaugeToHighlight.getBoundingClientRect().left - origin; - - // Mutate at end to avoid layout thrashing. - this.highlightEl.style.transform = `translate(${offset}px)`; - this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader); - } - } - - class DropDown { - /** - * @param {DOM} dom - */ - constructor(dom) { - /** @type {DOM} */ - this._dom = dom; - /** @type {HTMLElement} */ - this._toggleEl; // eslint-disable-line no-unused-expressions - /** @type {HTMLElement} */ - this._menuEl; // eslint-disable-line no-unused-expressions - - this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this); - this.onToggleClick = this.onToggleClick.bind(this); - this.onToggleKeydown = this.onToggleKeydown.bind(this); - this.onMenuFocusOut = this.onMenuFocusOut.bind(this); - this.onMenuKeydown = this.onMenuKeydown.bind(this); - - this._getNextMenuItem = this._getNextMenuItem.bind(this); - this._getNextSelectableNode = this._getNextSelectableNode.bind(this); - this._getPreviousMenuItem = this._getPreviousMenuItem.bind(this); - } - - /** - * @param {function(MouseEvent): any} menuClickHandler - */ - setup(menuClickHandler) { - this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document()); - 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.addEventListener('keydown', this.onMenuKeydown); - this._menuEl.addEventListener('click', menuClickHandler); - } - - close() { - this._toggleEl.classList.remove('active'); - this._toggleEl.setAttribute('aria-expanded', 'false'); - 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); - } - - /** - * @param {HTMLElement} firstFocusElement - */ - open(firstFocusElement) { - if (this._toggleEl.classList.contains('active')) { - // If the drop down is already open focus on the element - firstFocusElement.focus(); - } else { - // Wait for drop down transition to complete so options are focusable. - this._menuEl.addEventListener('transitionend', () => { - firstFocusElement.focus(); - }, {once: true}); - } - - this._toggleEl.classList.add('active'); - this._toggleEl.setAttribute('aria-expanded', 'true'); - this._menuEl.addEventListener('focusout', this.onMenuFocusOut); - this._dom.document().addEventListener('keydown', this.onDocumentKeyDown); - } - - /** - * Click handler for tools button. - * @param {Event} e - */ - onToggleClick(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - - if (this._toggleEl.classList.contains('active')) { - this.close(); - } else { - this.open(this._getNextMenuItem()); - } - } - - /** - * Handler for tool button. - * @param {KeyboardEvent} e - */ - onToggleKeydown(e) { - switch (e.code) { - case 'ArrowUp': - e.preventDefault(); - this.open(this._getPreviousMenuItem()); - break; - case 'ArrowDown': - case 'Enter': - case ' ': - e.preventDefault(); - this.open(this._getNextMenuItem()); - break; - // no op - } - } - - /** - * Handler for tool DropDown. - * @param {KeyboardEvent} e - */ - onMenuKeydown(e) { - const el = /** @type {?HTMLElement} */ (e.target); - - switch (e.code) { - case 'ArrowUp': - e.preventDefault(); - this._getPreviousMenuItem(el).focus(); - break; - case 'ArrowDown': - e.preventDefault(); - this._getNextMenuItem(el).focus(); - break; - case 'Home': - e.preventDefault(); - this._getNextMenuItem().focus(); - break; - case 'End': - e.preventDefault(); - this._getPreviousMenuItem().focus(); - break; - // no op - } - } - - /** - * Keydown handler for the document. - * @param {KeyboardEvent} e - */ - onDocumentKeyDown(e) { - if (e.keyCode === 27) { // ESC - this.close(); - } - } - - /** - * Focus out handler for the drop down menu. - * @param {FocusEvent} e - */ - onMenuFocusOut(e) { - const focusedEl = /** @type {?HTMLElement} */ (e.relatedTarget); - - if (!this._menuEl.contains(focusedEl)) { - this.close(); - } - } - - /** - * @param {Array} allNodes - * @param {?HTMLElement=} startNode - * @returns {HTMLElement} - */ - _getNextSelectableNode(allNodes, startNode) { - const nodes = allNodes.filter(/** @return {node is HTMLElement} */ (node) => { - if (!(node instanceof HTMLElement)) { - return false; - } - - // 'Save as Gist' option may be disabled. - if (node.hasAttribute('disabled')) { - return false; - } - - // 'Save as Gist' option may have display none. - if (window.getComputedStyle(node).display === 'none') { - return false; - } - - return true; - }); - - let nextIndex = startNode ? (nodes.indexOf(startNode) + 1) : 0; - if (nextIndex >= nodes.length) { - nextIndex = 0; - } - - return nodes[nextIndex]; - } - - /** - * @param {?HTMLElement=} startEl - * @returns {HTMLElement} - */ - _getNextMenuItem(startEl) { - const nodes = Array.from(this._menuEl.childNodes); - return this._getNextSelectableNode(nodes, startEl); - } - - /** - * @param {?HTMLElement=} startEl - * @returns {HTMLElement} - */ - _getPreviousMenuItem(startEl) { - const nodes = Array.from(this._menuEl.childNodes).reverse(); - return this._getNextSelectableNode(nodes, startEl); - } - } - - /** - * @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. - */ - - // OR: we could take an options objec - /** - * @typedef RenderOptions - * @property {LH.Result} lhr - * @property {Element} containerEl Parent element to render the report into. - */ - - - // TODO: we could instead return an Element (not appending to the dom), - // and replace `containerEl` with an options `document: Document` property. - - /** - * @param {RenderOptions} opts - */ - function renderLighthouseReport(opts) { - const dom = new DOM(opts.containerEl.ownerDocument); - const renderer = new ReportRenderer(dom); - renderer.renderReport(opts.lhr, opts.containerEl); - - // Hook in JS features and page-level event listeners after the report - // is in the document. - const features = new ReportUIFeatures(dom); - features.initFeatures(opts.lhr); - } - - /** - * @license - * Copyright 2017 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. - */ - - /** - * Logs messages via a UI butter. - */ - class Logger { - /** - * @param {Element} element - */ - constructor(element) { - this.el = element; - this._id = undefined; - } - - /** - * Shows a butter bar. - * @param {string} msg The message to show. - * @param {boolean=} autoHide True to hide the message after a duration. - * Default is true. - */ - log(msg, autoHide = true) { - this._id && clearTimeout(this._id); - - this.el.textContent = msg; - this.el.classList.add('show'); - if (autoHide) { - this._id = setTimeout(_ => { - this.el.classList.remove('show'); - }, 7000); - } - } - - /** - * @param {string} msg - */ - warn(msg) { - this.log('Warning: ' + msg); - } - - /** - * @param {string} msg - */ - error(msg) { - this.log(msg); - - // Rethrow to make sure it's auditable as an error, but in a setTimeout so page - // recovers gracefully and user can try loading a report again. - setTimeout(_ => { - throw new Error(msg); - }, 0); - } - - /** - * Explicitly hides the butter bar. - */ - hide() { - this._id && clearTimeout(this._id); - this.el.classList.remove('show'); - } - } - - /** - * @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. - */ - - function __initLighthouseReport__() { - const mainEl = document.querySelector('main'); - if (!mainEl) return; - - /** @type {LH.ReportResult} */ - // @ts-expect-error - const lhr = window.__LIGHTHOUSE_JSON__; - renderLighthouseReport({ - lhr, - containerEl: mainEl, - }); - } - - __initLighthouseReport__(); - - document.addEventListener('lh-analytics', /** @param {Event} e */ e => { - // @ts-expect-error - if (window.ga) ga(e.detail.cmd, e.detail.fields); - }); - - document.addEventListener('lh-log', /** @param {Event} e */ e => { - const el = document.querySelector('#lh-log'); - if (!el) return; - - const logger = new Logger(el); - // @ts-expect-error - const detail = e.detail; - - switch (detail.cmd) { - case 'log': - logger.log(detail.msg); - break; - case 'warn': - logger.warn(detail.msg); - break; - case 'error': - logger.error(detail.msg); - break; - case 'hide': - logger.hide(); - break; - } - }); - -}()); diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js index f0b7049b114d..20c5ab635a31 100644 --- a/lighthouse-core/report/html/renderer/psi.js +++ b/lighthouse-core/report/html/renderer/psi.js @@ -21,7 +21,6 @@ */ 'use strict'; -// import {renderLighthouseReport} from './common/render.js'; import {DetailsRenderer} from './common/details-renderer.js'; import {DOM} from './common/dom.js'; import {ElementScreenshotRenderer} from './common/element-screenshot-renderer.js'; diff --git a/lighthouse-core/report/html/renderer/standalone.js b/lighthouse-core/report/html/renderer/standalone.js index 221b8de796a9..dfdf36f518d1 100644 --- a/lighthouse-core/report/html/renderer/standalone.js +++ b/lighthouse-core/report/html/renderer/standalone.js @@ -7,23 +7,25 @@ /* global document window ga */ -import {renderLighthouseReport} from './common/render.js'; +import {DOM} from './common/dom.js'; import {Logger} from './common/logger.js'; +import {ReportRenderer} from './common/report-renderer.js'; +import {ReportUIFeatures} from './common/report-ui-features.js'; -function __initLighthouseReport__() { - const mainEl = document.querySelector('main'); - if (!mainEl) return; - +(function __initLighthouseReport__() { + const dom = new DOM(document); + const renderer = new ReportRenderer(dom); + const container = dom.find('main', document); /** @type {LH.ReportResult} */ // @ts-expect-error const lhr = window.__LIGHTHOUSE_JSON__; - renderLighthouseReport({ - lhr, - containerEl: mainEl, - }); -} + renderer.renderReport(lhr, container); -__initLighthouseReport__(); + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(lhr); +})(); document.addEventListener('lh-analytics', /** @param {Event} e */ e => { // @ts-expect-error diff --git a/lighthouse-core/scripts/copy-util-commonjs.sh b/lighthouse-core/scripts/copy-util-commonjs.sh index 1083e5254158..1c0e70d90940 100644 --- a/lighthouse-core/scripts/copy-util-commonjs.sh +++ b/lighthouse-core/scripts/copy-util-commonjs.sh @@ -14,5 +14,5 @@ OUT_FILE="$LH_ROOT_DIR"/lighthouse-core/util-commonjs.js echo '// @ts-nocheck' > "$OUT_FILE" echo '// Auto-generated by lighthouse-core/scripts/copy-util-commonjs.sh' >> "$OUT_FILE" echo '// Temporary solution until all our code uses esmodules' >> "$OUT_FILE" -sed 's/export //g' "$LH_ROOT_DIR"/lighthouse-core/report/html/renderer/common/util.js >> "$OUT_FILE" +sed 's/export class Util/class Util/g' "$LH_ROOT_DIR"/lighthouse-core/report/html/renderer/common/util.js >> "$OUT_FILE" echo 'module.exports = Util;' >> "$OUT_FILE" diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 7ddf06a5b314..839b976d77d8 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -14,7 +14,7 @@ const path = require('path'); const assert = require('assert').strict; const tsc = require('typescript'); const MessageParser = require('intl-messageformat-parser').default; -const Util = require('../../report/html/renderer/util.js'); +const Util = require('../../util-commonjs.js'); const {collectAndBakeCtcStrings} = require('./bake-ctc-to-lhl.js'); const {pruneObsoleteLhlMessages} = require('./prune-obsolete-lhl-messages.js'); const {countTranslatedMessages} = require('./count-translated.js'); diff --git a/lighthouse-core/scripts/open-devtools.sh b/lighthouse-core/scripts/open-devtools.sh index 1f83ef68e2a2..33145d5df756 100644 --- a/lighthouse-core/scripts/open-devtools.sh +++ b/lighthouse-core/scripts/open-devtools.sh @@ -36,7 +36,6 @@ if ! which gn ; then export PYTHONPATH="${PYTHONPATH:-}:$BLINK_TOOLS_PATH/latest/third_party/typ" fi -yarn build-devtools yarn devtools "$DEVTOOLS_PATH" cd "$DEVTOOLS_PATH" diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index b7066f526be2..394bd1838e1e 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -39,26 +39,21 @@ mkdir -p "$fe_lh_dir" lh_bg_js="dist/lighthouse-dt-bundle.js" +yarn build-report +yarn build-devtools + # copy lighthouse-dt-bundle (potentially stale) cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." -# generate .d.ts files -npx tsc --allowJs --declaration --emitDeclarationOnly lighthouse-core/report/html/renderer/standalone.js +# generate report.d.ts +npx tsc --allowJs --declaration --emitDeclarationOnly dist/report.js # copy report code $fe_lh_dir fe_lh_report_dir="$fe_lh_dir/report/" -rsync -avh lighthouse-core/report/html/renderer/ "$fe_lh_report_dir" --exclude="BUILD.gn" --exclude="report-tsconfig.json" --exclude="generated" --exclude="psi.js" --delete -# file-namer.js is not used, but we should export something so it compiles. -echo 'export const getFilenamePrefix = () => {throw new Error("not used in CDT")};' > "$fe_lh_report_dir/common/file-namer.js" +cp dist/report.js dist/report.d.ts "$fe_lh_report_dir" echo -e "$check Report code copied." -# delete those .d.ts files -rm -rf lighthouse-core/report/html/renderer/**/*.d.ts -rm lighthouse-core/lib/file-namer.d.ts -# weird that this is needed too ... -rm lighthouse-core/report/html/renderer/standalone.d.ts - # copy report generator + cached resources into $fe_lh_dir fe_lh_report_assets_dir="$fe_lh_dir/report-assets/" rsync -avh dist/dt-report-resources/ "$fe_lh_report_assets_dir" --delete diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index 7de4b42eed1d..3c1c19bf53c1 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -7,7 +7,7 @@ const assert = require('assert').strict; const jsdom = require('jsdom'); -const DOM = require('../../report/html/renderer/dom.js'); +const DOM = require('../../report/html/renderer/common/dom.js'); const pageFunctions = require('../../lib/page-functions.js'); /* eslint-env jest */ diff --git a/lighthouse-core/test/report/html/renderer/category-renderer-test.js b/lighthouse-core/test/report/html/renderer/category-renderer-test.js index 88dad03374bc..c910db93935c 100644 --- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js @@ -10,13 +10,13 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); const CriticalRequestChainRenderer = require( - '../../../../report/html/renderer/crc-details-renderer.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); + '../../../../report/html/renderer/common/crc-details-renderer.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); const sampleResultsOrig = require('../../../results/sample_v2.json'); const TEMPLATE_FILE = fs.readFileSync(__dirname + diff --git a/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js b/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js index 8ed6fee49227..a36eb45bc9a4 100644 --- a/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js @@ -10,12 +10,12 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); const CriticalRequestChainRenderer = - require('../../../../report/html/renderer/crc-details-renderer.js'); + require('../../../../report/html/renderer/common/crc-details-renderer.js'); const TEMPLATE_FILE = fs.readFileSync(__dirname + '/../../../../report/html/templates.html', 'utf8'); diff --git a/lighthouse-core/test/report/html/renderer/details-renderer-test.js b/lighthouse-core/test/report/html/renderer/details-renderer-test.js index adbc450efe4d..a6bfe050e908 100644 --- a/lighthouse-core/test/report/html/renderer/details-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/details-renderer-test.js @@ -8,14 +8,15 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); -const SnippetRenderer = require('../../../../report/html/renderer/snippet-renderer.js'); -const CrcDetailsRenderer = require('../../../../report/html/renderer/crc-details-renderer.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); +const SnippetRenderer = require('../../../../report/html/renderer/common/snippet-renderer.js'); +const CrcDetailsRenderer = + require('../../../../report/html/renderer/common/crc-details-renderer.js'); const ElementScreenshotRenderer = - require('../../../../report/html/renderer/element-screenshot-renderer.js'); + require('../../../../report/html/renderer/common/element-screenshot-renderer.js'); const TEMPLATE_FILE = fs.readFileSync(__dirname + '/../../../../report/html/templates.html', 'utf8'); diff --git a/lighthouse-core/test/report/html/renderer/dom-test.js b/lighthouse-core/test/report/html/renderer/dom-test.js index e1b5706966e7..805dffa54094 100644 --- a/lighthouse-core/test/report/html/renderer/dom-test.js +++ b/lighthouse-core/test/report/html/renderer/dom-test.js @@ -8,9 +8,9 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); const TEMPLATE_FILE = fs.readFileSync(__dirname + '/../../../../report/html/templates.html', 'utf8'); diff --git a/lighthouse-core/test/report/html/renderer/element-screenshot-renderer-test.js b/lighthouse-core/test/report/html/renderer/element-screenshot-renderer-test.js index 65740b8cd5f4..c023b662eead 100644 --- a/lighthouse-core/test/report/html/renderer/element-screenshot-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/element-screenshot-renderer-test.js @@ -10,11 +10,11 @@ const fs = require('fs'); const jsdom = require('jsdom'); const ElementScreenshotRenderer = - require('../../../../report/html/renderer/element-screenshot-renderer.js'); + require('../../../../report/html/renderer/common/element-screenshot-renderer.js'); const RectHelpers = require('../../../../../lighthouse-core/lib/rect-helpers.js'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); const TEMPLATE_FILE = fs.readFileSync( __dirname + '/../../../../report/html/templates.html', diff --git a/lighthouse-core/test/report/html/renderer/i18n-test.js b/lighthouse-core/test/report/html/renderer/i18n-test.js index 3921ca9cc767..5c6c9d94247f 100644 --- a/lighthouse-core/test/report/html/renderer/i18n-test.js +++ b/lighthouse-core/test/report/html/renderer/i18n-test.js @@ -6,8 +6,8 @@ 'use strict'; const assert = require('assert').strict; -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); // Require i18n to make sure Intl is polyfilled in Node without full-icu for testing. // When Util is run in a browser, Intl will be supplied natively (IE11+). diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js index a8eb0c3f06ac..5b7b375d6f0b 100644 --- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js @@ -10,14 +10,14 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); const URL = require('../../../../lib/url-shim.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); const CriticalRequestChainRenderer = require( - '../../../../report/html/renderer/crc-details-renderer.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); + '../../../../report/html/renderer/common/crc-details-renderer.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); const sampleResultsOrig = require('../../../results/sample_v2.json'); const TEMPLATE_FILE = fs.readFileSync(__dirname + @@ -35,7 +35,7 @@ describe('PerfCategoryRenderer', () => { global.CategoryRenderer = CategoryRenderer; const PerformanceCategoryRenderer = - require('../../../../report/html/renderer/performance-category-renderer.js'); + require('../../../../report/html/renderer/common/performance-category-renderer.js'); const {document} = new jsdom.JSDOM(TEMPLATE_FILE).window; const dom = new DOM(document); diff --git a/lighthouse-core/test/report/html/renderer/psi-test.js b/lighthouse-core/test/report/html/renderer/psi-test.js index 2199c8122eb2..e8b5a206827d 100644 --- a/lighthouse-core/test/report/html/renderer/psi-test.js +++ b/lighthouse-core/test/report/html/renderer/psi-test.js @@ -12,17 +12,17 @@ const jsdom = require('jsdom'); const testUtils = require('../../../test-utils.js'); const prepareLabData = require('../../../../report/html/renderer/psi.js'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); const CriticalRequestChainRenderer = - require('../../../../report/html/renderer/crc-details-renderer.js'); + require('../../../../report/html/renderer/common/crc-details-renderer.js'); const ElementScreenshotRenderer = - require('../../../../report/html/renderer/element-screenshot-renderer.js'); + require('../../../../report/html/renderer/common/element-screenshot-renderer.js'); const ReportUIFeatures = - require('../../../../report/html/renderer/report-ui-features.js'); + require('../../../../report/html/renderer/common/report-ui-features.js'); const {itIfProtoExists, sampleResultsRoundtripStr} = testUtils.getProtoRoundTrip(); const sampleResultsStr = fs.readFileSync(__dirname + '/../../../results/sample_v2.json', 'utf-8'); @@ -46,7 +46,7 @@ describe('DOM', () => { // Delayed so that CategoryRenderer is in global scope const PerformanceCategoryRenderer = - require('../../../../report/html/renderer/performance-category-renderer.js'); + require('../../../../report/html/renderer/common/performance-category-renderer.js'); global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; global.ElementScreenshotRenderer = ElementScreenshotRenderer; diff --git a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js index 62752def9c64..5a6d7e8b60d7 100644 --- a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js @@ -10,11 +10,11 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); const sampleResultsOrig = require('../../../results/sample_v2.json'); const TEMPLATE_FILE = fs.readFileSync(__dirname + @@ -31,7 +31,7 @@ describe('PwaCategoryRenderer', () => { global.CategoryRenderer = CategoryRenderer; const PwaCategoryRenderer = - require('../../../../report/html/renderer/pwa-category-renderer.js'); + require('../../../../report/html/renderer/common/pwa-category-renderer.js'); const {document} = new jsdom.JSDOM(TEMPLATE_FILE).window; const dom = new DOM(document); diff --git a/lighthouse-core/test/report/html/renderer/report-renderer-test.js b/lighthouse-core/test/report/html/renderer/report-renderer-test.js index a8a8534d292e..cbda069d5994 100644 --- a/lighthouse-core/test/report/html/renderer/report-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/report-renderer-test.js @@ -10,18 +10,18 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); const URL = require('../../../../lib/url-shim.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); -const ReportUIFeatures = require('../../../../report/html/renderer/report-ui-features.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); +const ReportUIFeatures = require('../../../../report/html/renderer/common/report-ui-features.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); const ElementScreenshotRenderer = - require('../../../../report/html/renderer/element-screenshot-renderer.js'); + require('../../../../report/html/renderer/common/element-screenshot-renderer.js'); const CriticalRequestChainRenderer = require( - '../../../../report/html/renderer/crc-details-renderer.js'); -const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js'); + '../../../../report/html/renderer/common/crc-details-renderer.js'); +const ReportRenderer = require('../../../../report/html/renderer/common/report-renderer.js'); const sampleResultsOrig = require('../../../results/sample_v2.json'); const TIMESTAMP_REGEX = /\d+, \d{4}.*\d+:\d+/; @@ -43,9 +43,9 @@ describe('ReportRenderer', () => { // lazy loaded because they depend on CategoryRenderer to be available globally global.PerformanceCategoryRenderer = - require('../../../../report/html/renderer/performance-category-renderer.js'); + require('../../../../report/html/renderer/common/performance-category-renderer.js'); global.PwaCategoryRenderer = - require('../../../../report/html/renderer/pwa-category-renderer.js'); + require('../../../../report/html/renderer/common/pwa-category-renderer.js'); // Stub out matchMedia for Node. global.matchMedia = function() { diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js index 21a4ad51ef99..ef0455ff39bc 100644 --- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js +++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js @@ -10,18 +10,18 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js'); -const ReportUIFeatures = require('../../../../report/html/renderer/report-ui-features.js'); -const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const DetailsRenderer = require('../../../../report/html/renderer/common/details-renderer.js'); +const ReportUIFeatures = require('../../../../report/html/renderer/common/report-ui-features.js'); +const CategoryRenderer = require('../../../../report/html/renderer/common/category-renderer.js'); const ElementScreenshotRenderer = - require('../../../../report/html/renderer/element-screenshot-renderer.js'); + require('../../../../report/html/renderer/common/element-screenshot-renderer.js'); const RectHelpers = require('../../../../../lighthouse-core/lib/rect-helpers.js'); const CriticalRequestChainRenderer = require( - '../../../../report/html/renderer/crc-details-renderer.js'); -const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js'); + '../../../../report/html/renderer/common/crc-details-renderer.js'); +const ReportRenderer = require('../../../../report/html/renderer/common/report-renderer.js'); const sampleResultsOrig = require('../../../results/sample_v2.json'); const TEMPLATE_FILE = fs.readFileSync(__dirname + @@ -60,9 +60,9 @@ describe('ReportUIFeatures', () => { // lazy loaded because they depend on CategoryRenderer to be available globally global.PerformanceCategoryRenderer = - require('../../../../report/html/renderer/performance-category-renderer.js'); + require('../../../../report/html/renderer/common/performance-category-renderer.js'); global.PwaCategoryRenderer = - require('../../../../report/html/renderer/pwa-category-renderer.js'); + require('../../../../report/html/renderer/common/pwa-category-renderer.js'); // Stub out matchMedia for Node. global.matchMedia = function() { diff --git a/lighthouse-core/test/report/html/renderer/snippet-renderer-test.js b/lighthouse-core/test/report/html/renderer/snippet-renderer-test.js index 373c36e98525..3d393abd499d 100644 --- a/lighthouse-core/test/report/html/renderer/snippet-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/snippet-renderer-test.js @@ -10,10 +10,10 @@ const assert = require('assert').strict; const fs = require('fs'); const jsdom = require('jsdom'); -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); -const DOM = require('../../../../report/html/renderer/dom.js'); -const SnippetRenderer = require('../../../../report/html/renderer/snippet-renderer.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); +const DOM = require('../../../../report/html/renderer/common/dom.js'); +const SnippetRenderer = require('../../../../report/html/renderer/common/snippet-renderer.js'); const TEMPLATE_FILE = fs.readFileSync( __dirname + '/../../../../report/html/templates.html', diff --git a/lighthouse-core/test/report/html/renderer/text-encoding-test.js b/lighthouse-core/test/report/html/renderer/text-encoding-test.js index 493e1b26b28c..02994d65f65a 100644 --- a/lighthouse-core/test/report/html/renderer/text-encoding-test.js +++ b/lighthouse-core/test/report/html/renderer/text-encoding-test.js @@ -5,7 +5,7 @@ */ 'use strict'; -const TextEncoding = require('../../../../report/html/renderer/text-encoding.js'); +const TextEncoding = require('../../../../report/html/renderer/common/text-encoding.js'); /* eslint-env jest */ diff --git a/lighthouse-core/test/report/html/renderer/util-test.js b/lighthouse-core/test/report/html/renderer/util-test.js index 6203be9f0cd2..a8abd5906f39 100644 --- a/lighthouse-core/test/report/html/renderer/util-test.js +++ b/lighthouse-core/test/report/html/renderer/util-test.js @@ -6,8 +6,8 @@ 'use strict'; const assert = require('assert').strict; -const Util = require('../../../../report/html/renderer/util.js'); -const I18n = require('../../../../report/html/renderer/i18n.js'); +const Util = require('../../../../report/html/renderer/common/util.js'); +const I18n = require('../../../../report/html/renderer/common/i18n.js'); const sampleResult = require('../../../results/sample_v2.json'); /* eslint-env jest */ diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js index 5d03ed96dbd7..d6515c6e6a82 100644 --- a/lighthouse-treemap/app/src/util.js +++ b/lighthouse-treemap/app/src/util.js @@ -9,7 +9,7 @@ /** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */ /** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector} ParseSelector */ -/** @template T @typedef {import('../../../lighthouse-core/report/html/renderer/i18n')} I18n */ +/** @template T @typedef {import('../../../lighthouse-core/report/html/renderer/common/i18n').I18n} I18n */ class TreemapUtil { /** diff --git a/lighthouse-treemap/types/treemap.d.ts b/lighthouse-treemap/types/treemap.d.ts index 4c44d77b31f0..ff50c5c657b3 100644 --- a/lighthouse-treemap/types/treemap.d.ts +++ b/lighthouse-treemap/types/treemap.d.ts @@ -1,9 +1,11 @@ import _TreemapUtil = require('../app/src/util.js'); -import _TextEncoding = require('../../lighthouse-core/report/html/renderer/text-encoding.js'); import _DragAndDrop = require('../../lighthouse-viewer/app/src/drag-and-drop.js'); import _FirebaseAuth = require('../../lighthouse-viewer/app/src/firebase-auth.js'); import _GithubApi = require('../../lighthouse-viewer/app/src/github-api.js'); -import _Logger = require('../../lighthouse-core/report/html/renderer/logger.js'); +import {TextEncoding as _TextEncoding} from '../../lighthouse-core/report/html/renderer/common/text-encoding.js'; +import {Logger as _Logger} from '../../lighthouse-core/report/html/renderer/common/logger.js'; +import {I18n as _I18n} from '../../lighthouse-core/report/html/renderer/common/i18n.js'; +import {getFilenamePrefix as _getFilenamePrefix} from '../../lighthouse-core/report/html/renderer/common/file-namer.js'; import {FirebaseNamespace} from '@firebase/app-types'; declare global { @@ -45,6 +47,8 @@ declare global { var firebase: Required; var idbKeyval: typeof import('idb-keyval'); var strings: Record; + var getFilenamePrefix: typeof _getFilenamePrefix; + var I18n: typeof _I18n; interface Window { logger: _Logger; diff --git a/lighthouse-viewer/app/src/viewer-ui-features.js b/lighthouse-viewer/app/src/viewer-ui-features.js index b4f33fde821c..9ea480f441f2 100644 --- a/lighthouse-viewer/app/src/viewer-ui-features.js +++ b/lighthouse-viewer/app/src/viewer-ui-features.js @@ -7,7 +7,7 @@ /* global ReportUIFeatures, ReportGenerator */ -/** @typedef {import('../../../lighthouse-core/report/html/renderer/dom')} DOM */ +/** @typedef {import('../../../lighthouse-core/report/html/renderer/common/dom').DOM} DOM */ /** * Extends ReportUIFeatures to add an (optional) ability to save to a gist and diff --git a/lighthouse-viewer/types/viewer.d.ts b/lighthouse-viewer/types/viewer.d.ts index ca3fcf1fd8d6..1f9edaa5559b 100644 --- a/lighthouse-viewer/types/viewer.d.ts +++ b/lighthouse-viewer/types/viewer.d.ts @@ -5,7 +5,11 @@ */ import _ReportGenerator = require('../../lighthouse-core/report/report-generator.js'); -import _Logger = require('../../lighthouse-core/report/html/renderer/logger.js'); +import {DOM as _DOM} from '../../lighthouse-core/report/html/renderer/common/dom.js'; +import {ReportRenderer as _ReportRenderer} from '../../lighthouse-core/report/html/renderer/common/report-renderer.js'; +import {ReportUIFeatures as _ReportUIFeatures} from '../../lighthouse-core/report/html/renderer/common/report-ui-features.js'; +import {Logger as _Logger} from '../../lighthouse-core/report/html/renderer/common/logger.js'; +import {getFilenamePrefix as _getFilenamePrefix} from '../../lighthouse-core/report/html/renderer/common/file-namer.js'; import _LighthouseReportViewer = require('../app/src/lighthouse-report-viewer.js'); import _DragAndDrop = require('../app/src/drag-and-drop.js'); import _GithubApi = require('../app/src/github-api.js'); @@ -19,8 +23,12 @@ import '@firebase/auth-types'; declare global { var ReportGenerator: typeof _ReportGenerator; + var DOM: typeof _DOM; + var ReportRenderer: typeof _ReportRenderer; + var ReportUIFeatures: typeof _ReportUIFeatures; var Logger: typeof _Logger; var logger: _Logger; + var getFilenamePrefix: typeof _getFilenamePrefix; var LighthouseReportViewer: typeof _LighthouseReportViewer; var DragAndDrop: typeof _DragAndDrop; var GithubApi: typeof _GithubApi; diff --git a/package.json b/package.json index 60b0ea57b96b..1439aee9d532 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ }, "scripts": { "build-all": "npm-run-posix-or-windows build-all:task", - "build-all:task": "yarn build-cdt-lib && yarn build-devtools && (yarn build-extension & yarn build-lr & yarn build-viewer & yarn build-treemap & yarn build-smokehouse-bundle & wait) && yarn build-pack", - "build-all:task:windows": "yarn build-cdt-lib && yarn build-extension && yarn build-devtools && yarn build-lr && yarn build-viewer && yarn build-treemap && yarn build-smokehouse-bundle", + "build-all:task": "yarn build-report && yarn build-cdt-lib && yarn build-devtools && (yarn build-extension & yarn build-lr & yarn build-viewer & yarn build-treemap & yarn build-smokehouse-bundle & wait) && yarn build-pack", + "build-all:task:windows": "yarn build-report && yarn build-cdt-lib && yarn build-extension && yarn build-devtools && yarn build-lr && yarn build-viewer && yarn build-treemap && yarn build-smokehouse-bundle", "build-cdt-lib": "node ./build/build-cdt-lib.js", "build-extension": "yarn build-extension-chrome && yarn build-extension-firefox", "build-extension-chrome": "node ./build/build-extension.js chrome", @@ -23,6 +23,7 @@ "build-smokehouse-bundle": "node ./build/build-smokehouse-bundle.js", "build-lr": "yarn reset-link && node ./build/build-lightrider-bundles.js", "build-pack": "bash build/build-pack.sh", + "build-report": "node build/build-report.js", "build-treemap": "node ./build/build-treemap.js", "build-viewer": "node ./build/build-viewer.js", "reset-link": "(yarn unlink || true) && yarn link && yarn link lighthouse", diff --git a/tsconfig.json b/tsconfig.json index b145463b9a32..80ea41a164d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ ], "exclude": [ "lighthouse-core/lib/cdt", + "lighthouse-core/report/html/renderer/generated", "lighthouse-core/test/audits/**/*.js", "lighthouse-core/test/fixtures/**/*.js", "lighthouse-core/test/report/**/*.js", From 0107c03cf612f0f1cb73950efd2c7d3fe85f1f97 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 20:01:47 -0700 Subject: [PATCH 11/71] report/clients --- .gitignore | 2 +- build/build-report.js | 4 ++-- lighthouse-core/scripts/copy-util-commonjs.sh | 2 +- report/{renderer => clients}/bundle.js | 6 +++--- report/{renderer => clients}/psi.js | 0 report/{renderer => clients}/standalone.js | 8 ++++---- report/report-assets.js | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) rename report/{renderer => clients}/bundle.js (84%) rename report/{renderer => clients}/psi.js (100%) rename report/{renderer => clients}/standalone.js (89%) diff --git a/.gitignore b/.gitignore index 6e08471ce4a3..d1d204a1d169 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ last-run-results.html *.artifacts.log *.ctc.json -lighthouse-core/report/html/renderer/generated +report/generated !lighthouse-core/test/results/artifacts/*.trace.json !lighthouse-core/test/results/artifacts/*.devtoolslog.json diff --git a/build/build-report.js b/build/build-report.js index f6fad3113bd8..227a28e44d9a 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -16,7 +16,7 @@ const commonjs = async function buildStandaloneReport() { const bundle = await rollup.rollup({ - input: 'report/renderer/standalone.js', + input: 'report/clients/standalone.js', plugins: [ commonjs(), ], @@ -32,7 +32,7 @@ async function buildStandaloneReport() { async function buildEsModulesBundle() { const bundle = await rollup.rollup({ - input: 'report/renderer/bundle.js', + input: 'report/clients/bundle.js', plugins: [ commonjs(), ], diff --git a/lighthouse-core/scripts/copy-util-commonjs.sh b/lighthouse-core/scripts/copy-util-commonjs.sh index 1c0e70d90940..85d10249b96e 100644 --- a/lighthouse-core/scripts/copy-util-commonjs.sh +++ b/lighthouse-core/scripts/copy-util-commonjs.sh @@ -14,5 +14,5 @@ OUT_FILE="$LH_ROOT_DIR"/lighthouse-core/util-commonjs.js echo '// @ts-nocheck' > "$OUT_FILE" echo '// Auto-generated by lighthouse-core/scripts/copy-util-commonjs.sh' >> "$OUT_FILE" echo '// Temporary solution until all our code uses esmodules' >> "$OUT_FILE" -sed 's/export class Util/class Util/g' "$LH_ROOT_DIR"/lighthouse-core/report/html/renderer/common/util.js >> "$OUT_FILE" +sed 's/export class Util/class Util/g' "$LH_ROOT_DIR"/report/renderer/util.js >> "$OUT_FILE" echo 'module.exports = Util;' >> "$OUT_FILE" diff --git a/report/renderer/bundle.js b/report/clients/bundle.js similarity index 84% rename from report/renderer/bundle.js rename to report/clients/bundle.js index d35043cf2c3f..261548266b20 100644 --- a/report/renderer/bundle.js +++ b/report/clients/bundle.js @@ -11,6 +11,6 @@ // until we work out a common rendering interface. // See: https://github.com/GoogleChrome/lighthouse/pull/12623 -export {DOM} from './common/dom.js'; -export {ReportRenderer} from './common/report-renderer.js'; -export {ReportUIFeatures} from './common/report-ui-features.js'; +export {DOM} from '../renderer/dom.js'; +export {ReportRenderer} from '../renderer/report-renderer.js'; +export {ReportUIFeatures} from '../renderer/report-ui-features.js'; diff --git a/report/renderer/psi.js b/report/clients/psi.js similarity index 100% rename from report/renderer/psi.js rename to report/clients/psi.js diff --git a/report/renderer/standalone.js b/report/clients/standalone.js similarity index 89% rename from report/renderer/standalone.js rename to report/clients/standalone.js index dfdf36f518d1..8235219a179f 100644 --- a/report/renderer/standalone.js +++ b/report/clients/standalone.js @@ -7,10 +7,10 @@ /* global document window ga */ -import {DOM} from './common/dom.js'; -import {Logger} from './common/logger.js'; -import {ReportRenderer} from './common/report-renderer.js'; -import {ReportUIFeatures} from './common/report-ui-features.js'; +import {DOM} from '../renderer/dom.js'; +import {Logger} from '../renderer/logger.js'; +import {ReportRenderer} from '../renderer/report-renderer.js'; +import {ReportUIFeatures} from '../renderer/report-ui-features.js'; (function __initLighthouseReport__() { const dom = new DOM(document); diff --git a/report/report-assets.js b/report/report-assets.js index 0af138c17c05..2b2dfb6a6ded 100644 --- a/report/report-assets.js +++ b/report/report-assets.js @@ -8,7 +8,7 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); -const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/renderer/generated/standalone.js', 'utf8'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/generated/standalone.js', 'utf8'); const REPORT_CSS = fs.readFileSync(__dirname + '/assets/styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/assets/templates.html', 'utf8'); From cc3b98c973144f46f278e5370156037f04f25adb Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 20:08:22 -0700 Subject: [PATCH 12/71] terser --- build/build-report.js | 5 ++-- package.json | 1 + yarn.lock | 58 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 227a28e44d9a..ef5ae21883f7 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -10,6 +10,8 @@ // esmodules bundle (for devtools/whatever): dist/report.js seems good. don't check in cuz dont need it for publishing. const rollup = require('rollup'); +const {terser} = require('rollup-plugin-terser'); +// Only needed b/c getFilenamePrefix loads a commonjs module. const commonjs = // @ts-expect-error types are wrong. /** @type {import('rollup-plugin-commonjs').default} */ (require('rollup-plugin-commonjs')); @@ -19,6 +21,7 @@ async function buildStandaloneReport() { input: 'report/clients/standalone.js', plugins: [ commonjs(), + terser(), ], }); @@ -26,8 +29,6 @@ async function buildStandaloneReport() { file: 'report/generated/standalone.js', format: 'iife', }); - - // TODO: run thru terser. } async function buildEsModulesBundle() { diff --git a/package.json b/package.json index 61fafb198967..3df643e1c2bb 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "puppeteer": "^9.1.1", "rollup": "^2.50.6", "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-terser": "^7.0.2", "tabulator-tables": "^4.9.3", "terser": "^5.3.8", "typed-query-selector": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index adb0b79211f0..5a58645a3069 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,13 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/code-frame@^7.10.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + dependencies: + "@babel/highlight" "^7.14.5" + "@babel/compat-data@^7.13.12": version "7.13.15" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4" @@ -228,6 +235,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== +"@babel/helper-validator-identifier@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" + integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -260,6 +272,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.15": version "7.13.15" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8" @@ -5265,6 +5286,15 @@ jest-watcher@^27.0.2: jest-util "^27.0.2" string-length "^4.0.1" +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest-worker@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.2.tgz#4ebeb56cef48b3e7514552f80d0d80c0129f0b05" @@ -6827,7 +6857,7 @@ quote-stream@^1.0.1: minimist "^1.1.3" through2 "^2.0.0" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -7176,6 +7206,16 @@ rollup-plugin-commonjs@^10.1.0: resolve "^1.11.0" rollup-pluginutils "^2.8.1" +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + rollup-pluginutils@^2.8.1: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" @@ -7279,6 +7319,13 @@ semver@^7.2.1, semver@^7.3.2: dependencies: lru-cache "^6.0.0" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -7887,6 +7934,15 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +terser@^5.0.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" + integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + terser@^5.3.8: version "5.3.8" resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.8.tgz#991ae8ba21a3d990579b54aa9af11586197a75dd" From 18eb5a43f31f883475b87b5812ca8b29439d9f81 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 20:12:05 -0700 Subject: [PATCH 13/71] tsignore --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d4e012a106ad..4780a3266007 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ ], "exclude": [ "lighthouse-core/lib/cdt", - "lighthouse-core/report/html/renderer/generated", "lighthouse-core/test/audits/**/*.js", "lighthouse-core/test/fixtures/**/*.js", "lighthouse-core/test/computed/**/*.js", @@ -30,6 +29,7 @@ "lighthouse-cli/test/fixtures/**/*.js", "lighthouse-core/scripts/legacy-javascript/variants", "lighthouse-core/test/chromium-web-tests/webtests", + "report/generated", // These test files require further changes before they can be type checked. "lighthouse-core/test/config/budget-test.js", "lighthouse-core/test/config/config-helpers-test.js", From 0ac3a8246c777eefec22fd8cca79266fb0457b3a Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 20:15:13 -0700 Subject: [PATCH 14/71] comments --- build/build-report.js | 4 ---- report/clients/psi.js | 5 ----- 2 files changed, 9 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index ef5ae21883f7..c4c5be9f4b52 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -5,10 +5,6 @@ */ 'use strict'; -// TODO: where to output? -// standalone: report/generated/standalone.js + checking into source seems simplest for publishing. -// esmodules bundle (for devtools/whatever): dist/report.js seems good. don't check in cuz dont need it for publishing. - const rollup = require('rollup'); const {terser} = require('rollup-plugin-terser'); // Only needed b/c getFilenamePrefix loads a commonjs module. diff --git a/report/clients/psi.js b/report/clients/psi.js index 20c5ab635a31..10cefd3b707c 100644 --- a/report/clients/psi.js +++ b/report/clients/psi.js @@ -1,8 +1,3 @@ -// TODO: restructure folders? -// html/renderer/common -> mostly everything, including index.js -// html/renderer/clients -> psi.js, standalone.js -// TODO: figure out how psi.js / report code should be added to google3. - /** * @license * Copyright 2018 The Lighthouse Authors. All Rights Reserved. From ad0974c623f8ed13e2368b0df18dc7ee8c687545 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 21:04:50 -0700 Subject: [PATCH 15/71] fix viewer --- build/build-report.js | 22 ++++++++++++++++++++-- build/build-viewer.js | 5 ++++- report/clients/viewer.js | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 report/clients/viewer.js diff --git a/build/build-report.js b/build/build-report.js index c4c5be9f4b52..7695ac0a088a 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -27,6 +27,20 @@ async function buildStandaloneReport() { }); } +async function buildViewerReport() { + const bundle = await rollup.rollup({ + input: 'report/clients/viewer.js', + plugins: [ + commonjs(), + ], + }); + + await bundle.write({ + file: 'dist/viewer-report.js', + format: 'iife', + }); +} + async function buildEsModulesBundle() { const bundle = await rollup.rollup({ input: 'report/clients/bundle.js', @@ -41,9 +55,13 @@ async function buildEsModulesBundle() { }); } -buildStandaloneReport(); -buildEsModulesBundle(); + +if (require.main === module) { + buildStandaloneReport(); + buildEsModulesBundle(); +} module.exports = { buildStandaloneReport, + buildViewerReport, }; diff --git a/build/build-viewer.js b/build/build-viewer.js index 1c7956b7db47..0a3d7a4429da 100644 --- a/build/build-viewer.js +++ b/build/build-viewer.js @@ -9,6 +9,7 @@ const fs = require('fs'); const browserify = require('browserify'); const GhPagesApp = require('./gh-pages-app.js'); const {minifyFileTransform} = require('./build-utils.js'); +const {buildViewerReport} = require('./build-report.js'); const htmlReportAssets = require('../report/report-assets.js'); /** @@ -30,6 +31,8 @@ async function run() { }); }); + await buildViewerReport(); + const app = new GhPagesApp({ name: 'viewer', appDir: `${__dirname}/../lighthouse-viewer/app`, @@ -43,7 +46,7 @@ async function run() { ], javascripts: [ await generatorJsPromise, - htmlReportAssets.REPORT_JAVASCRIPT, + fs.readFileSync(__dirname + '/../dist/viewer-report.js', 'utf8'), fs.readFileSync(require.resolve('idb-keyval/dist/idb-keyval-min.js'), 'utf8'), {path: 'src/*'}, ], diff --git a/report/clients/viewer.js b/report/clients/viewer.js new file mode 100644 index 000000000000..2d763d3ea7f7 --- /dev/null +++ b/report/clients/viewer.js @@ -0,0 +1,16 @@ +/** + * @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 {Logger} from '../renderer/logger.js'; +import {ReportRenderer} from '../renderer/report-renderer.js'; +import {ReportUIFeatures} from '../renderer/report-ui-features.js'; + +window.DOM = DOM; +window.Logger = Logger; +window.ReportRenderer = ReportRenderer; +window.ReportUIFeatures = ReportUIFeatures; From 593906b9ba614da9040e2e4e4849a140daac86a6 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 24 Jun 2021 21:08:38 -0700 Subject: [PATCH 16/71] fix devtools action --- .github/workflows/devtools.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/devtools.yml b/.github/workflows/devtools.yml index 8b277623b7ec..dcce705d0e42 100644 --- a/.github/workflows/devtools.yml +++ b/.github/workflows/devtools.yml @@ -42,6 +42,8 @@ jobs: - run: yarn --frozen-lockfile working-directory: ${{ github.workspace }}/lighthouse + - run: yarn build-report + working-directory: ${{ github.workspace }}/lighthouse - run: yarn build-devtools working-directory: ${{ github.workspace }}/lighthouse From 3a38115335898ef2d22fa602d4a1c34e3b777cf6 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:02:22 -0700 Subject: [PATCH 17/71] build: add build step for report --- .github/workflows/ci.yml | 1 + .github/workflows/devtools.yml | 2 + .gitignore | 2 + build/build-report.js | 38 +++++++++++++++ build/build-viewer.js | 3 ++ build/readme.md | 2 +- docs/releasing.md | 1 - lighthouse-core/runner.js | 10 ++++ lighthouse-core/scripts/open-devtools.sh | 1 - lighthouse-core/scripts/roll-to-devtools.sh | 3 ++ package.json | 5 +- report/assets/standalone-template.html | 42 +---------------- report/clients/standalone.js | 52 +++++++++++++++++++++ report/report-assets.js | 21 +-------- 14 files changed, 118 insertions(+), 65 deletions(-) create mode 100644 build/build-report.js create mode 100644 report/clients/standalone.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a0b933e28c1..28bfd259e619 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - run: yarn test-legacy-javascript - run: yarn i18n:checks - run: yarn dogfood-lhci + - run: sh lighthouse-core/scripts/copy-util-commonjs.sh # Fail if any changes were written to any source files or generated untracked files (ex, from: build/build-cdt-lib.js). - run: git add -A && git diff --cached --exit-code diff --git a/.github/workflows/devtools.yml b/.github/workflows/devtools.yml index 8b277623b7ec..dcce705d0e42 100644 --- a/.github/workflows/devtools.yml +++ b/.github/workflows/devtools.yml @@ -42,6 +42,8 @@ jobs: - run: yarn --frozen-lockfile working-directory: ${{ github.workspace }}/lighthouse + - run: yarn build-report + working-directory: ${{ github.workspace }}/lighthouse - run: yarn build-devtools working-directory: ${{ github.workspace }}/lighthouse diff --git a/.gitignore b/.gitignore index 25096b00309f..d1d204a1d169 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ last-run-results.html *.artifacts.log *.ctc.json +report/generated + !lighthouse-core/test/results/artifacts/*.trace.json !lighthouse-core/test/results/artifacts/*.devtoolslog.json !lighthouse-core/test/fixtures/artifacts/**/*.trace.json diff --git a/build/build-report.js b/build/build-report.js new file mode 100644 index 000000000000..8d153ced0972 --- /dev/null +++ b/build/build-report.js @@ -0,0 +1,38 @@ +/** + * @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'; + +const fs = require('fs'); + +async function buildStandaloneReport() { + const REPORT_JAVASCRIPT = [ + fs.readFileSync(__dirname + '/../report/renderer/util.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/dom.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/details-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/crc-details-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/snippet-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/element-screenshot-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../lighthouse-core/lib/file-namer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/logger.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/report-ui-features.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/performance-category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/pwa-category-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/report-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/i18n.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/renderer/text-encoding.js', 'utf8'), + fs.readFileSync(__dirname + '/../report/clients/standalone.js', 'utf8'), + ].join(';\n'); + fs.writeFileSync(__dirname + '/../report/generated/standalone.js', REPORT_JAVASCRIPT); +} + +if (require.main === module) { + buildStandaloneReport(); +} + +module.exports = { + buildStandaloneReport, +}; diff --git a/build/build-viewer.js b/build/build-viewer.js index 1c7956b7db47..65aaa6565cbd 100644 --- a/build/build-viewer.js +++ b/build/build-viewer.js @@ -9,6 +9,7 @@ const fs = require('fs'); const browserify = require('browserify'); const GhPagesApp = require('./gh-pages-app.js'); const {minifyFileTransform} = require('./build-utils.js'); +const {buildStandaloneReport} = require('./build-report.js'); const htmlReportAssets = require('../report/report-assets.js'); /** @@ -30,6 +31,8 @@ async function run() { }); }); + await buildStandaloneReport(); + const app = new GhPagesApp({ name: 'viewer', appDir: `${__dirname}/../lighthouse-viewer/app`, diff --git a/build/readme.md b/build/readme.md index c8900c9c6751..164f423e2d41 100644 --- a/build/readme.md +++ b/build/readme.md @@ -15,7 +15,7 @@ Additionally, there are build processes for: To build the devtools files and roll them into a local checkout of Chromium: ```sh -yarn build-devtools && yarn devtools +yarn devtools ``` diff --git a/docs/releasing.md b/docs/releasing.md index 3f4578516ba7..87d6c7f32e52 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -154,7 +154,6 @@ echo "Complete the _Release publicity_ tasks documented above" ```sh git checkout vx.x.x # Checkout the specific version. -yarn build-devtools yarn devtools ~/src/devtools/devtools-frontend cd ~/src/devtools/devtools-frontend diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index 82c35c57e92a..aced7a7d1f6d 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -169,6 +169,16 @@ class Runner { assetSaver.saveLhr(lhr, path); } + // Build report if in local dev env so we don't have to run a watch command. + const forHtml = settings.output === 'html' || + (Array.isArray(settings.output) && settings.output.includes('html')); + if (forHtml && !global.isDevtools && !global.isLightrider && + fs.existsSync('dist') && fs.existsSync('.git')) { + // Prevent bundling. + const buildReportPath = '../build/build-report.js'; + await require(buildReportPath).buildStandaloneReport(); + } + // Create the HTML, JSON, and/or CSV string const report = generateReport(lhr, settings.output); diff --git a/lighthouse-core/scripts/open-devtools.sh b/lighthouse-core/scripts/open-devtools.sh index 1f83ef68e2a2..33145d5df756 100644 --- a/lighthouse-core/scripts/open-devtools.sh +++ b/lighthouse-core/scripts/open-devtools.sh @@ -36,7 +36,6 @@ if ! which gn ; then export PYTHONPATH="${PYTHONPATH:-}:$BLINK_TOOLS_PATH/latest/third_party/typ" fi -yarn build-devtools yarn devtools "$DEVTOOLS_PATH" cd "$DEVTOOLS_PATH" diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index 170c7aa438f2..e99dc27da9de 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -39,6 +39,9 @@ mkdir -p "$fe_lh_dir" lh_bg_js="dist/lighthouse-dt-bundle.js" +yarn build-report +yarn build-devtools + # copy lighthouse-dt-bundle (potentially stale) cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." diff --git a/package.json b/package.json index 890b5e64e8f1..e273b75a43ea 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ }, "scripts": { "build-all": "npm-run-posix-or-windows build-all:task", - "build-all:task": "yarn build-cdt-lib && yarn build-devtools && (yarn build-extension & yarn build-lr & yarn build-viewer & yarn build-treemap & yarn build-smokehouse-bundle & wait) && yarn build-pack", - "build-all:task:windows": "yarn build-cdt-lib && yarn build-extension && yarn build-devtools && yarn build-lr && yarn build-viewer && yarn build-treemap && yarn build-smokehouse-bundle", + "build-all:task": "yarn build-report && yarn build-cdt-lib && yarn build-devtools && (yarn build-extension & yarn build-lr & yarn build-viewer & yarn build-treemap & yarn build-smokehouse-bundle & wait) && yarn build-pack", + "build-all:task:windows": "yarn build-report && yarn build-cdt-lib && yarn build-extension && yarn build-devtools && yarn build-lr && yarn build-viewer && yarn build-treemap && yarn build-smokehouse-bundle", "build-cdt-lib": "node ./build/build-cdt-lib.js", "build-extension": "yarn build-extension-chrome && yarn build-extension-firefox", "build-extension-chrome": "node ./build/build-extension.js chrome", @@ -23,6 +23,7 @@ "build-smokehouse-bundle": "node ./build/build-smokehouse-bundle.js", "build-lr": "yarn reset-link && node ./build/build-lightrider-bundles.js", "build-pack": "bash build/build-pack.sh", + "build-report": "node build/build-report.js", "build-treemap": "node ./build/build-treemap.js", "build-viewer": "node ./build/build-viewer.js", "reset-link": "(yarn unlink || true) && yarn link && yarn link lighthouse", diff --git a/report/assets/standalone-template.html b/report/assets/standalone-template.html index 96c481103efe..e7cd816d40c0 100644 --- a/report/assets/standalone-template.html +++ b/report/assets/standalone-template.html @@ -31,50 +31,10 @@
+ - - diff --git a/report/clients/standalone.js b/report/clients/standalone.js new file mode 100644 index 000000000000..b45511233c9d --- /dev/null +++ b/report/clients/standalone.js @@ -0,0 +1,52 @@ +/** + * @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 ga DOM ReportRenderer ReportUIFeatures Logger */ + +(function __initLighthouseReport__() { + const dom = new DOM(document); + const renderer = new ReportRenderer(dom); + const container = dom.find('main', document); + /** @type {LH.ReportResult} */ + // @ts-expect-error + const lhr = window.__LIGHTHOUSE_JSON__; + renderer.renderReport(lhr, container); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + const features = new ReportUIFeatures(dom); + features.initFeatures(lhr); +})(); + +document.addEventListener('lh-analytics', /** @param {Event} e */ e => { + // @ts-expect-error + if (window.ga) ga(e.detail.cmd, e.detail.fields); +}); + +document.addEventListener('lh-log', /** @param {Event} e */ e => { + const el = document.querySelector('#lh-log'); + if (!el) return; + + const logger = new Logger(el); + // @ts-expect-error + const detail = e.detail; + + switch (detail.cmd) { + case 'log': + logger.log(detail.msg); + break; + case 'warn': + logger.warn(detail.msg); + break; + case 'error': + logger.error(detail.msg); + break; + case 'hide': + logger.hide(); + break; + } +}); diff --git a/report/report-assets.js b/report/report-assets.js index b92b031bf613..2b2dfb6a6ded 100644 --- a/report/report-assets.js +++ b/report/report-assets.js @@ -7,25 +7,8 @@ const fs = require('fs'); -const REPORT_TEMPLATE = - fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); -const REPORT_JAVASCRIPT = [ - fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/crc-details-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/snippet-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/element-screenshot-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/../lighthouse-core/lib/file-namer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-ui-features.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'), - fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'), -].join(';\n'); +const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); +const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/generated/standalone.js', 'utf8'); const REPORT_CSS = fs.readFileSync(__dirname + '/assets/styles.css', 'utf8'); const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/assets/templates.html', 'utf8'); From 53033c8fabb3e67c8af0ca3f0e1adeb6a05fa323 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:13:21 -0700 Subject: [PATCH 18/71] mkdir --- build/build-report.js | 1 + lighthouse-core/runner.js | 10 ---------- package.json | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 8d153ced0972..7fa8ab544bfc 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -26,6 +26,7 @@ async function buildStandaloneReport() { fs.readFileSync(__dirname + '/../report/renderer/text-encoding.js', 'utf8'), fs.readFileSync(__dirname + '/../report/clients/standalone.js', 'utf8'), ].join(';\n'); + fs.mkdirSync(__dirname + '/../report/generated', {recursive: true}); fs.writeFileSync(__dirname + '/../report/generated/standalone.js', REPORT_JAVASCRIPT); } diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index aced7a7d1f6d..82c35c57e92a 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -169,16 +169,6 @@ class Runner { assetSaver.saveLhr(lhr, path); } - // Build report if in local dev env so we don't have to run a watch command. - const forHtml = settings.output === 'html' || - (Array.isArray(settings.output) && settings.output.includes('html')); - if (forHtml && !global.isDevtools && !global.isLightrider && - fs.existsSync('dist') && fs.existsSync('.git')) { - // Prevent bundling. - const buildReportPath = '../build/build-report.js'; - await require(buildReportPath).buildStandaloneReport(); - } - // Create the HTML, JSON, and/or CSV string const report = generateReport(lhr, settings.output); diff --git a/package.json b/package.json index e273b75a43ea..5ec00d22648d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .", "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", - "start": "node ./lighthouse-cli/index.js", + "start": "yarn build-report && node ./lighthouse-cli/index.js", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "jest \"clients/\"", From 9b425a0704551417bf72b71d1e6b6f56a4d8aedb Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:18:07 -0700 Subject: [PATCH 19/71] build that shiz --- .github/workflows/smoke.yml | 1 + .github/workflows/unit.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f7bf9370768a..ceff3adaa289 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -46,6 +46,7 @@ jobs: run: bash $GITHUB_WORKSPACE/lighthouse-core/scripts/download-chrome.sh && mv chrome-linux chrome-linux-tot - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - run: sudo apt-get install xvfb - name: Run smoke tests diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 498b7aaf2b05..40e98609c770 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -44,6 +44,7 @@ jobs: pip install protobuf==3.7.1 - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - run: yarn test-proto # Run before unit-core because the roundtrip json is needed for proto tests. From 19e11bba6ea23587c0af92dcd76e905a6244b391 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:19:03 -0700 Subject: [PATCH 20/71] rm --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28bfd259e619..8a0b933e28c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,6 @@ jobs: - run: yarn test-legacy-javascript - run: yarn i18n:checks - run: yarn dogfood-lhci - - run: sh lighthouse-core/scripts/copy-util-commonjs.sh # Fail if any changes were written to any source files or generated untracked files (ex, from: build/build-cdt-lib.js). - run: git add -A && git diff --cached --exit-code From 844d5bf7dec79182128fbafc0bc1291ae71bcfa0 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:27:12 -0700 Subject: [PATCH 21/71] ohhhh windows --- .github/workflows/smoke.yml | 1 + .github/workflows/unit.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index ceff3adaa289..298323a68855 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -79,6 +79,7 @@ jobs: node-version: 12.x - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - name: Run smoke tests # Windows bots are slow, so only run enough tests to verify matching behavior. diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 40e98609c770..5d922d89f2ab 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -82,6 +82,7 @@ jobs: node-version: 12.x - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - name: yarn unit-cli run: yarn unit-cli From cafef7cbc09071bbf916d1d61045d75905604d9c Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 11:31:09 -0700 Subject: [PATCH 22/71] ugh --- .github/workflows/smoke.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 298323a68855..f237ea300888 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -102,6 +102,7 @@ jobs: node-version: 12.x - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - run: yarn build-devtools - run: sudo apt-get install xvfb From 9073d974cdc26705e97dfd3ebfc41404864d8d19 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 13:27:15 -0700 Subject: [PATCH 23/71] fix viewer --- build/build-report.js | 11 +++++++++-- build/build-viewer.js | 6 ++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 7fa8ab544bfc..80dad2d1bf99 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -7,8 +7,8 @@ const fs = require('fs'); -async function buildStandaloneReport() { - const REPORT_JAVASCRIPT = [ +function concatRendererCode() { + return [ fs.readFileSync(__dirname + '/../report/renderer/util.js', 'utf8'), fs.readFileSync(__dirname + '/../report/renderer/dom.js', 'utf8'), fs.readFileSync(__dirname + '/../report/renderer/details-renderer.js', 'utf8'), @@ -24,6 +24,12 @@ async function buildStandaloneReport() { fs.readFileSync(__dirname + '/../report/renderer/report-renderer.js', 'utf8'), fs.readFileSync(__dirname + '/../report/renderer/i18n.js', 'utf8'), fs.readFileSync(__dirname + '/../report/renderer/text-encoding.js', 'utf8'), + ].join(';\n'); +} + +async function buildStandaloneReport() { + const REPORT_JAVASCRIPT = [ + concatRendererCode(), fs.readFileSync(__dirname + '/../report/clients/standalone.js', 'utf8'), ].join(';\n'); fs.mkdirSync(__dirname + '/../report/generated', {recursive: true}); @@ -36,4 +42,5 @@ if (require.main === module) { module.exports = { buildStandaloneReport, + concatRendererCode, }; diff --git a/build/build-viewer.js b/build/build-viewer.js index 65aaa6565cbd..f9eefedec21a 100644 --- a/build/build-viewer.js +++ b/build/build-viewer.js @@ -9,7 +9,7 @@ const fs = require('fs'); const browserify = require('browserify'); const GhPagesApp = require('./gh-pages-app.js'); const {minifyFileTransform} = require('./build-utils.js'); -const {buildStandaloneReport} = require('./build-report.js'); +const {concatRendererCode} = require('./build-report.js'); const htmlReportAssets = require('../report/report-assets.js'); /** @@ -31,8 +31,6 @@ async function run() { }); }); - await buildStandaloneReport(); - const app = new GhPagesApp({ name: 'viewer', appDir: `${__dirname}/../lighthouse-viewer/app`, @@ -46,7 +44,7 @@ async function run() { ], javascripts: [ await generatorJsPromise, - htmlReportAssets.REPORT_JAVASCRIPT, + concatRendererCode(), fs.readFileSync(require.resolve('idb-keyval/dist/idb-keyval-min.js'), 'utf8'), {path: 'src/*'}, ], From f574fe24ac17e9bea347ddaf9288f31797b53c01 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 25 Jun 2021 13:43:14 -0700 Subject: [PATCH 24/71] fix collect strings --- lighthouse-core/scripts/i18n/collect-strings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index fdafa9c8240f..2a821558beb9 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -28,7 +28,7 @@ const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; const foldersWithStrings = [ `${LH_ROOT}/lighthouse-core`, - `${LH_ROOT}/report`, + `${LH_ROOT}/report/renderer`, `${LH_ROOT}/lighthouse-treemap`, path.dirname(require.resolve('lighthouse-stack-packs')) + '/packs', ]; From a66a62663885f2409aa8fa1224deb1f8230bab48 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Mon, 28 Jun 2021 19:37:55 -0700 Subject: [PATCH 25/71] update psi.js --- report/clients/package.json | 1 + report/clients/psi.js | 14 +++++++------- report/renderer/package.json | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 report/clients/package.json create mode 100644 report/renderer/package.json diff --git a/report/clients/package.json b/report/clients/package.json new file mode 100644 index 000000000000..1632c2c4df68 --- /dev/null +++ b/report/clients/package.json @@ -0,0 +1 @@ +{"type": "module"} \ No newline at end of file diff --git a/report/clients/psi.js b/report/clients/psi.js index 10cefd3b707c..e061c7ab26e0 100644 --- a/report/clients/psi.js +++ b/report/clients/psi.js @@ -16,13 +16,13 @@ */ 'use strict'; -import {DetailsRenderer} from './common/details-renderer.js'; -import {DOM} from './common/dom.js'; -import {ElementScreenshotRenderer} from './common/element-screenshot-renderer.js'; -import {I18n} from './common/i18n.js'; -import {PerformanceCategoryRenderer} from './common/performance-category-renderer.js'; -import {ReportUIFeatures} from './common/report-ui-features.js'; -import {Util} from './common/util.js'; +import {DetailsRenderer} from '../renderer/details-renderer.js'; +import {DOM} from '../renderer/dom.js'; +import {ElementScreenshotRenderer} from '../renderer/element-screenshot-renderer.js'; +import {I18n} from '../renderer/i18n.js'; +import {PerformanceCategoryRenderer} from '../renderer/performance-category-renderer.js'; +import {ReportUIFeatures} from '../renderer/report-ui-features.js'; +import {Util} from '../renderer/util.js'; /** * Returns all the elements that PSI needs to render the report diff --git a/report/renderer/package.json b/report/renderer/package.json new file mode 100644 index 000000000000..1632c2c4df68 --- /dev/null +++ b/report/renderer/package.json @@ -0,0 +1 @@ +{"type": "module"} \ No newline at end of file From 008c3b0cd292aa949773a6c8736163c41002f72f Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 15:52:59 -0700 Subject: [PATCH 26/71] rm autobuild --- lighthouse-core/runner.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index aced7a7d1f6d..82c35c57e92a 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -169,16 +169,6 @@ class Runner { assetSaver.saveLhr(lhr, path); } - // Build report if in local dev env so we don't have to run a watch command. - const forHtml = settings.output === 'html' || - (Array.isArray(settings.output) && settings.output.includes('html')); - if (forHtml && !global.isDevtools && !global.isLightrider && - fs.existsSync('dist') && fs.existsSync('.git')) { - // Prevent bundling. - const buildReportPath = '../build/build-report.js'; - await require(buildReportPath).buildStandaloneReport(); - } - // Create the HTML, JSON, and/or CSV string const report = generateReport(lhr, settings.output); From aee255b643eeb60015ac41c3f0fbd2b8b6e176fb Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 16:02:51 -0700 Subject: [PATCH 27/71] convert tests --- report/clients/viewer.js | 2 ++ .../test/renderer/category-renderer-test.js | 21 ++++++------- .../renderer/crc-details-renderer-test.js | 17 ++++++----- report/test/renderer/details-renderer-test.js | 20 ++++++------- report/test/renderer/dom-test.js | 12 ++++---- .../element-screenshot-renderer-test.js | 15 +++++----- report/test/renderer/i18n-test.js | 10 +++---- report/test/renderer/package.json | 1 + .../performance-category-renderer-test.js | 23 +++++++------- report/test/renderer/psi-test.js | 30 +++++++++---------- .../renderer/pwa-category-renderer-test.js | 19 ++++++------ report/test/renderer/report-renderer-test.js | 29 +++++++++--------- .../test/renderer/report-ui-features-test.js | 29 +++++++++--------- report/test/renderer/snippet-renderer-test.js | 15 +++++----- report/test/renderer/text-encoding-test.js | 2 +- report/test/renderer/util-test.js | 8 ++--- 16 files changed, 131 insertions(+), 122 deletions(-) create mode 100644 report/test/renderer/package.json diff --git a/report/clients/viewer.js b/report/clients/viewer.js index 2d763d3ea7f7..83fab3e08a10 100644 --- a/report/clients/viewer.js +++ b/report/clients/viewer.js @@ -5,6 +5,8 @@ */ 'use strict'; +/* global window */ + import {DOM} from '../renderer/dom.js'; import {Logger} from '../renderer/logger.js'; import {ReportRenderer} from '../renderer/report-renderer.js'; diff --git a/report/test/renderer/category-renderer-test.js b/report/test/renderer/category-renderer-test.js index 9b3fcd046d7f..7bf03644182d 100644 --- a/report/test/renderer/category-renderer-test.js +++ b/report/test/renderer/category-renderer-test.js @@ -7,16 +7,17 @@ /* eslint-env jest, browser */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const sampleResultsOrig = require('../../../lighthouse-core/test/results/sample_v2.json'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('CategoryRenderer', () => { let renderer; diff --git a/report/test/renderer/crc-details-renderer-test.js b/report/test/renderer/crc-details-renderer-test.js index 4b799cd485b5..988cbcfa7afd 100644 --- a/report/test/renderer/crc-details-renderer-test.js +++ b/report/test/renderer/crc-details-renderer-test.js @@ -7,14 +7,15 @@ /* eslint-env jest */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; const superLongURL = 'https://example.com/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichWeWantToTest.js'; diff --git a/report/test/renderer/details-renderer-test.js b/report/test/renderer/details-renderer-test.js index eb4da2fc2488..90f6c252d3de 100644 --- a/report/test/renderer/details-renderer-test.js +++ b/report/test/renderer/details-renderer-test.js @@ -5,16 +5,16 @@ */ 'use strict'; -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const DOM = require('../../renderer/dom.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const SnippetRenderer = require('../../renderer/snippet-renderer.js'); -const CrcDetailsRenderer = require('../../renderer/crc-details-renderer.js'); -const ElementScreenshotRenderer = require('../../renderer/element-screenshot-renderer.js'); +import { strict as assert } from 'assert'; +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import DOM from '../../renderer/dom.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import SnippetRenderer from '../../renderer/snippet-renderer.js'; +import CrcDetailsRenderer from '../../renderer/crc-details-renderer.js'; +import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; /* eslint-env jest */ diff --git a/report/test/renderer/dom-test.js b/report/test/renderer/dom-test.js index 3a80f54bd6b6..ec0f04ff0813 100644 --- a/report/test/renderer/dom-test.js +++ b/report/test/renderer/dom-test.js @@ -5,12 +5,12 @@ */ 'use strict'; -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const DOM = require('../../renderer/dom.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); +import { strict as assert } from 'assert'; +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import DOM from '../../renderer/dom.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; /* eslint-env jest */ diff --git a/report/test/renderer/element-screenshot-renderer-test.js b/report/test/renderer/element-screenshot-renderer-test.js index 7acb56ebc70a..0dc93c2c134e 100644 --- a/report/test/renderer/element-screenshot-renderer-test.js +++ b/report/test/renderer/element-screenshot-renderer-test.js @@ -7,13 +7,14 @@ /* eslint-env jest */ -const jsdom = require('jsdom'); -const ElementScreenshotRenderer = require('../../renderer/element-screenshot-renderer.js'); -const RectHelpers = require('../../../lighthouse-core/lib/rect-helpers.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const reportAssets = require('../../report-assets.js'); +import jsdom from 'jsdom'; + +import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import reportAssets from '../../report-assets.js'; /** * @param {{left: number, top: number, width: number, height:number}} opts diff --git a/report/test/renderer/i18n-test.js b/report/test/renderer/i18n-test.js index 49c973759dad..2d099582eea7 100644 --- a/report/test/renderer/i18n-test.js +++ b/report/test/renderer/i18n-test.js @@ -5,15 +5,15 @@ */ 'use strict'; -const assert = require('assert').strict; -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const {isNode12SmallIcu} = require('../../../lighthouse-core/test/test-utils.js'); +import { strict as assert } from 'assert'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import { isNode12SmallIcu } from '../../../lighthouse-core/test/test-utils.js'; // Require i18n to make sure Intl is polyfilled in Node without full-icu for testing. // When Util is run in a browser, Intl will be supplied natively (IE11+). // eslint-disable-next-line no-unused-vars -const i18n = require('../../../lighthouse-core/lib/i18n/i18n.js'); +import i18n from '../../../lighthouse-core/lib/i18n/i18n.js'; const NBSP = '\xa0'; diff --git a/report/test/renderer/package.json b/report/test/renderer/package.json new file mode 100644 index 000000000000..1632c2c4df68 --- /dev/null +++ b/report/test/renderer/package.json @@ -0,0 +1 @@ +{"type": "module"} \ No newline at end of file diff --git a/report/test/renderer/performance-category-renderer-test.js b/report/test/renderer/performance-category-renderer-test.js index f6c4ae96d843..d2f796c4b46f 100644 --- a/report/test/renderer/performance-category-renderer-test.js +++ b/report/test/renderer/performance-category-renderer-test.js @@ -7,17 +7,18 @@ /* eslint-env jest, browser */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const URL = require('../../../lighthouse-core/lib/url-shim.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const sampleResultsOrig = require('../../../lighthouse-core/test/results/sample_v2.json'); -const reportAssets = require('../../report-assets.js'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import URL from '../../../lighthouse-core/lib/url-shim.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; +import reportAssets from '../../report-assets.js'; describe('PerfCategoryRenderer', () => { let category; diff --git a/report/test/renderer/psi-test.js b/report/test/renderer/psi-test.js index 64c7f0f0d3d2..d44455d07a6d 100644 --- a/report/test/renderer/psi-test.js +++ b/report/test/renderer/psi-test.js @@ -5,22 +5,20 @@ */ 'use strict'; -const assert = require('assert').strict; -const fs = require('fs'); - -const jsdom = require('jsdom'); - -const testUtils = require('../../../lighthouse-core/test/test-utils.js'); -const reportAssets = require('../../report-assets.js'); -const prepareLabData = require('../../renderer/psi.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); -const ElementScreenshotRenderer = require('../../renderer/element-screenshot-renderer.js'); -const ReportUIFeatures = require('../../renderer/report-ui-features.js'); +import { strict as assert } from 'assert'; +import fs from 'fs'; +import jsdom from 'jsdom'; +import testUtils from '../../../lighthouse-core/test/test-utils.js'; +import reportAssets from '../../report-assets.js'; +import prepareLabData from '../../clients/psi.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import ReportUIFeatures from '../../renderer/report-ui-features.js'; const {itIfProtoExists, sampleResultsRoundtripStr} = testUtils.getProtoRoundTrip(); const sampleResultsStr = diff --git a/report/test/renderer/pwa-category-renderer-test.js b/report/test/renderer/pwa-category-renderer-test.js index 863bef876deb..a462f3fea661 100644 --- a/report/test/renderer/pwa-category-renderer-test.js +++ b/report/test/renderer/pwa-category-renderer-test.js @@ -7,15 +7,16 @@ /* eslint-env jest, browser */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const sampleResultsOrig = require('../../../lighthouse-core/test/results/sample_v2.json'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('PwaCategoryRenderer', () => { let category; diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 396ac595ae8c..27841aa11bc7 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -7,20 +7,21 @@ /* eslint-env jest */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const URL = require('../../../lighthouse-core/lib/url-shim.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const ReportUIFeatures = require('../../renderer/report-ui-features.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const ElementScreenshotRenderer = require('../../renderer/element-screenshot-renderer.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); -const ReportRenderer = require('../../renderer/report-renderer.js'); -const sampleResultsOrig = require('../../../lighthouse-core/test/results/sample_v2.json'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import URL from '../../../lighthouse-core/lib/url-shim.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import ReportUIFeatures from '../../renderer/report-ui-features.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import ReportRenderer from '../../renderer/report-renderer.js'; +import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; const TIMESTAMP_REGEX = /\d+, \d{4}.*\d+:\d+/; diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 8db7fb90b8a7..11b754f834e3 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -7,20 +7,21 @@ /* eslint-env jest */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const DetailsRenderer = require('../../renderer/details-renderer.js'); -const ReportUIFeatures = require('../../renderer/report-ui-features.js'); -const CategoryRenderer = require('../../renderer/category-renderer.js'); -const ElementScreenshotRenderer = require('../../renderer/element-screenshot-renderer.js'); -const RectHelpers = require('../../../lighthouse-core/lib/rect-helpers.js'); -const CriticalRequestChainRenderer = require('../../renderer/crc-details-renderer.js'); -const ReportRenderer = require('../../renderer/report-renderer.js'); -const sampleResultsOrig = require('../../../lighthouse-core/test/results/sample_v2.json'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import DetailsRenderer from '../../renderer/details-renderer.js'; +import ReportUIFeatures from '../../renderer/report-ui-features.js'; +import CategoryRenderer from '../../renderer/category-renderer.js'; +import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; +import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import ReportRenderer from '../../renderer/report-renderer.js'; +import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('ReportUIFeatures', () => { let sampleResults; diff --git a/report/test/renderer/snippet-renderer-test.js b/report/test/renderer/snippet-renderer-test.js index fe58c811e39a..155ed390974a 100644 --- a/report/test/renderer/snippet-renderer-test.js +++ b/report/test/renderer/snippet-renderer-test.js @@ -7,13 +7,14 @@ /* eslint-env jest */ -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const reportAssets = require('../../report-assets.js'); -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const DOM = require('../../renderer/dom.js'); -const SnippetRenderer = require('../../renderer/snippet-renderer.js'); +import { strict as assert } from 'assert'; + +import jsdom from 'jsdom'; +import reportAssets from '../../report-assets.js'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import DOM from '../../renderer/dom.js'; +import SnippetRenderer from '../../renderer/snippet-renderer.js'; /* Generates a snippet lines array like this (for a single range from 1 to 4): [ diff --git a/report/test/renderer/text-encoding-test.js b/report/test/renderer/text-encoding-test.js index 8ba46f84f0a1..097f0298ac5a 100644 --- a/report/test/renderer/text-encoding-test.js +++ b/report/test/renderer/text-encoding-test.js @@ -5,7 +5,7 @@ */ 'use strict'; -const TextEncoding = require('../../renderer/text-encoding.js'); +import TextEncoding from '../../renderer/text-encoding.js'; /* eslint-env jest */ diff --git a/report/test/renderer/util-test.js b/report/test/renderer/util-test.js index a8bf9f95fb8f..3165b9b04db0 100644 --- a/report/test/renderer/util-test.js +++ b/report/test/renderer/util-test.js @@ -5,10 +5,10 @@ */ 'use strict'; -const assert = require('assert').strict; -const Util = require('../../renderer/util.js'); -const I18n = require('../../renderer/i18n.js'); -const sampleResult = require('../../../lighthouse-core/test/results/sample_v2.json'); +import { strict as assert } from 'assert'; +import Util from '../../renderer/util.js'; +import I18n from '../../renderer/i18n.js'; +import sampleResult from '../../../lighthouse-core/test/results/sample_v2.json'; /* eslint-env jest */ From 489e1a8f824dd1471e5c4dbcbb2ecdaeb254e8ce Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 16:58:18 -0700 Subject: [PATCH 28/71] convert report tests --- lighthouse-core/test/test-utils.js | 11 ++++++ report/clients/standalone.js | 6 ++++ report/renderer/report-renderer.js | 7 +--- report/report-generator.js | 3 +- report/test/clients/package.json | 1 + report/test/{renderer => clients}/psi-test.js | 30 ++++++++-------- .../test/renderer/category-renderer-test.js | 14 ++++---- .../renderer/crc-details-renderer-test.js | 12 +++---- report/test/renderer/details-renderer-test.js | 18 +++++----- report/test/renderer/dom-test.js | 8 ++--- .../element-screenshot-renderer-test.js | 8 ++--- report/test/renderer/i18n-test.js | 8 ++--- .../performance-category-renderer-test.js | 18 +++++----- .../renderer/pwa-category-renderer-test.js | 16 ++++----- report/test/renderer/report-renderer-test.js | 35 +++++++++---------- .../test/renderer/report-ui-features-test.js | 30 ++++++++-------- report/test/renderer/snippet-renderer-test.js | 10 +++--- report/test/renderer/text-encoding-test.js | 11 ++++-- report/test/renderer/util-test.js | 6 ++-- report/test/report-generator-test.js | 4 +-- 20 files changed, 132 insertions(+), 124 deletions(-) create mode 100644 report/test/clients/package.json rename report/test/{renderer => clients}/psi-test.js (87%) diff --git a/lighthouse-core/test/test-utils.js b/lighthouse-core/test/test-utils.js index 7246eeaef725..b7f9946c5393 100644 --- a/lighthouse-core/test/test-utils.js +++ b/lighthouse-core/test/test-utils.js @@ -8,6 +8,8 @@ /* eslint-env jest */ const fs = require('fs'); +const url = require('url'); +const path = require('path'); const i18n = require('../lib/i18n/i18n.js'); const mockCommands = require('./gather/mock-commands.js'); const {default: {toBeCloseTo}} = require('expect/build/matchers.js'); @@ -283,6 +285,14 @@ function isNode12SmallIcu() { Intl.NumberFormat.supportedLocalesOf('es').length === 0; } +/** + * @param {ImportMeta} importMeta + * @return {string} + */ +function esmGetDirname(importMeta) { + return path.dirname(url.fileURLToPath(importMeta.url)); +} + module.exports = { getProtoRoundTrip, loadSourceMapFixture, @@ -293,5 +303,6 @@ module.exports = { flushAllTimersAndMicrotasks, makeMocksForGatherRunner, isNode12SmallIcu, + esmGetDirname, ...mockCommands, }; diff --git a/report/clients/standalone.js b/report/clients/standalone.js index 8235219a179f..42874b69ecf7 100644 --- a/report/clients/standalone.js +++ b/report/clients/standalone.js @@ -5,6 +5,12 @@ */ 'use strict'; +/** + * @fileoverview The entry point for rendering the Lighthouse report for the HTML + * file created by ReportGenerator. + * The renderer code is bundled and injected into the report HTML along with the JSON report. + */ + /* global document window ga */ import {DOM} from '../renderer/dom.js'; diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 653ad2e7645d..7ad656be4b34 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -13,15 +13,10 @@ * 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 The entry point for rendering the Lighthouse report based on the JSON output. - * This file is injected into the report HTML along with the JSON report. * * Dummy text for ensuring report robustness: pre$`post %%LIGHTHOUSE_JSON%% */ +'use strict'; /** @typedef {import('./dom.js').DOM} DOM */ diff --git a/report/report-generator.js b/report/report-generator.js index 01a456c0e169..a0f2fbecb535 100644 --- a/report/report-generator.js +++ b/report/report-generator.js @@ -37,11 +37,10 @@ class ReportGenerator { .replace(/ { beforeAll(() => { global.Util = Util; global.I18n = I18n; - global.DOM = DOM; global.CategoryRenderer = CategoryRenderer; global.DetailsRenderer = DetailsRenderer; - - // Delayed so that CategoryRenderer is in global scope - const PerformanceCategoryRenderer = - require('../../renderer/performance-category-renderer.js'); global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; global.ElementScreenshotRenderer = ElementScreenshotRenderer; diff --git a/report/test/renderer/category-renderer-test.js b/report/test/renderer/category-renderer-test.js index 7bf03644182d..31529c9a8fcf 100644 --- a/report/test/renderer/category-renderer-test.js +++ b/report/test/renderer/category-renderer-test.js @@ -7,16 +7,16 @@ /* eslint-env jest, browser */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; -import CategoryRenderer from '../../renderer/category-renderer.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; +import {CategoryRenderer} from '../../renderer/category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('CategoryRenderer', () => { diff --git a/report/test/renderer/crc-details-renderer-test.js b/report/test/renderer/crc-details-renderer-test.js index 988cbcfa7afd..6cea70a1bdb2 100644 --- a/report/test/renderer/crc-details-renderer-test.js +++ b/report/test/renderer/crc-details-renderer-test.js @@ -7,15 +7,15 @@ /* eslint-env jest */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; const superLongURL = 'https://example.com/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichWeWantToTest.js'; diff --git a/report/test/renderer/details-renderer-test.js b/report/test/renderer/details-renderer-test.js index 90f6c252d3de..15208a0237b2 100644 --- a/report/test/renderer/details-renderer-test.js +++ b/report/test/renderer/details-renderer-test.js @@ -5,16 +5,16 @@ */ 'use strict'; -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import DOM from '../../renderer/dom.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import SnippetRenderer from '../../renderer/snippet-renderer.js'; -import CrcDetailsRenderer from '../../renderer/crc-details-renderer.js'; -import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import {DOM} from '../../renderer/dom.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {SnippetRenderer} from '../../renderer/snippet-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; +import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; /* eslint-env jest */ @@ -31,7 +31,7 @@ describe('DetailsRenderer', () => { beforeAll(() => { global.Util = Util; global.Util.i18n = new I18n('en', {...Util.UIStrings}); - global.CriticalRequestChainRenderer = CrcDetailsRenderer; + global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; global.SnippetRenderer = SnippetRenderer; global.ElementScreenshotRenderer = ElementScreenshotRenderer; createRenderer(); diff --git a/report/test/renderer/dom-test.js b/report/test/renderer/dom-test.js index ec0f04ff0813..3b107d572adb 100644 --- a/report/test/renderer/dom-test.js +++ b/report/test/renderer/dom-test.js @@ -5,12 +5,12 @@ */ 'use strict'; -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import DOM from '../../renderer/dom.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; /* eslint-env jest */ diff --git a/report/test/renderer/element-screenshot-renderer-test.js b/report/test/renderer/element-screenshot-renderer-test.js index 0dc93c2c134e..c6855ad42363 100644 --- a/report/test/renderer/element-screenshot-renderer-test.js +++ b/report/test/renderer/element-screenshot-renderer-test.js @@ -9,11 +9,11 @@ import jsdom from 'jsdom'; -import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; import reportAssets from '../../report-assets.js'; /** diff --git a/report/test/renderer/i18n-test.js b/report/test/renderer/i18n-test.js index 2d099582eea7..1c5d1f296f27 100644 --- a/report/test/renderer/i18n-test.js +++ b/report/test/renderer/i18n-test.js @@ -5,10 +5,10 @@ */ 'use strict'; -import { strict as assert } from 'assert'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import { isNode12SmallIcu } from '../../../lighthouse-core/test/test-utils.js'; +import {strict as assert} from 'assert'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {isNode12SmallIcu} from '../../../lighthouse-core/test/test-utils.js'; // Require i18n to make sure Intl is polyfilled in Node without full-icu for testing. // When Util is run in a browser, Intl will be supplied natively (IE11+). diff --git a/report/test/renderer/performance-category-renderer-test.js b/report/test/renderer/performance-category-renderer-test.js index d2f796c4b46f..2732614ee0e3 100644 --- a/report/test/renderer/performance-category-renderer-test.js +++ b/report/test/renderer/performance-category-renderer-test.js @@ -7,16 +7,17 @@ /* eslint-env jest, browser */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; import URL from '../../../lighthouse-core/lib/url-shim.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; -import CategoryRenderer from '../../renderer/category-renderer.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; +import {CategoryRenderer} from '../../renderer/category-renderer.js'; +import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; import reportAssets from '../../report-assets.js'; @@ -31,9 +32,6 @@ describe('PerfCategoryRenderer', () => { global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; global.CategoryRenderer = CategoryRenderer; - // Delayed so that CategoryRenderer is in global scope - const PerformanceCategoryRenderer = require('../../renderer/performance-category-renderer.js'); - const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; const dom = new DOM(document); const detailsRenderer = new DetailsRenderer(dom); diff --git a/report/test/renderer/pwa-category-renderer-test.js b/report/test/renderer/pwa-category-renderer-test.js index a462f3fea661..15280626d7fd 100644 --- a/report/test/renderer/pwa-category-renderer-test.js +++ b/report/test/renderer/pwa-category-renderer-test.js @@ -7,15 +7,16 @@ /* eslint-env jest, browser */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import CategoryRenderer from '../../renderer/category-renderer.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {CategoryRenderer} from '../../renderer/category-renderer.js'; +import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('PwaCategoryRenderer', () => { @@ -28,9 +29,6 @@ describe('PwaCategoryRenderer', () => { global.Util.i18n = new I18n('en', {...Util.UIStrings}); global.CategoryRenderer = CategoryRenderer; - // Delayed so that CategoryRenderer is in global scope - const PwaCategoryRenderer = require('../../renderer/pwa-category-renderer.js'); - const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; const dom = new DOM(document); const detailsRenderer = new DetailsRenderer(dom); diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 27841aa11bc7..1f4aab10bfdc 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -7,20 +7,22 @@ /* eslint-env jest */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; import URL from '../../../lighthouse-core/lib/url-shim.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import ReportUIFeatures from '../../renderer/report-ui-features.js'; -import CategoryRenderer from '../../renderer/category-renderer.js'; -import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; -import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; -import ReportRenderer from '../../renderer/report-renderer.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {ReportUIFeatures} from '../../renderer/report-ui-features.js'; +import {CategoryRenderer} from '../../renderer/category-renderer.js'; +import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; +import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; +import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; +import {ReportRenderer} from '../../renderer/report-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; const TIMESTAMP_REGEX = /\d+, \d{4}.*\d+:\d+/; @@ -37,11 +39,8 @@ describe('ReportRenderer', () => { global.DetailsRenderer = DetailsRenderer; global.CategoryRenderer = CategoryRenderer; global.ElementScreenshotRenderer = ElementScreenshotRenderer; - - // lazy loaded because they depend on CategoryRenderer to be available globally - global.PerformanceCategoryRenderer = - require('../../renderer/performance-category-renderer.js'); - global.PwaCategoryRenderer = require('../../renderer/pwa-category-renderer.js'); + global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; + global.PwaCategoryRenderer = PwaCategoryRenderer; // Stub out matchMedia for Node. global.matchMedia = function() { @@ -272,15 +271,15 @@ describe('ReportRenderer', () => { describe('axe-core', () => { let axe; - beforeAll(() =>{ + beforeAll(async () =>{ // Needed by axe-core // https://github.com/dequelabs/axe-core/blob/581c441c/doc/examples/jsdom/test/a11y.js#L24 global.window = global.self; global.Node = global.self.Node; global.Element = global.self.Element; - // axe-core must be required after the global polyfills - axe = require('axe-core'); + // axe-core must be imported after the global polyfills + axe = (await import('axe-core')).default; }); afterAll(() => { diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index 11b754f834e3..abd80212bf78 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -7,20 +7,22 @@ /* eslint-env jest */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; -import DetailsRenderer from '../../renderer/details-renderer.js'; -import ReportUIFeatures from '../../renderer/report-ui-features.js'; -import CategoryRenderer from '../../renderer/category-renderer.js'; -import ElementScreenshotRenderer from '../../renderer/element-screenshot-renderer.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {DetailsRenderer} from '../../renderer/details-renderer.js'; +import {ReportUIFeatures} from '../../renderer/report-ui-features.js'; +import {CategoryRenderer} from '../../renderer/category-renderer.js'; +import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; +import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; +import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; -import CriticalRequestChainRenderer from '../../renderer/crc-details-renderer.js'; -import ReportRenderer from '../../renderer/report-renderer.js'; +import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; +import {ReportRenderer} from '../../renderer/report-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; describe('ReportUIFeatures', () => { @@ -51,12 +53,8 @@ describe('ReportUIFeatures', () => { global.CategoryRenderer = CategoryRenderer; global.ElementScreenshotRenderer = ElementScreenshotRenderer; global.RectHelpers = RectHelpers; - - // lazy loaded because they depend on CategoryRenderer to be available globally - global.PerformanceCategoryRenderer = - require('../../renderer/performance-category-renderer.js'); - global.PwaCategoryRenderer = - require('../../renderer/pwa-category-renderer.js'); + global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; + global.PwaCategoryRenderer = PwaCategoryRenderer; // Stub out matchMedia for Node. global.matchMedia = function() { diff --git a/report/test/renderer/snippet-renderer-test.js b/report/test/renderer/snippet-renderer-test.js index 155ed390974a..b8ac062c7122 100644 --- a/report/test/renderer/snippet-renderer-test.js +++ b/report/test/renderer/snippet-renderer-test.js @@ -7,14 +7,14 @@ /* eslint-env jest */ -import { strict as assert } from 'assert'; +import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; -import DOM from '../../renderer/dom.js'; -import SnippetRenderer from '../../renderer/snippet-renderer.js'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; +import {DOM} from '../../renderer/dom.js'; +import {SnippetRenderer} from '../../renderer/snippet-renderer.js'; /* Generates a snippet lines array like this (for a single range from 1 to 4): [ diff --git a/report/test/renderer/text-encoding-test.js b/report/test/renderer/text-encoding-test.js index 097f0298ac5a..6ea70be5fdc0 100644 --- a/report/test/renderer/text-encoding-test.js +++ b/report/test/renderer/text-encoding-test.js @@ -5,13 +5,18 @@ */ 'use strict'; -import TextEncoding from '../../renderer/text-encoding.js'; +import fs from 'fs'; +import pako from 'pako'; +import {TextEncoding} from '../../renderer/text-encoding.js'; +import testUtils from '../../../lighthouse-core/test/test-utils.js'; + +const dir = testUtils.esmGetDirname(import.meta); /* eslint-env jest */ describe('TextEncoding', () => { beforeAll(() => { - global.window = {pako: require('pako')}; + global.window = {pako}; }); afterAll(() => { @@ -36,6 +41,6 @@ describe('TextEncoding', () => { await test('Some examples of emoji are 😃, 🧘🏻‍♂️, 🌍, 🍞, 🚗, 📞, 🎉, ♥️, 🍆, and 🏁.'); await test('.'.repeat(125183)); await test('😃'.repeat(125183)); - await test(JSON.stringify(require('../../../lighthouse-treemap/app/debug.json'))); + await test(fs.readFileSync(dir + '/../../../lighthouse-treemap/app/debug.json', 'utf-8')); }); }); diff --git a/report/test/renderer/util-test.js b/report/test/renderer/util-test.js index 3165b9b04db0..99d1df033bee 100644 --- a/report/test/renderer/util-test.js +++ b/report/test/renderer/util-test.js @@ -5,9 +5,9 @@ */ 'use strict'; -import { strict as assert } from 'assert'; -import Util from '../../renderer/util.js'; -import I18n from '../../renderer/i18n.js'; +import {strict as assert} from 'assert'; +import {Util} from '../../renderer/util.js'; +import {I18n} from '../../renderer/i18n.js'; import sampleResult from '../../../lighthouse-core/test/results/sample_v2.json'; /* eslint-env jest */ diff --git a/report/test/report-generator-test.js b/report/test/report-generator-test.js index 74027a852992..cd089757f144 100644 --- a/report/test/report-generator-test.js +++ b/report/test/report-generator-test.js @@ -70,8 +70,8 @@ describe('ReportGenerator', () => { it('should inject the report renderer javascript', () => { const result = ReportGenerator.generateReportHtml({}); - assert.ok(result.includes('ReportRenderer'), 'injects the script'); - assert.ok(result.includes('robustness: \\u003c/script'), 'escapes HTML tags in javascript'); + 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'); assert.ok(result.includes('LIGHTHOUSE_JSON'), 'cannot be tricked'); }); From cf5480a3e4d01bf5a8937d8e2860c54f05bda44f Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 17:08:10 -0700 Subject: [PATCH 29/71] remove global declaring stuff --- .../test/renderer/category-renderer-test.js | 9 ++---- .../renderer/crc-details-renderer-test.js | 7 ++--- report/test/renderer/details-renderer-test.js | 15 ++-------- report/test/renderer/dom-test.js | 7 ++--- .../element-screenshot-renderer-test.js | 10 ++----- report/test/renderer/i18n-test.js | 2 +- .../performance-category-renderer-test.js | 12 ++------ .../renderer/pwa-category-renderer-test.js | 9 ++---- report/test/renderer/report-renderer-test.js | 25 ---------------- .../test/renderer/report-ui-features-test.js | 30 ------------------- report/test/renderer/snippet-renderer-test.js | 6 ++-- 11 files changed, 20 insertions(+), 112 deletions(-) diff --git a/report/test/renderer/category-renderer-test.js b/report/test/renderer/category-renderer-test.js index 31529c9a8fcf..4bcf57d3438a 100644 --- a/report/test/renderer/category-renderer-test.js +++ b/report/test/renderer/category-renderer-test.js @@ -15,7 +15,6 @@ import {Util} from '../../renderer/util.js'; import {I18n} from '../../renderer/i18n.js'; import {DOM} from '../../renderer/dom.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; -import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; import {CategoryRenderer} from '../../renderer/category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; @@ -24,9 +23,7 @@ describe('CategoryRenderer', () => { let sampleResults; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); - global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; + Util.i18n = new I18n('en', {...Util.UIStrings}); const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; const dom = new DOM(document); @@ -37,9 +34,7 @@ describe('CategoryRenderer', () => { }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; - global.CriticalRequestChainRenderer = undefined; + Util.i18n = undefined; }); it('renders an audit', () => { diff --git a/report/test/renderer/crc-details-renderer-test.js b/report/test/renderer/crc-details-renderer-test.js index 6cea70a1bdb2..3de20537bf13 100644 --- a/report/test/renderer/crc-details-renderer-test.js +++ b/report/test/renderer/crc-details-renderer-test.js @@ -76,16 +76,15 @@ describe('DetailsRenderer', () => { let detailsRenderer; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); + Util.i18n = new I18n('en', {...Util.UIStrings}); + const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; dom = new DOM(document); detailsRenderer = new DetailsRenderer(dom); }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; + Util.i18n = undefined; }); it('renders tree structure', () => { diff --git a/report/test/renderer/details-renderer-test.js b/report/test/renderer/details-renderer-test.js index 15208a0237b2..ac634130eb78 100644 --- a/report/test/renderer/details-renderer-test.js +++ b/report/test/renderer/details-renderer-test.js @@ -12,9 +12,6 @@ import {DOM} from '../../renderer/dom.js'; import {Util} from '../../renderer/util.js'; import {I18n} from '../../renderer/i18n.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; -import {SnippetRenderer} from '../../renderer/snippet-renderer.js'; -import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; -import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; /* eslint-env jest */ @@ -29,20 +26,12 @@ describe('DetailsRenderer', () => { } beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); - global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; - global.SnippetRenderer = SnippetRenderer; - global.ElementScreenshotRenderer = ElementScreenshotRenderer; + Util.i18n = new I18n('en', {...Util.UIStrings}); createRenderer(); }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; - global.CriticalRequestChainRenderer = undefined; - global.SnippetRenderer = undefined; - global.ElementScreenshotRenderer = undefined; + Util.i18n = undefined; }); describe('render', () => { diff --git a/report/test/renderer/dom-test.js b/report/test/renderer/dom-test.js index 3b107d572adb..80aa5d7e3619 100644 --- a/report/test/renderer/dom-test.js +++ b/report/test/renderer/dom-test.js @@ -18,16 +18,15 @@ describe('DOM', () => { let dom; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); + Util.i18n = new I18n('en', {...Util.UIStrings}); + const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; dom = new DOM(document); dom.setLighthouseChannel('someChannel'); }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; + Util.i18n = undefined; }); describe('createElement', () => { diff --git a/report/test/renderer/element-screenshot-renderer-test.js b/report/test/renderer/element-screenshot-renderer-test.js index c6855ad42363..010728728b68 100644 --- a/report/test/renderer/element-screenshot-renderer-test.js +++ b/report/test/renderer/element-screenshot-renderer-test.js @@ -10,7 +10,6 @@ import jsdom from 'jsdom'; import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; -import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; import {Util} from '../../renderer/util.js'; import {I18n} from '../../renderer/i18n.js'; import {DOM} from '../../renderer/dom.js'; @@ -32,17 +31,14 @@ describe('ElementScreenshotRenderer', () => { let dom; beforeAll(() => { - global.RectHelpers = RectHelpers; - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); + Util.i18n = new I18n('en', {...Util.UIStrings}); + const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; dom = new DOM(document); }); afterAll(() => { - global.RectHelpers = undefined; - global.Util.i18n = undefined; - global.Util = undefined; + Util.i18n = undefined; }); it('renders screenshot', () => { diff --git a/report/test/renderer/i18n-test.js b/report/test/renderer/i18n-test.js index 1c5d1f296f27..877a90849049 100644 --- a/report/test/renderer/i18n-test.js +++ b/report/test/renderer/i18n-test.js @@ -13,7 +13,7 @@ import {isNode12SmallIcu} from '../../../lighthouse-core/test/test-utils.js'; // Require i18n to make sure Intl is polyfilled in Node without full-icu for testing. // When Util is run in a browser, Intl will be supplied natively (IE11+). // eslint-disable-next-line no-unused-vars -import i18n from '../../../lighthouse-core/lib/i18n/i18n.js'; +import '../../../lighthouse-core/lib/i18n/i18n.js'; const NBSP = '\xa0'; diff --git a/report/test/renderer/performance-category-renderer-test.js b/report/test/renderer/performance-category-renderer-test.js index 2732614ee0e3..abb1f31ff3bf 100644 --- a/report/test/renderer/performance-category-renderer-test.js +++ b/report/test/renderer/performance-category-renderer-test.js @@ -15,8 +15,6 @@ import {I18n} from '../../renderer/i18n.js'; import URL from '../../../lighthouse-core/lib/url-shim.js'; import {DOM} from '../../renderer/dom.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; -import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; -import {CategoryRenderer} from '../../renderer/category-renderer.js'; import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; import reportAssets from '../../report-assets.js'; @@ -27,10 +25,7 @@ describe('PerfCategoryRenderer', () => { let sampleResults; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); - global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; - global.CategoryRenderer = CategoryRenderer; + Util.i18n = new I18n('en', {...Util.UIStrings}); const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; const dom = new DOM(document); @@ -43,10 +38,7 @@ describe('PerfCategoryRenderer', () => { }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; - global.CriticalRequestChainRenderer = undefined; - global.CategoryRenderer = undefined; + Util.i18n = undefined; }); it('renders the category header', () => { diff --git a/report/test/renderer/pwa-category-renderer-test.js b/report/test/renderer/pwa-category-renderer-test.js index 15280626d7fd..e99984ebb7d2 100644 --- a/report/test/renderer/pwa-category-renderer-test.js +++ b/report/test/renderer/pwa-category-renderer-test.js @@ -15,7 +15,6 @@ import {Util} from '../../renderer/util.js'; import {I18n} from '../../renderer/i18n.js'; import {DOM} from '../../renderer/dom.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; -import {CategoryRenderer} from '../../renderer/category-renderer.js'; import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; @@ -25,9 +24,7 @@ describe('PwaCategoryRenderer', () => { let sampleResults; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); - global.CategoryRenderer = CategoryRenderer; + Util.i18n = new I18n('en', {...Util.UIStrings}); const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; const dom = new DOM(document); @@ -44,9 +41,7 @@ describe('PwaCategoryRenderer', () => { }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; - global.CategoryRenderer = undefined; + Util.i18n = undefined; }); it('renders the regular audits', () => { diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 1f4aab10bfdc..e40a870ad87a 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -12,16 +12,10 @@ import {strict as assert} from 'assert'; import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; import {Util} from '../../renderer/util.js'; -import {I18n} from '../../renderer/i18n.js'; import URL from '../../../lighthouse-core/lib/url-shim.js'; import {DOM} from '../../renderer/dom.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; -import {ReportUIFeatures} from '../../renderer/report-ui-features.js'; import {CategoryRenderer} from '../../renderer/category-renderer.js'; -import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; -import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; -import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; -import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; import {ReportRenderer} from '../../renderer/report-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; @@ -32,16 +26,6 @@ describe('ReportRenderer', () => { let sampleResults; beforeAll(() => { - global.Util = Util; - global.I18n = I18n; - global.ReportUIFeatures = ReportUIFeatures; - global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; - global.DetailsRenderer = DetailsRenderer; - global.CategoryRenderer = CategoryRenderer; - global.ElementScreenshotRenderer = ElementScreenshotRenderer; - global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; - global.PwaCategoryRenderer = PwaCategoryRenderer; - // Stub out matchMedia for Node. global.matchMedia = function() { return { @@ -61,16 +45,7 @@ describe('ReportRenderer', () => { afterAll(() => { global.self = undefined; - global.Util = undefined; - global.I18n = undefined; - global.ReportUIFeatures = undefined; global.matchMedia = undefined; - global.CriticalRequestChainRenderer = undefined; - global.DetailsRenderer = undefined; - global.CategoryRenderer = undefined; - global.ElementScreenshotRenderer = undefined; - global.PerformanceCategoryRenderer = undefined; - global.PwaCategoryRenderer = undefined; }); describe('renderReport', () => { diff --git a/report/test/renderer/report-ui-features-test.js b/report/test/renderer/report-ui-features-test.js index abd80212bf78..1d2947ab7e1a 100644 --- a/report/test/renderer/report-ui-features-test.js +++ b/report/test/renderer/report-ui-features-test.js @@ -8,20 +8,13 @@ /* eslint-env jest */ import {strict as assert} from 'assert'; - import jsdom from 'jsdom'; import reportAssets from '../../report-assets.js'; import {Util} from '../../renderer/util.js'; -import {I18n} from '../../renderer/i18n.js'; import {DOM} from '../../renderer/dom.js'; import {DetailsRenderer} from '../../renderer/details-renderer.js'; import {ReportUIFeatures} from '../../renderer/report-ui-features.js'; import {CategoryRenderer} from '../../renderer/category-renderer.js'; -import {PerformanceCategoryRenderer} from '../../renderer/performance-category-renderer.js'; -import {PwaCategoryRenderer} from '../../renderer/pwa-category-renderer.js'; -import {ElementScreenshotRenderer} from '../../renderer/element-screenshot-renderer.js'; -import RectHelpers from '../../../lighthouse-core/lib/rect-helpers.js'; -import {CriticalRequestChainRenderer} from '../../renderer/crc-details-renderer.js'; import {ReportRenderer} from '../../renderer/report-renderer.js'; import sampleResultsOrig from '../../../lighthouse-core/test/results/sample_v2.json'; @@ -45,17 +38,6 @@ describe('ReportUIFeatures', () => { } beforeAll(() => { - global.Util = Util; - global.I18n = I18n; - global.ReportUIFeatures = ReportUIFeatures; - global.CriticalRequestChainRenderer = CriticalRequestChainRenderer; - global.DetailsRenderer = DetailsRenderer; - global.CategoryRenderer = CategoryRenderer; - global.ElementScreenshotRenderer = ElementScreenshotRenderer; - global.RectHelpers = RectHelpers; - global.PerformanceCategoryRenderer = PerformanceCategoryRenderer; - global.PwaCategoryRenderer = PwaCategoryRenderer; - // Stub out matchMedia for Node. global.matchMedia = function() { return { @@ -90,18 +72,6 @@ describe('ReportUIFeatures', () => { }); afterAll(() => { - global.self = undefined; - global.Util = undefined; - global.I18n = undefined; - global.ReportUIFeatures = undefined; - global.matchMedia = undefined; - global.CriticalRequestChainRenderer = undefined; - global.DetailsRenderer = undefined; - global.CategoryRenderer = undefined; - global.ElementScreenshotRenderer = undefined; - global.RectHelpers = undefined; - global.PerformanceCategoryRenderer = undefined; - global.PwaCategoryRenderer = undefined; global.window = undefined; global.HTMLElement = undefined; global.HTMLInputElement = undefined; diff --git a/report/test/renderer/snippet-renderer-test.js b/report/test/renderer/snippet-renderer-test.js index b8ac062c7122..c404f055ca98 100644 --- a/report/test/renderer/snippet-renderer-test.js +++ b/report/test/renderer/snippet-renderer-test.js @@ -58,15 +58,13 @@ describe('DetailsRenderer', () => { let dom; beforeAll(() => { - global.Util = Util; - global.Util.i18n = new I18n('en', {...Util.UIStrings}); + Util.i18n = new I18n('en', {...Util.UIStrings}); const {document} = new jsdom.JSDOM(reportAssets.REPORT_TEMPLATES).window; dom = new DOM(document); }); afterAll(() => { - global.Util.i18n = undefined; - global.Util = undefined; + Util.i18n = undefined; }); function renderSnippet(details) { From 0ea11ab745637ea75dfb78cbd6c71d5a595a978a Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 17:41:24 -0700 Subject: [PATCH 30/71] fix treemap --- build/build-report.js | 15 +++++++++++++++ build/build-treemap.js | 7 ++++--- lighthouse-treemap/test/treemap-test-pptr.js | 13 ++++++++----- report/clients/treemap.js | 18 ++++++++++++++++++ report/clients/viewer.js | 2 ++ 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 report/clients/treemap.js diff --git a/build/build-report.js b/build/build-report.js index 7695ac0a088a..7a85a7e12513 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -41,6 +41,20 @@ async function buildViewerReport() { }); } +async function buildTreemapReport() { + const bundle = await rollup.rollup({ + input: 'report/clients/treemap.js', + plugins: [ + commonjs(), + ], + }); + + await bundle.write({ + file: 'dist/treemap-report.js', + format: 'iife', + }); +} + async function buildEsModulesBundle() { const bundle = await rollup.rollup({ input: 'report/clients/bundle.js', @@ -64,4 +78,5 @@ if (require.main === module) { module.exports = { buildStandaloneReport, buildViewerReport, + buildTreemapReport, }; diff --git a/build/build-treemap.js b/build/build-treemap.js index 3260a69e7e7e..00202262e493 100644 --- a/build/build-treemap.js +++ b/build/build-treemap.js @@ -8,6 +8,7 @@ /** @typedef {import('../lighthouse-core/lib/i18n/locales').LhlMessages} LhlMessages */ const fs = require('fs'); +const {buildTreemapReport} = require('./build-report.js'); const GhPagesApp = require('./gh-pages-app.js'); /** @@ -44,6 +45,8 @@ function buildStrings() { * Build treemap app, optionally deploying to gh-pages if `--deploy` flag was set. */ async function run() { + await buildTreemapReport(); + const app = new GhPagesApp({ name: 'treemap', appDir: `${__dirname}/../lighthouse-treemap/app`, @@ -64,9 +67,7 @@ async function run() { fs.readFileSync(require.resolve('pako/dist/pako_inflate.js'), 'utf-8'), /* eslint-enable max-len */ buildStrings(), - {path: '../../report/renderer/logger.js'}, - {path: '../../report/renderer/i18n.js'}, - {path: '../../report/renderer/text-encoding.js'}, + fs.readFileSync(__dirname + '/../dist/treemap-report.js', 'utf8'), {path: '../../lighthouse-viewer/app/src/drag-and-drop.js'}, {path: '../../lighthouse-viewer/app/src/github-api.js'}, {path: '../../lighthouse-viewer/app/src/firebase-auth.js'}, diff --git a/lighthouse-treemap/test/treemap-test-pptr.js b/lighthouse-treemap/test/treemap-test-pptr.js index e4da9b585a2c..60336543829e 100644 --- a/lighthouse-treemap/test/treemap-test-pptr.js +++ b/lighthouse-treemap/test/treemap-test-pptr.js @@ -20,6 +20,11 @@ const debugOptions = require('../app/debug.json'); // Make sure we get the more helpful test-specific timeout error instead of jest's generic one. jest.setTimeout(35_000); +function getTextEncodingCode() { + const code = fs.readFileSync(require.resolve('../../report/renderer/text-encoding.js'), 'utf-8'); + return code.replace('export ', ''); +} + describe('Lighthouse Treemap', () => { // eslint-disable-next-line no-console console.log('\n✨ Be sure to have recently run this: yarn build-treemap'); @@ -105,13 +110,12 @@ describe('Lighthouse Treemap', () => { expect(error).toBe('Error: Invalid options'); }); - it('from encoded fragment (gzip)', async () => { + it.only('from encoded fragment (gzip)', async () => { const options = JSON.parse(JSON.stringify(debugOptions)); options.lhr.requestedUrl += '😃😃😃'; const json = JSON.stringify(options); const encoded = await page.evaluate(` - ${fs.readFileSync( - require.resolve('../../report/renderer/text-encoding.js'), 'utf-8')} + ${getTextEncodingCode()} TextEncoding.toBase64(${JSON.stringify(json)}, {gzip: true}); `); @@ -128,8 +132,7 @@ describe('Lighthouse Treemap', () => { options.lhr.requestedUrl += '😃😃😃'; const json = JSON.stringify(options); const encoded = await page.evaluate(` - ${fs.readFileSync( - require.resolve('../../report/renderer/text-encoding.js'), 'utf-8')} + ${getTextEncodingCode()} TextEncoding.toBase64(${JSON.stringify(json)}, {gzip: false}); `); diff --git a/report/clients/treemap.js b/report/clients/treemap.js new file mode 100644 index 000000000000..b12dcf84834c --- /dev/null +++ b/report/clients/treemap.js @@ -0,0 +1,18 @@ +/** + * @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 window */ + +// TODO(esmodules): delete when treemap app is esm. + +import {I18n} from '../renderer/i18n.js'; +import {Logger} from '../renderer/logger.js'; +import {TextEncoding} from '../renderer/text-encoding.js'; + +window.I18n = I18n; +window.Logger = Logger; +window.TextEncoding = TextEncoding; diff --git a/report/clients/viewer.js b/report/clients/viewer.js index 83fab3e08a10..9c47545ca138 100644 --- a/report/clients/viewer.js +++ b/report/clients/viewer.js @@ -7,6 +7,8 @@ /* global window */ +// TODO(esmodules): delete when viewer app is esm. + import {DOM} from '../renderer/dom.js'; import {Logger} from '../renderer/logger.js'; import {ReportRenderer} from '../renderer/report-renderer.js'; From 6579cce7ac74dae6b5764a33253122f3f9c9e410 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 17:43:51 -0700 Subject: [PATCH 31/71] --experimental-vm-modules --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 601427c27b4b..109d717ee98d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "node ./lighthouse-cli/index.js", + "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "jest \"clients/\"", From 214eb779cc99ac16327b00d3cd4a9294ddf427ca Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 17:57:33 -0700 Subject: [PATCH 32/71] yarn jest --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 109d717ee98d..af069a613bae 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "node ./lighthouse-cli/index.js", - "jest": "node --experimental-vm-modules node_modules/.bin/jest", + "jest": "jest --experimental-vm-modules", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", - "test-clients": "jest \"clients/\"", + "test-clients": "yarn jest \"clients/\"", "test-viewer": "yarn unit-viewer && jest lighthouse-viewer/test/viewer-test-pptr.js", "test-treemap": "yarn unit-treemap && jest lighthouse-treemap/test/treemap-test-pptr.js", "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh", @@ -45,7 +45,7 @@ "test-proto": "yarn compile-proto && yarn build-proto-roundtrip", "unit-core": "jest \"lighthouse-core\"", "unit-cli": "jest \"lighthouse-cli/\"", - "unit-report": "jest \"report/\"", + "unit-report": "yarn jest \"report/\"", "unit-treemap": "jest \"lighthouse-treemap/.*-test.js\"", "unit-viewer": "jest \"lighthouse-viewer/.*-test.js\"", "unit": "yarn unit-core && yarn unit-cli && yarn unit-report && yarn unit-viewer && yarn unit-treemap", From 91b6926e43086b14622b1cee7aadbfb733f1e3da Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 18:02:20 -0700 Subject: [PATCH 33/71] build report --- .github/workflows/smoke.yml | 3 +++ .github/workflows/unit.yml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f7bf9370768a..a50ee626116b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -46,6 +46,7 @@ jobs: run: bash $GITHUB_WORKSPACE/lighthouse-core/scripts/download-chrome.sh && mv chrome-linux chrome-linux-tot - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - run: sudo apt-get install xvfb - name: Run smoke tests @@ -78,6 +79,7 @@ jobs: node-version: 12.x - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - name: Run smoke tests # Windows bots are slow, so only run enough tests to verify matching behavior. @@ -101,6 +103,7 @@ jobs: - run: yarn install --frozen-lockfile --network-timeout 1000000 - run: yarn build-devtools + - run: yarn build-report - run: sudo apt-get install xvfb - name: yarn test-bundle diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 498b7aaf2b05..5d922d89f2ab 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -44,6 +44,7 @@ jobs: pip install protobuf==3.7.1 - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - run: yarn test-proto # Run before unit-core because the roundtrip json is needed for proto tests. @@ -81,6 +82,7 @@ jobs: node-version: 12.x - run: yarn install --frozen-lockfile --network-timeout 1000000 + - run: yarn build-report - name: yarn unit-cli run: yarn unit-cli From 353c772bcffdbbbbcbb754537438418c8142785a Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 29 Jun 2021 21:46:01 -0700 Subject: [PATCH 34/71] dist --- .npmignore | 3 +++ build/build-report.js | 4 ++-- report/report-assets.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.npmignore b/.npmignore index 6bcf25f46d47..a587309a47f8 100644 --- a/.npmignore +++ b/.npmignore @@ -51,6 +51,9 @@ yarn-error.log results.html *.lcov +# generated files needed for publish +!dist/report/standalone.js + # dev files .DS_Store .editorconfig diff --git a/build/build-report.js b/build/build-report.js index 80dad2d1bf99..457c601283fe 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -32,8 +32,8 @@ async function buildStandaloneReport() { concatRendererCode(), fs.readFileSync(__dirname + '/../report/clients/standalone.js', 'utf8'), ].join(';\n'); - fs.mkdirSync(__dirname + '/../report/generated', {recursive: true}); - fs.writeFileSync(__dirname + '/../report/generated/standalone.js', REPORT_JAVASCRIPT); + fs.mkdirSync(__dirname + '/../dist/report', {recursive: true}); + fs.writeFileSync(__dirname + '/../dist/report/standalone.js', REPORT_JAVASCRIPT); } if (require.main === module) { diff --git a/report/report-assets.js b/report/report-assets.js index 2b2dfb6a6ded..b54411100f1c 100644 --- a/report/report-assets.js +++ b/report/report-assets.js @@ -8,7 +8,7 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/assets/standalone-template.html', 'utf8'); -const REPORT_JAVASCRIPT = fs.readFileSync(__dirname + '/generated/standalone.js', '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'); From 060eced429b991e03097be044fd01ae391e6c925 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 13:06:56 -0700 Subject: [PATCH 35/71] fix yarn jest --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af069a613bae..3c63825d5060 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "node ./lighthouse-cli/index.js", - "jest": "jest --experimental-vm-modules", + "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "yarn jest \"clients/\"", From bef23c16ace4dd1b654f83ec3924538add75bf1b Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 13:14:49 -0700 Subject: [PATCH 36/71] yarn build-report --- build/build-report.js | 10 ++++++++-- lighthouse-core/scripts/roll-to-devtools.sh | 2 +- package.json | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index b23a60a46921..35c7209ca652 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -71,8 +71,14 @@ async function buildEsModulesBundle() { if (require.main === module) { - buildStandaloneReport(); - buildEsModulesBundle(); + if (process.argv[2] === '--only-standalone') { + buildStandaloneReport(); + } else if (process.argv[2] === '--only-bundle') { + buildEsModulesBundle(); + } else { + buildStandaloneReport(); + buildEsModulesBundle(); + } } module.exports = { diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index 394bd1838e1e..f5d1eadb5d76 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -39,7 +39,7 @@ mkdir -p "$fe_lh_dir" lh_bg_js="dist/lighthouse-dt-bundle.js" -yarn build-report +yarn build-report --only-bundle yarn build-devtools # copy lighthouse-dt-bundle (potentially stale) diff --git a/package.json b/package.json index 0960b9629c52..447f8dce7b8f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .", "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", - "start": "yarn build-report && node ./lighthouse-cli/index.js", + "start": "yarn build-report --only-standalone && node ./lighthouse-cli/index.js", "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", From 5805f3461ed41cb7c377796df52045202a7b1417 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 13:26:36 -0700 Subject: [PATCH 37/71] lol --- .github/workflows/smoke.yml | 1 - lighthouse-core/scripts/copy-util-commonjs.sh | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 15f90da012ac..f237ea300888 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -104,7 +104,6 @@ jobs: - run: yarn install --frozen-lockfile --network-timeout 1000000 - run: yarn build-report - run: yarn build-devtools - - run: yarn build-report - run: sudo apt-get install xvfb - name: yarn test-bundle diff --git a/lighthouse-core/scripts/copy-util-commonjs.sh b/lighthouse-core/scripts/copy-util-commonjs.sh index 85d10249b96e..6c871a98049a 100644 --- a/lighthouse-core/scripts/copy-util-commonjs.sh +++ b/lighthouse-core/scripts/copy-util-commonjs.sh @@ -6,6 +6,8 @@ # 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. ## +# TODO(esmodules): delete when consumers of util.js are all esm. + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" LH_ROOT_DIR="$SCRIPT_DIR/../.." From f1b4351a088f6a489c715e1a2783651bab4b472a Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 14:31:13 -0700 Subject: [PATCH 38/71] tweak --- build/build-treemap.js | 2 +- build/build-viewer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/build-treemap.js b/build/build-treemap.js index aaa30567b4a1..9860c91514d3 100644 --- a/build/build-treemap.js +++ b/build/build-treemap.js @@ -68,7 +68,7 @@ async function run() { fs.readFileSync(require.resolve('pako/dist/pako_inflate.js'), 'utf-8'), /* eslint-enable max-len */ buildStrings(), - fs.readFileSync(LH_ROOT + '/dist/report/treemap.js', 'utf8'), + {path: '../../dist/report/treemap.js'}, {path: '../../lighthouse-viewer/app/src/drag-and-drop.js'}, {path: '../../lighthouse-viewer/app/src/github-api.js'}, {path: '../../lighthouse-viewer/app/src/firebase-auth.js'}, diff --git a/build/build-viewer.js b/build/build-viewer.js index 838bed9e1835..c55ffed2baee 100644 --- a/build/build-viewer.js +++ b/build/build-viewer.js @@ -47,8 +47,8 @@ async function run() { ], javascripts: [ await generatorJsPromise, - fs.readFileSync(LH_ROOT + '/dist/report/viewer.js', 'utf8'), fs.readFileSync(require.resolve('idb-keyval/dist/idb-keyval-min.js'), 'utf8'), + {path: '../../dist/report/viewer.js'}, {path: 'src/*'}, ], assets: [ From 7c9d522a0bfaaf9d50769658819a938c42d84163 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 14:33:11 -0700 Subject: [PATCH 39/71] only --- lighthouse-treemap/test/treemap-test-pptr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-treemap/test/treemap-test-pptr.js b/lighthouse-treemap/test/treemap-test-pptr.js index 60336543829e..0fe5e63307ce 100644 --- a/lighthouse-treemap/test/treemap-test-pptr.js +++ b/lighthouse-treemap/test/treemap-test-pptr.js @@ -110,7 +110,7 @@ describe('Lighthouse Treemap', () => { expect(error).toBe('Error: Invalid options'); }); - it.only('from encoded fragment (gzip)', async () => { + it('from encoded fragment (gzip)', async () => { const options = JSON.parse(JSON.stringify(debugOptions)); options.lhr.requestedUrl += '😃😃😃'; const json = JSON.stringify(options); From be94766c193bcfd07981e13fc9a7177b103a5fc4 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 14:36:17 -0700 Subject: [PATCH 40/71] tweak roll script --- lighthouse-core/scripts/roll-to-devtools.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index f5d1eadb5d76..037106d8ac92 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -46,12 +46,12 @@ yarn build-devtools cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." -# generate report.d.ts -npx tsc --allowJs --declaration --emitDeclarationOnly dist/report.js +# generate bundle.d.ts +npx tsc --allowJs --declaration --emitDeclarationOnly dist/report/bundle.js # copy report code $fe_lh_dir fe_lh_report_dir="$fe_lh_dir/report/" -cp dist/report.js dist/report.d.ts "$fe_lh_report_dir" +cp dist/report/bundle.js dist/report/bundle.d.ts "$fe_lh_report_dir" echo -e "$check Report code copied." # copy report generator + cached resources into $fe_lh_dir From c419c302ccf1473c91bfc0e6323a5a8e198477fe Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 14:40:54 -0700 Subject: [PATCH 41/71] tweak --- build/build-report.js | 2 -- lighthouse-core/scripts/roll-to-devtools.sh | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/build/build-report.js b/build/build-report.js index 35c7209ca652..d68f876f5f08 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -73,8 +73,6 @@ async function buildEsModulesBundle() { if (require.main === module) { if (process.argv[2] === '--only-standalone') { buildStandaloneReport(); - } else if (process.argv[2] === '--only-bundle') { - buildEsModulesBundle(); } else { buildStandaloneReport(); buildEsModulesBundle(); diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh index 037106d8ac92..eb10a572e3e1 100755 --- a/lighthouse-core/scripts/roll-to-devtools.sh +++ b/lighthouse-core/scripts/roll-to-devtools.sh @@ -39,12 +39,12 @@ mkdir -p "$fe_lh_dir" lh_bg_js="dist/lighthouse-dt-bundle.js" -yarn build-report --only-bundle +yarn build-report yarn build-devtools -# copy lighthouse-dt-bundle (potentially stale) +# copy lighthouse-dt-bundle cp -pPR "$lh_bg_js" "$fe_lh_dir/lighthouse-dt-bundle.js" -echo -e "$check (Potentially stale) lighthouse-dt-bundle copied." +echo -e "$check lighthouse-dt-bundle copied." # generate bundle.d.ts npx tsc --allowJs --declaration --emitDeclarationOnly dist/report/bundle.js From 9ec4934ca42cd2ca5de6c5009401565778df2722 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 15:17:05 -0700 Subject: [PATCH 42/71] fix dt build --- build/build-dt-report-resources.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/build-dt-report-resources.js b/build/build-dt-report-resources.js index 6f3a97ddf339..9f1e9d853119 100644 --- a/build/build-dt-report-resources.js +++ b/build/build-dt-report-resources.js @@ -38,8 +38,8 @@ writeFile('report-generator.d.ts', 'export {}'); const pathToReportAssets = require.resolve('../clients/devtools-report-assets.js'); browserify(generatorFilename, {standalone: 'Lighthouse.ReportGenerator'}) - // Shims './html/html-report-assets.js' to resolve to devtools-report-assets.js - .require(pathToReportAssets, {expose: './html/html-report-assets.js'}) + // Shims './report/report-assets.js' to resolve to devtools-report-assets.js + .require(pathToReportAssets, {expose: './report-assets.js'}) .bundle((err, src) => { if (err) throw err; fs.writeFileSync(bundleOutFile, src.toString()); From 329f09b512fc1d612e2994abee490a40732acf3d Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 15:33:26 -0700 Subject: [PATCH 43/71] fix test --- lighthouse-core/test/lib/page-functions-test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index 6736404b6f87..39b7cc5cbfe2 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -7,9 +7,11 @@ const assert = require('assert').strict; const jsdom = require('jsdom'); -const DOM = require('../../../report/renderer/dom.js'); const pageFunctions = require('../../lib/page-functions.js'); +/** @type {import('../../../report/renderer/dom.js').DOM} */ +let DOM; + /* eslint-env jest */ describe('Page Functions', () => { @@ -17,6 +19,8 @@ describe('Page Functions', () => { let dom; beforeAll(() => { + // TODO(esmodules): remove when this file is esm. + DOM = await import('../../../report/renderer/dom.js'); const {document, ShadowRoot, Node, HTMLElement} = new jsdom.JSDOM('', {url}).window; global.ShadowRoot = ShadowRoot; global.Node = Node; From a3bb85a5cbd97b4f62352cea27cd103bdc7e9304 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Wed, 30 Jun 2021 15:45:25 -0700 Subject: [PATCH 44/71] psi --- build/build-lightrider-bundles.js | 2 ++ build/build-report.js | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/build/build-lightrider-bundles.js b/build/build-lightrider-bundles.js index 88d376a81ce3..27970be7150d 100644 --- a/build/build-lightrider-bundles.js +++ b/build/build-lightrider-bundles.js @@ -10,6 +10,7 @@ const fs = require('fs'); const path = require('path'); const bundleBuilder = require('./build-bundle.js'); const {minifyFileTransform} = require('./build-utils.js'); +const {buildPsiReport} = require('./build-report.js'); const {LH_ROOT} = require('../root.js'); const distDir = path.join(LH_ROOT, 'dist', 'lightrider'); @@ -53,6 +54,7 @@ async function run() { await Promise.all([ buildEntryPoint(), buildReportGenerator(), + buildPsiReport(), ]); } diff --git a/build/build-report.js b/build/build-report.js index d68f876f5f08..881cd9165188 100644 --- a/build/build-report.js +++ b/build/build-report.js @@ -27,6 +27,20 @@ async function buildStandaloneReport() { }); } +async function buildPsiReport() { + const bundle = await rollup.rollup({ + input: 'report/clients/psi.js', + plugins: [ + commonjs(), + ], + }); + + await bundle.write({ + file: 'dist/report/psi.js', + format: 'esm', + }); +} + async function buildViewerReport() { const bundle = await rollup.rollup({ input: 'report/clients/viewer.js', @@ -69,7 +83,6 @@ async function buildEsModulesBundle() { }); } - if (require.main === module) { if (process.argv[2] === '--only-standalone') { buildStandaloneReport(); @@ -81,6 +94,7 @@ if (require.main === module) { module.exports = { buildStandaloneReport, + buildPsiReport, buildViewerReport, buildTreemapReport, }; From a8bb6f043c8806403092ecfb67ca4c375c1c0b43 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 12:36:48 -0700 Subject: [PATCH 45/71] fix yarn test-clients --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 447f8dce7b8f..63e03e3e746e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", - "test-clients": "yarn jest \"clients/\"", + "test-clients": "yarn jest \"$PWD/clients/\"", "test-viewer": "yarn unit-viewer && jest lighthouse-viewer/test/viewer-test-pptr.js", "test-treemap": "yarn unit-treemap && jest lighthouse-treemap/test/treemap-test-pptr.js", "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh", From d78111aafb6cae1c66cec179007b035fdd5aed76 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 16:59:28 -0700 Subject: [PATCH 46/71] scripts(i18n): support es modules in collect-strings --- lighthouse-core/lib/i18n/locales/en-US.json | 3 + lighthouse-core/lib/i18n/locales/en-XL.json | 3 + .../scripts/i18n/bake-ctc-to-lhl.js | 22 ++---- .../scripts/i18n/collect-strings.js | 77 +++++++++++-------- .../scripts/i18n/count-translated.js | 17 ++-- lighthouse-core/scripts/i18n/package.json | 3 + .../i18n/prune-obsolete-lhl-messages.js | 32 ++++---- .../scripts/i18n/tmp-esm-strings.js | 13 ++++ package.json | 1 + report/renderer/util.js | 3 + root.js | 17 +++- tsconfig.json | 4 +- types/es-main.d.ts | 10 +++ yarn.lock | 5 ++ 14 files changed, 136 insertions(+), 74 deletions(-) create mode 100644 lighthouse-core/scripts/i18n/package.json create mode 100644 lighthouse-core/scripts/i18n/tmp-esm-strings.js create mode 100644 types/es-main.d.ts diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index d3b91964c626..d0f3a217f2a7 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1955,6 +1955,9 @@ "lighthouse-core/lib/lh-error.js | urlInvalid": { "message": "The URL you have provided appears to be invalid." }, + "lighthouse-core/scripts/i18n/tmp-esm-strings.js | varianceDisclaimer": { + "message": "Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics." + }, "lighthouse-treemap/app/src/util.js | allLabel": { "message": "All" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index d068a57c3511..b6ed573d4515 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1955,6 +1955,9 @@ "lighthouse-core/lib/lh-error.js | urlInvalid": { "message": "T̂h́ê ÚR̂Ĺ ŷóû h́âv́ê ṕr̂óv̂íd̂éd̂ áp̂ṕêár̂ś t̂ó b̂é îńv̂ál̂íd̂." }, + "lighthouse-core/scripts/i18n/tmp-esm-strings.js | varianceDisclaimer": { + "message": "V̂ál̂úêś âŕê éŝt́îḿât́êd́ âńd̂ ḿâý v̂ár̂ý. T̂h́ê [ṕêŕf̂ór̂ḿâńĉé ŝćôŕê íŝ ćâĺĉúl̂át̂éd̂](https://web.dev/performance-scoring/) d́îŕêćt̂ĺŷ f́r̂óm̂ t́ĥéŝé m̂ét̂ŕîćŝ." + }, "lighthouse-treemap/app/src/util.js | allLabel": { "message": "Âĺl̂" }, diff --git a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js index 741d8ad58ad3..446324289a87 100644 --- a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js +++ b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js @@ -8,9 +8,8 @@ /* eslint-disable no-console */ -const fs = require('fs'); -const path = require('path'); -const {LH_ROOT} = require('../../../root.js'); +import fs from 'fs'; +import path from 'path'; /** * @typedef CtcMessage @@ -64,7 +63,7 @@ const {LH_ROOT} = require('../../../root.js'); * @param {Record} messages * @return {Record} */ -function bakePlaceholders(messages) { +export function bakePlaceholders(messages) { /** @type {Record} */ const bakedMessages = {}; @@ -119,15 +118,13 @@ function saveLhlStrings(path, localeStrings) { * @param {string} dir * @return {Array} */ -function collectAndBakeCtcStrings(dir) { +export function collectAndBakeCtcStrings(dir) { const lhlFilenames = []; for (const filename of fs.readdirSync(dir)) { - const fullPath = path.join(dir, filename); - const relativePath = path.relative(LH_ROOT, fullPath); - if (filename.endsWith('.ctc.json')) { - if (!process.env.CI) console.log('Baking', relativePath); - const ctcStrings = loadCtcStrings(relativePath); + const fullPath = path.join(dir, filename); + if (!process.env.CI) console.log('Baking', fullPath); + const ctcStrings = loadCtcStrings(fullPath); const strings = bakePlaceholders(ctcStrings); const outputFile = path.join(dir, path.basename(filename).replace('.ctc', '')); saveLhlStrings(outputFile, strings); @@ -136,8 +133,3 @@ function collectAndBakeCtcStrings(dir) { } return lhlFilenames; } - -module.exports = { - collectAndBakeCtcStrings, - bakePlaceholders, -}; diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 2f5b20febab1..16ba1f10c51b 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -8,18 +8,25 @@ /* eslint-disable no-console, max-len */ -const fs = require('fs'); -const glob = require('glob'); -const path = require('path'); -const expect = require('expect'); -const tsc = require('typescript'); -const MessageParser = require('intl-messageformat-parser').default; -const Util = require('../../../report/renderer/util.js'); -const {collectAndBakeCtcStrings} = require('./bake-ctc-to-lhl.js'); -const {pruneObsoleteLhlMessages} = require('./prune-obsolete-lhl-messages.js'); -const {countTranslatedMessages} = require('./count-translated.js'); -const {LH_ROOT} = require('../../../root.js'); - +import fs from 'fs'; +import glob from 'glob'; +import path from 'path'; +import expect from 'expect'; +import tsc from 'typescript'; +import MessageParser from 'intl-messageformat-parser'; +import Util from '../../../report/renderer/util.js'; +import {collectAndBakeCtcStrings} from './bake-ctc-to-lhl.js'; +import {pruneObsoleteLhlMessages} from './prune-obsolete-lhl-messages.js'; +import {countTranslatedMessages} from './count-translated.js'; +import module from 'module'; +import url from 'url'; +import esMain from 'es-main'; + +const require = module.createRequire(import.meta.url); + +const dir = path.dirname(url.fileURLToPath(import.meta.url)); + +const LH_ROOT = path.join(dir, '../../..'); const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; /** @typedef {import('./bake-ctc-to-lhl.js').CtcMessage} CtcMessage */ @@ -28,19 +35,25 @@ const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; const foldersWithStrings = [ `${LH_ROOT}/lighthouse-core`, - `${LH_ROOT}/report/renderer`, + `${LH_ROOT}/report`, `${LH_ROOT}/lighthouse-treemap`, path.dirname(require.resolve('lighthouse-stack-packs')) + '/packs', ]; const ignoredPathComponents = [ '**/.git/**', - '**/scripts/**', + // TODO(esmodules): remove when some other file with real strings is esm. + '**/scripts/!(i18n)/**', // ignore all scripts *except* test esm file + '**/collect-strings.js', + // '**/scripts/**', + // end TODO '**/node_modules/!(lighthouse-stack-packs)/**', // ignore all node modules *except* stack packs '**/lighthouse-core/lib/stack-packs.js', '**/test/**', '**/*-test.js', '**/*-renderer.js', + '**/report/clients/*.js', + '**/util-commonjs.js', 'lighthouse-treemap/app/src/main.js', ]; @@ -143,7 +156,7 @@ function parseExampleJsDoc(rawExample) { * @param {Record} examples * @return {IncrementalCtc} */ -function convertMessageToCtc(lhlMessage, examples = {}) { +export function convertMessageToCtc(lhlMessage, examples = {}) { _lhlValidityChecks(lhlMessage); /** @type {IncrementalCtc} */ @@ -414,7 +427,7 @@ function _ctcValidityChecks(icu) { * @param {Record} messages * @return {Record} */ -function createPsuedoLocaleStrings(messages) { +export function createPsuedoLocaleStrings(messages) { /** @type {Record} */ const psuedoLocalizedStrings = {}; for (const [key, ctc] of Object.entries(messages)) { @@ -479,7 +492,7 @@ function getIdentifier(node) { * @param {Record} liveUIStrings The actual imported UIStrings object. * @return {Record} */ -function parseUIStrings(sourceStr, liveUIStrings) { +export function parseUIStrings(sourceStr, liveUIStrings) { const tsAst = tsc.createSourceFile('uistrings', sourceStr, tsc.ScriptTarget.ES2019, true, tsc.ScriptKind.JS); const extractionError = new Error('UIStrings declaration was not extracted correctly by the collect-strings regex.'); @@ -521,9 +534,9 @@ function parseUIStrings(sourceStr, liveUIStrings) { * Collects all LHL messsages defined in UIString from Javascript files in dir, * and converts them into CTC. * @param {string} dir absolute path - * @return {Record} + * @return {Promise>} */ -function collectAllStringsInDir(dir) { +async function collectAllStringsInDir(dir) { /** @type {Record} */ const strings = {}; @@ -538,9 +551,9 @@ function collectAllStringsInDir(dir) { if (!process.env.CI) console.log('Collecting from', relativeToRootPath); const content = fs.readFileSync(absolutePath, 'utf8'); - const exportVars = require(absolutePath); + const exportVars = await import(absolutePath); const regexMatch = content.match(UISTRINGS_REGEX); - const exportedUIStrings = exportVars.UIStrings; + const exportedUIStrings = exportVars.UIStrings || (exportVars.default && exportVars.default.UIStrings); if (!regexMatch) { // No UIStrings found in the file text or exports, so move to the next. @@ -550,7 +563,8 @@ function collectAllStringsInDir(dir) { } if (!exportedUIStrings) { - throw new Error('UIStrings defined in file but not exported'); + console.log({exportVars}); + throw new Error(`UIStrings defined in file but not exported: ${absolutePath}`); } // just parse the UIStrings substring to avoid ES version issues, save time, etc @@ -606,6 +620,7 @@ function writeStringsToCtcFiles(locale, strings) { * * @param {Record} strings */ +// eslint-disable-next-line no-unused-vars function resolveMessageCollisions(strings) { /** @type {Map>} */ const stringsByMessage = new Map(); @@ -689,18 +704,19 @@ function resolveMessageCollisions(strings) { } } -// Test if called from the CLI or as a module. -if (require.main === module) { +async function main() { /** @type {Record} */ const strings = {}; for (const folderWithStrings of foldersWithStrings) { console.log(`\n====\nCollecting strings from ${folderWithStrings}\n====`); - const moreStrings = collectAllStringsInDir(folderWithStrings); + const moreStrings = await collectAllStringsInDir(folderWithStrings); Object.assign(strings, moreStrings); } - resolveMessageCollisions(strings); + // TODO: commenting out for now. After PR review, will uncomment and delete the "tmp-esm-strings.js" + // that shows esm working. + // resolveMessageCollisions(strings); writeStringsToCtcFiles('en-US', strings); console.log('Written to disk!', 'en-US.ctc.json'); @@ -730,8 +746,7 @@ if (require.main === module) { console.log('✨ Complete!'); } -module.exports = { - parseUIStrings, - createPsuedoLocaleStrings, - convertMessageToCtc, -}; +// Test if called from the CLI or as a module. +if (esMain(import.meta)) { + main(); +} diff --git a/lighthouse-core/scripts/i18n/count-translated.js b/lighthouse-core/scripts/i18n/count-translated.js index d64da7eacfec..b4033dbd57fe 100644 --- a/lighthouse-core/scripts/i18n/count-translated.js +++ b/lighthouse-core/scripts/i18n/count-translated.js @@ -7,21 +7,20 @@ /** @typedef {import('../../lib/i18n/locales').LhlMessages} LhlMessages */ -const fs = require('fs'); -const glob = require('glob'); -const path = require('path'); -const {LH_ROOT} = require('../../../root.js'); +import fs from 'fs'; +import path from 'path'; +import glob from 'glob'; +import {LH_ROOT, importJson} from '../../../root.js'; -const enUsLhlFilename = LH_ROOT + '/lighthouse-core/lib/i18n/locales/en-US.json'; /** @type {LhlMessages} */ -const enUsLhl = JSON.parse(fs.readFileSync(enUsLhlFilename, 'utf8')); +const enUsLhl = importJson('../../../lighthouse-core/lib/i18n/locales/en-US.json', import.meta); /** * Count how many locale files have a translated version of each string found in * the `en-US.json` i18n messages. * @return {{localeCount: number, messageCount: number, translatedCount: number, partiallyTranslatedCount: number, notTranslatedCount: number}} */ -function countTranslatedMessages() { +export function countTranslatedMessages() { // Find all locale files, ignoring self-generated en-US and en-XL, and ctc files. const ignore = [ '**/.ctc.json', @@ -66,7 +65,3 @@ function countTranslatedMessages() { notTranslatedCount, }; } - -module.exports = { - countTranslatedMessages, -}; diff --git a/lighthouse-core/scripts/i18n/package.json b/lighthouse-core/scripts/i18n/package.json new file mode 100644 index 000000000000..aead43de364c --- /dev/null +++ b/lighthouse-core/scripts/i18n/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js index 724ec783ab08..73a2f9acc6d6 100644 --- a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js +++ b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js @@ -5,12 +5,14 @@ */ 'use strict'; -const fs = require('fs'); -const path = require('path'); -const glob = require('glob'); -const MessageParser = require('intl-messageformat-parser').default; -const {collectAllCustomElementsFromICU} = require('../../lib/i18n/i18n.js'); -const {LH_ROOT} = require('../../../root.js'); +import fs from 'fs'; +import glob from 'glob'; +import path from 'path'; +import url from 'url'; +import MessageParser from 'intl-messageformat-parser'; +import {collectAllCustomElementsFromICU} from '../../lib/i18n/i18n.js'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); /** @typedef {Record} LhlMessages */ @@ -114,8 +116,9 @@ function getGoldenLocaleArgumentIds(goldenLhl) { * translations should no longer be used, it's up to the author to remove them * (e.g. by picking a new message id). */ -function pruneObsoleteLhlMessages() { - const goldenLhl = require('../../lib/i18n/locales/en-US.json'); +export function pruneObsoleteLhlMessages() { + const goldenLhlPath = new URL('../../lib/i18n/locales/en-US.json', import.meta.url); + const goldenLhl = JSON.parse(fs.readFileSync(goldenLhlPath, 'utf-8')); const goldenLocaleArgumentIds = getGoldenLocaleArgumentIds(goldenLhl); // Find all locale files, ignoring self-generated en-US, en-XL, and ctc files. @@ -125,15 +128,16 @@ function pruneObsoleteLhlMessages() { '**/en-XL.json', ]; const globPattern = 'lighthouse-core/lib/i18n/locales/**/+([-a-zA-Z0-9]).json'; + const lhRoot = `${__dirname}/../../../`; const localePaths = glob.sync(globPattern, { ignore, - cwd: LH_ROOT, + cwd: lhRoot, }); /** @type {Set} */ const alreadyLoggedPrunes = new Set(); for (const localePath of localePaths) { - const absoluteLocalePath = path.join(LH_ROOT, localePath); + const absoluteLocalePath = path.join(lhRoot, localePath); // readFileSync so that the file is pulled again once updated by a collect-strings run const localeLhl = JSON.parse(fs.readFileSync(absoluteLocalePath, 'utf-8')); const prunedLocale = pruneLocale(goldenLocaleArgumentIds, localeLhl, alreadyLoggedPrunes); @@ -143,10 +147,8 @@ function pruneObsoleteLhlMessages() { } } -module.exports = { - pruneObsoleteLhlMessages, - - // Exported for testing. +// Exported for testing. +export { getGoldenLocaleArgumentIds, - pruneLocale, + pruneLocale }; diff --git a/lighthouse-core/scripts/i18n/tmp-esm-strings.js b/lighthouse-core/scripts/i18n/tmp-esm-strings.js new file mode 100644 index 000000000000..f56e7cbf659a --- /dev/null +++ b/lighthouse-core/scripts/i18n/tmp-esm-strings.js @@ -0,0 +1,13 @@ +/** + * @license Copyright 2018 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'; + +// TODO(esmodules): remove when some other file with real strings is esm. + +export const UIStrings = { + /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */ + varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.', +}; diff --git a/package.json b/package.json index 0413e87e7734..4f15fa6d84b6 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "cross-env": "^7.0.2", "csv-validator": "^0.0.3", "devtools-protocol": "0.0.863986", + "es-main": "^1.0.2", "eslint": "^7.23.0", "eslint-config-google": "^0.9.1", "eslint-plugin-local-rules": "0.1.0", diff --git a/report/renderer/util.js b/report/renderer/util.js index a979531a1712..eb0bf14ec9eb 100644 --- a/report/renderer/util.js +++ b/report/renderer/util.js @@ -645,3 +645,6 @@ if (typeof module !== 'undefined' && module.exports) { } else { self.Util = Util; } + +// TODO(esmodules): export these strings too, then collect-strings will work when this file is esm. +// export const UIStrings = Util.UIStrings; diff --git a/root.js b/root.js index ecf261013fbb..e90666a305aa 100644 --- a/root.js +++ b/root.js @@ -5,4 +5,19 @@ */ 'use strict'; -module.exports.LH_ROOT = __dirname; +const fs = require('fs'); +const url = require('url'); + +/** + * @param {string} path + * @param {ImportMeta} importMeta + */ +function importJson(path, importMeta) { + const json = fs.readFileSync(url.fileURLToPath(new URL(path, importMeta.url)), 'utf-8'); + return JSON.parse(json); +} + +module.exports = { + LH_ROOT: __dirname, + importJson, +}; diff --git a/tsconfig.json b/tsconfig.json index c2654b7de1ab..319cc8ad0e00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "noEmit": true, - "module": "commonjs", + "module": "ES2020", + "moduleResolution": "node", "target": "ES2020", "allowJs": true, "checkJs": true, @@ -10,6 +11,7 @@ // "noErrorTruncation": true, "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, "diagnostics": true }, "include": [ diff --git a/types/es-main.d.ts b/types/es-main.d.ts new file mode 100644 index 000000000000..8f3235ee0d1b --- /dev/null +++ b/types/es-main.d.ts @@ -0,0 +1,10 @@ +/** + * @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 function esMain(importMeta: ImportMeta): boolean; +declare module 'es-main' { + export = esMain; +} diff --git a/yarn.lock b/yarn.lock index 119315badee7..6e99942b5ccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3174,6 +3174,11 @@ es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.0" +es-main@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-main/-/es-main-1.0.2.tgz#c9030d78796f609f865b66f4125a78d77fec3de3" + integrity sha512-LLgW8Cby/FiyQygrI23q2EswulHiDKoyjWlDRgTGXjQ3iRim2R26VfoehpxI5oKRXSNams3L/80KtggoUdxdDQ== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" From cd3e92aa3289204cbbffad45dba0ad8aef838fd5 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 17:06:26 -0700 Subject: [PATCH 47/71] more root --- lighthouse-core/scripts/i18n/collect-strings.js | 9 +++------ .../scripts/i18n/prune-obsolete-lhl-messages.js | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 16ba1f10c51b..8e01bf5c6bab 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -14,19 +14,16 @@ import path from 'path'; import expect from 'expect'; import tsc from 'typescript'; import MessageParser from 'intl-messageformat-parser'; +import module from 'module'; +import esMain from 'es-main'; import Util from '../../../report/renderer/util.js'; import {collectAndBakeCtcStrings} from './bake-ctc-to-lhl.js'; import {pruneObsoleteLhlMessages} from './prune-obsolete-lhl-messages.js'; import {countTranslatedMessages} from './count-translated.js'; -import module from 'module'; -import url from 'url'; -import esMain from 'es-main'; +import {LH_ROOT} from '../../../root.js'; const require = module.createRequire(import.meta.url); -const dir = path.dirname(url.fileURLToPath(import.meta.url)); - -const LH_ROOT = path.join(dir, '../../..'); const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; /** @typedef {import('./bake-ctc-to-lhl.js').CtcMessage} CtcMessage */ diff --git a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js index 73a2f9acc6d6..c8b6151534b5 100644 --- a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js +++ b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js @@ -8,11 +8,9 @@ import fs from 'fs'; import glob from 'glob'; import path from 'path'; -import url from 'url'; import MessageParser from 'intl-messageformat-parser'; import {collectAllCustomElementsFromICU} from '../../lib/i18n/i18n.js'; - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +import {LH_ROOT} from '../../../root.js'; /** @typedef {Record} LhlMessages */ @@ -128,16 +126,15 @@ export function pruneObsoleteLhlMessages() { '**/en-XL.json', ]; const globPattern = 'lighthouse-core/lib/i18n/locales/**/+([-a-zA-Z0-9]).json'; - const lhRoot = `${__dirname}/../../../`; const localePaths = glob.sync(globPattern, { ignore, - cwd: lhRoot, + cwd: LH_ROOT, }); /** @type {Set} */ const alreadyLoggedPrunes = new Set(); for (const localePath of localePaths) { - const absoluteLocalePath = path.join(lhRoot, localePath); + const absoluteLocalePath = path.join(LH_ROOT, localePath); // readFileSync so that the file is pulled again once updated by a collect-strings run const localeLhl = JSON.parse(fs.readFileSync(absoluteLocalePath, 'utf-8')); const prunedLocale = pruneLocale(goldenLocaleArgumentIds, localeLhl, alreadyLoggedPrunes); From 258dcaf4d7eed0f8ab45964d5e45650c59fc41c0 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 17:12:06 -0700 Subject: [PATCH 48/71] fix script tests --- lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js | 2 +- lighthouse-core/test/scripts/i18n/collect-strings-test.js | 2 +- lighthouse-core/test/scripts/i18n/package.json | 1 + .../test/scripts/i18n/prune-obsolete-lhl-messages-test.js | 2 +- package.json | 3 ++- 5 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 lighthouse-core/test/scripts/i18n/package.json diff --git a/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js b/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js index 16d2547f495f..e232341507fe 100644 --- a/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js +++ b/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -const bakery = require('../../../scripts/i18n/bake-ctc-to-lhl.js'); +import * as bakery from '../../../scripts/i18n/bake-ctc-to-lhl.js'; describe('Baking Placeholders', () => { it('passthroughs a basic message unchanged', () => { diff --git a/lighthouse-core/test/scripts/i18n/collect-strings-test.js b/lighthouse-core/test/scripts/i18n/collect-strings-test.js index 181e7720f4db..85dfdbb74e93 100644 --- a/lighthouse-core/test/scripts/i18n/collect-strings-test.js +++ b/lighthouse-core/test/scripts/i18n/collect-strings-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -const collect = require('../../../scripts/i18n/collect-strings.js'); +import * as collect from '../../../scripts/i18n/collect-strings.js'; function evalJustUIStrings(justUIStrings) { return Function(`'use strict'; ${justUIStrings} return UIStrings;`)(); diff --git a/lighthouse-core/test/scripts/i18n/package.json b/lighthouse-core/test/scripts/i18n/package.json new file mode 100644 index 000000000000..1632c2c4df68 --- /dev/null +++ b/lighthouse-core/test/scripts/i18n/package.json @@ -0,0 +1 @@ +{"type": "module"} \ No newline at end of file diff --git a/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js b/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js index fd216675b9c1..ac4fd8f66b5d 100644 --- a/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js +++ b/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -const pruneObsoleteLhlMessages = require('../../../scripts/i18n/prune-obsolete-lhl-messages.js'); +import * as pruneObsoleteLhlMessages from '../../../scripts/i18n/prune-obsolete-lhl-messages.js'; /** * Prune `localeLhl` based on `goldenLhl`. diff --git a/package.json b/package.json index 4f15fa6d84b6..15928e9cdf9b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "yarn build-report && node ./lighthouse-cli/index.js", + "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "jest \"clients/\"", @@ -42,7 +43,7 @@ "test-legacy-javascript": "bash lighthouse-core/scripts/test-legacy-javascript.sh", "test-docs": "yarn --cwd docs/recipes/auth && jest docs/recipes/integration-test && yarn --cwd docs/recipes/custom-gatherer-puppeteer test", "test-proto": "yarn compile-proto && yarn build-proto-roundtrip", - "unit-core": "jest \"lighthouse-core\"", + "unit-core": "yarn jest \"lighthouse-core\"", "unit-cli": "jest \"lighthouse-cli/\"", "unit-report": "jest \"report/\"", "unit-treemap": "jest \"lighthouse-treemap/.*-test.js\"", From 2e013444f7eaf7d3b1249f5166a607274aa7c2c9 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 18:58:04 -0700 Subject: [PATCH 49/71] ci --- .github/workflows/ci.yml | 2 +- lighthouse-core/scripts/copy-util-commonjs.sh | 2 +- lighthouse-core/util-commonjs.js | 2 ++ types/es-main.d.ts | 3 +-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28bfd259e619..bb081aa0d865 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: - run: yarn test-legacy-javascript - run: yarn i18n:checks - run: yarn dogfood-lhci - - run: sh lighthouse-core/scripts/copy-util-commonjs.sh + - run: bash lighthouse-core/scripts/copy-util-commonjs.sh # Fail if any changes were written to any source files or generated untracked files (ex, from: build/build-cdt-lib.js). - run: git add -A && git diff --cached --exit-code diff --git a/lighthouse-core/scripts/copy-util-commonjs.sh b/lighthouse-core/scripts/copy-util-commonjs.sh index 6c871a98049a..7f83cd7f467e 100644 --- a/lighthouse-core/scripts/copy-util-commonjs.sh +++ b/lighthouse-core/scripts/copy-util-commonjs.sh @@ -16,5 +16,5 @@ OUT_FILE="$LH_ROOT_DIR"/lighthouse-core/util-commonjs.js echo '// @ts-nocheck' > "$OUT_FILE" echo '// Auto-generated by lighthouse-core/scripts/copy-util-commonjs.sh' >> "$OUT_FILE" echo '// Temporary solution until all our code uses esmodules' >> "$OUT_FILE" -sed 's/export class Util/class Util/g' "$LH_ROOT_DIR"/report/renderer/util.js >> "$OUT_FILE" +sed 's/export class Util/class Util/g; s/export const UIStrings = Util.UIStrings;//g' "$LH_ROOT_DIR"/report/renderer/util.js >> "$OUT_FILE" echo 'module.exports = Util;' >> "$OUT_FILE" diff --git a/lighthouse-core/util-commonjs.js b/lighthouse-core/util-commonjs.js index 07fdce225d47..e7fb08bed0b7 100644 --- a/lighthouse-core/util-commonjs.js +++ b/lighthouse-core/util-commonjs.js @@ -640,4 +640,6 @@ Util.UIStrings = { /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */ throttlingProvided: 'Provided by environment', }; + + module.exports = Util; diff --git a/types/es-main.d.ts b/types/es-main.d.ts index 8f3235ee0d1b..bbc6ec3866f7 100644 --- a/types/es-main.d.ts +++ b/types/es-main.d.ts @@ -4,7 +4,6 @@ * 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 function esMain(importMeta: ImportMeta): boolean; declare module 'es-main' { - export = esMain; + export default function(meta: ImportMeta): boolean; } From 8307eaf0dbb1e5e93055a630f92ddc8b833b4a7c Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:11:41 -0700 Subject: [PATCH 50/71] just import --- .../scripts/i18n/bake-ctc-to-lhl.js | 22 ++++--- .../scripts/i18n/collect-strings.js | 58 ++++++++----------- .../scripts/i18n/count-translated.js | 17 ++++-- .../i18n/prune-obsolete-lhl-messages.js | 25 ++++---- 4 files changed, 64 insertions(+), 58 deletions(-) diff --git a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js index 446324289a87..741d8ad58ad3 100644 --- a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js +++ b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js @@ -8,8 +8,9 @@ /* eslint-disable no-console */ -import fs from 'fs'; -import path from 'path'; +const fs = require('fs'); +const path = require('path'); +const {LH_ROOT} = require('../../../root.js'); /** * @typedef CtcMessage @@ -63,7 +64,7 @@ import path from 'path'; * @param {Record} messages * @return {Record} */ -export function bakePlaceholders(messages) { +function bakePlaceholders(messages) { /** @type {Record} */ const bakedMessages = {}; @@ -118,13 +119,15 @@ function saveLhlStrings(path, localeStrings) { * @param {string} dir * @return {Array} */ -export function collectAndBakeCtcStrings(dir) { +function collectAndBakeCtcStrings(dir) { const lhlFilenames = []; for (const filename of fs.readdirSync(dir)) { + const fullPath = path.join(dir, filename); + const relativePath = path.relative(LH_ROOT, fullPath); + if (filename.endsWith('.ctc.json')) { - const fullPath = path.join(dir, filename); - if (!process.env.CI) console.log('Baking', fullPath); - const ctcStrings = loadCtcStrings(fullPath); + if (!process.env.CI) console.log('Baking', relativePath); + const ctcStrings = loadCtcStrings(relativePath); const strings = bakePlaceholders(ctcStrings); const outputFile = path.join(dir, path.basename(filename).replace('.ctc', '')); saveLhlStrings(outputFile, strings); @@ -133,3 +136,8 @@ export function collectAndBakeCtcStrings(dir) { } return lhlFilenames; } + +module.exports = { + collectAndBakeCtcStrings, + bakePlaceholders, +}; diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 8e01bf5c6bab..a74a1f7d6e81 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -8,21 +8,17 @@ /* eslint-disable no-console, max-len */ -import fs from 'fs'; -import glob from 'glob'; -import path from 'path'; -import expect from 'expect'; -import tsc from 'typescript'; -import MessageParser from 'intl-messageformat-parser'; -import module from 'module'; -import esMain from 'es-main'; -import Util from '../../../report/renderer/util.js'; -import {collectAndBakeCtcStrings} from './bake-ctc-to-lhl.js'; -import {pruneObsoleteLhlMessages} from './prune-obsolete-lhl-messages.js'; -import {countTranslatedMessages} from './count-translated.js'; -import {LH_ROOT} from '../../../root.js'; - -const require = module.createRequire(import.meta.url); +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); +const expect = require('expect'); +const tsc = require('typescript'); +const MessageParser = require('intl-messageformat-parser').default; +const Util = require('../../../report/renderer/util.js'); +const {collectAndBakeCtcStrings} = require('./bake-ctc-to-lhl.js'); +const {pruneObsoleteLhlMessages} = require('./prune-obsolete-lhl-messages.js'); +const {countTranslatedMessages} = require('./count-translated.js'); +const {LH_ROOT} = require('../../../root.js'); const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; @@ -32,25 +28,19 @@ const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; const foldersWithStrings = [ `${LH_ROOT}/lighthouse-core`, - `${LH_ROOT}/report`, + `${LH_ROOT}/report/renderer`, `${LH_ROOT}/lighthouse-treemap`, path.dirname(require.resolve('lighthouse-stack-packs')) + '/packs', ]; const ignoredPathComponents = [ '**/.git/**', - // TODO(esmodules): remove when some other file with real strings is esm. - '**/scripts/!(i18n)/**', // ignore all scripts *except* test esm file - '**/collect-strings.js', - // '**/scripts/**', - // end TODO + '**/scripts/**', '**/node_modules/!(lighthouse-stack-packs)/**', // ignore all node modules *except* stack packs '**/lighthouse-core/lib/stack-packs.js', '**/test/**', '**/*-test.js', '**/*-renderer.js', - '**/report/clients/*.js', - '**/util-commonjs.js', 'lighthouse-treemap/app/src/main.js', ]; @@ -153,7 +143,7 @@ function parseExampleJsDoc(rawExample) { * @param {Record} examples * @return {IncrementalCtc} */ -export function convertMessageToCtc(lhlMessage, examples = {}) { +function convertMessageToCtc(lhlMessage, examples = {}) { _lhlValidityChecks(lhlMessage); /** @type {IncrementalCtc} */ @@ -424,7 +414,7 @@ function _ctcValidityChecks(icu) { * @param {Record} messages * @return {Record} */ -export function createPsuedoLocaleStrings(messages) { +function createPsuedoLocaleStrings(messages) { /** @type {Record} */ const psuedoLocalizedStrings = {}; for (const [key, ctc] of Object.entries(messages)) { @@ -489,7 +479,7 @@ function getIdentifier(node) { * @param {Record} liveUIStrings The actual imported UIStrings object. * @return {Record} */ -export function parseUIStrings(sourceStr, liveUIStrings) { +function parseUIStrings(sourceStr, liveUIStrings) { const tsAst = tsc.createSourceFile('uistrings', sourceStr, tsc.ScriptTarget.ES2019, true, tsc.ScriptKind.JS); const extractionError = new Error('UIStrings declaration was not extracted correctly by the collect-strings regex.'); @@ -560,8 +550,7 @@ async function collectAllStringsInDir(dir) { } if (!exportedUIStrings) { - console.log({exportVars}); - throw new Error(`UIStrings defined in file but not exported: ${absolutePath}`); + throw new Error('UIStrings defined in file but not exported'); } // just parse the UIStrings substring to avoid ES version issues, save time, etc @@ -617,7 +606,6 @@ function writeStringsToCtcFiles(locale, strings) { * * @param {Record} strings */ -// eslint-disable-next-line no-unused-vars function resolveMessageCollisions(strings) { /** @type {Map>} */ const stringsByMessage = new Map(); @@ -711,9 +699,7 @@ async function main() { Object.assign(strings, moreStrings); } - // TODO: commenting out for now. After PR review, will uncomment and delete the "tmp-esm-strings.js" - // that shows esm working. - // resolveMessageCollisions(strings); + resolveMessageCollisions(strings); writeStringsToCtcFiles('en-US', strings); console.log('Written to disk!', 'en-US.ctc.json'); @@ -744,6 +730,12 @@ async function main() { } // Test if called from the CLI or as a module. -if (esMain(import.meta)) { +if (require.main === module) { main(); } + +module.exports = { + parseUIStrings, + createPsuedoLocaleStrings, + convertMessageToCtc, +}; diff --git a/lighthouse-core/scripts/i18n/count-translated.js b/lighthouse-core/scripts/i18n/count-translated.js index b4033dbd57fe..d64da7eacfec 100644 --- a/lighthouse-core/scripts/i18n/count-translated.js +++ b/lighthouse-core/scripts/i18n/count-translated.js @@ -7,20 +7,21 @@ /** @typedef {import('../../lib/i18n/locales').LhlMessages} LhlMessages */ -import fs from 'fs'; -import path from 'path'; -import glob from 'glob'; -import {LH_ROOT, importJson} from '../../../root.js'; +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); +const {LH_ROOT} = require('../../../root.js'); +const enUsLhlFilename = LH_ROOT + '/lighthouse-core/lib/i18n/locales/en-US.json'; /** @type {LhlMessages} */ -const enUsLhl = importJson('../../../lighthouse-core/lib/i18n/locales/en-US.json', import.meta); +const enUsLhl = JSON.parse(fs.readFileSync(enUsLhlFilename, 'utf8')); /** * Count how many locale files have a translated version of each string found in * the `en-US.json` i18n messages. * @return {{localeCount: number, messageCount: number, translatedCount: number, partiallyTranslatedCount: number, notTranslatedCount: number}} */ -export function countTranslatedMessages() { +function countTranslatedMessages() { // Find all locale files, ignoring self-generated en-US and en-XL, and ctc files. const ignore = [ '**/.ctc.json', @@ -65,3 +66,7 @@ export function countTranslatedMessages() { notTranslatedCount, }; } + +module.exports = { + countTranslatedMessages, +}; diff --git a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js index c8b6151534b5..724ec783ab08 100644 --- a/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js +++ b/lighthouse-core/scripts/i18n/prune-obsolete-lhl-messages.js @@ -5,12 +5,12 @@ */ 'use strict'; -import fs from 'fs'; -import glob from 'glob'; -import path from 'path'; -import MessageParser from 'intl-messageformat-parser'; -import {collectAllCustomElementsFromICU} from '../../lib/i18n/i18n.js'; -import {LH_ROOT} from '../../../root.js'; +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const MessageParser = require('intl-messageformat-parser').default; +const {collectAllCustomElementsFromICU} = require('../../lib/i18n/i18n.js'); +const {LH_ROOT} = require('../../../root.js'); /** @typedef {Record} LhlMessages */ @@ -114,9 +114,8 @@ function getGoldenLocaleArgumentIds(goldenLhl) { * translations should no longer be used, it's up to the author to remove them * (e.g. by picking a new message id). */ -export function pruneObsoleteLhlMessages() { - const goldenLhlPath = new URL('../../lib/i18n/locales/en-US.json', import.meta.url); - const goldenLhl = JSON.parse(fs.readFileSync(goldenLhlPath, 'utf-8')); +function pruneObsoleteLhlMessages() { + const goldenLhl = require('../../lib/i18n/locales/en-US.json'); const goldenLocaleArgumentIds = getGoldenLocaleArgumentIds(goldenLhl); // Find all locale files, ignoring self-generated en-US, en-XL, and ctc files. @@ -144,8 +143,10 @@ export function pruneObsoleteLhlMessages() { } } -// Exported for testing. -export { +module.exports = { + pruneObsoleteLhlMessages, + + // Exported for testing. getGoldenLocaleArgumentIds, - pruneLocale + pruneLocale, }; From 1f386b78eb06fb503cff82ef00ab09b31bcd0be5 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:12:08 -0700 Subject: [PATCH 51/71] rm --- lighthouse-core/scripts/i18n/tmp-esm-strings.js | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 lighthouse-core/scripts/i18n/tmp-esm-strings.js diff --git a/lighthouse-core/scripts/i18n/tmp-esm-strings.js b/lighthouse-core/scripts/i18n/tmp-esm-strings.js deleted file mode 100644 index f56e7cbf659a..000000000000 --- a/lighthouse-core/scripts/i18n/tmp-esm-strings.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license Copyright 2018 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'; - -// TODO(esmodules): remove when some other file with real strings is esm. - -export const UIStrings = { - /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */ - varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.', -}; From 9f50d9dd3ac07c2e0a4b91f3d8daad337fbd3677 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:12:34 -0700 Subject: [PATCH 52/71] update --- lighthouse-core/lib/i18n/locales/en-US.json | 3 --- lighthouse-core/lib/i18n/locales/en-XL.json | 3 --- lighthouse-core/scripts/i18n/package.json | 3 --- 3 files changed, 9 deletions(-) delete mode 100644 lighthouse-core/scripts/i18n/package.json diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index d0f3a217f2a7..d3b91964c626 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1955,9 +1955,6 @@ "lighthouse-core/lib/lh-error.js | urlInvalid": { "message": "The URL you have provided appears to be invalid." }, - "lighthouse-core/scripts/i18n/tmp-esm-strings.js | varianceDisclaimer": { - "message": "Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics." - }, "lighthouse-treemap/app/src/util.js | allLabel": { "message": "All" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index b6ed573d4515..d068a57c3511 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1955,9 +1955,6 @@ "lighthouse-core/lib/lh-error.js | urlInvalid": { "message": "T̂h́ê ÚR̂Ĺ ŷóû h́âv́ê ṕr̂óv̂íd̂éd̂ áp̂ṕêár̂ś t̂ó b̂é îńv̂ál̂íd̂." }, - "lighthouse-core/scripts/i18n/tmp-esm-strings.js | varianceDisclaimer": { - "message": "V̂ál̂úêś âŕê éŝt́îḿât́êd́ âńd̂ ḿâý v̂ár̂ý. T̂h́ê [ṕêŕf̂ór̂ḿâńĉé ŝćôŕê íŝ ćâĺĉúl̂át̂éd̂](https://web.dev/performance-scoring/) d́îŕêćt̂ĺŷ f́r̂óm̂ t́ĥéŝé m̂ét̂ŕîćŝ." - }, "lighthouse-treemap/app/src/util.js | allLabel": { "message": "Âĺl̂" }, diff --git a/lighthouse-core/scripts/i18n/package.json b/lighthouse-core/scripts/i18n/package.json deleted file mode 100644 index aead43de364c..000000000000 --- a/lighthouse-core/scripts/i18n/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} \ No newline at end of file From 642289b55d607c2f62cbdaa53ad527a7e83093a7 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:14:04 -0700 Subject: [PATCH 53/71] revertests --- lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js | 2 +- lighthouse-core/test/scripts/i18n/collect-strings-test.js | 2 +- .../test/scripts/i18n/prune-obsolete-lhl-messages-test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js b/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js index e232341507fe..16d2547f495f 100644 --- a/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js +++ b/lighthouse-core/test/scripts/i18n/bake-ctc-to-lhl-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -import * as bakery from '../../../scripts/i18n/bake-ctc-to-lhl.js'; +const bakery = require('../../../scripts/i18n/bake-ctc-to-lhl.js'); describe('Baking Placeholders', () => { it('passthroughs a basic message unchanged', () => { diff --git a/lighthouse-core/test/scripts/i18n/collect-strings-test.js b/lighthouse-core/test/scripts/i18n/collect-strings-test.js index 85dfdbb74e93..181e7720f4db 100644 --- a/lighthouse-core/test/scripts/i18n/collect-strings-test.js +++ b/lighthouse-core/test/scripts/i18n/collect-strings-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -import * as collect from '../../../scripts/i18n/collect-strings.js'; +const collect = require('../../../scripts/i18n/collect-strings.js'); function evalJustUIStrings(justUIStrings) { return Function(`'use strict'; ${justUIStrings} return UIStrings;`)(); diff --git a/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js b/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js index ac4fd8f66b5d..fd216675b9c1 100644 --- a/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js +++ b/lighthouse-core/test/scripts/i18n/prune-obsolete-lhl-messages-test.js @@ -7,7 +7,7 @@ /* eslint-env jest */ -import * as pruneObsoleteLhlMessages from '../../../scripts/i18n/prune-obsolete-lhl-messages.js'; +const pruneObsoleteLhlMessages = require('../../../scripts/i18n/prune-obsolete-lhl-messages.js'); /** * Prune `localeLhl` based on `goldenLhl`. From 3e9f32e8de4b6366806b9a5db82cf4c0ea309ad4 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:15:13 -0700 Subject: [PATCH 54/71] more revert --- lighthouse-core/test/scripts/i18n/package.json | 1 - package.json | 4 +--- root.js | 17 +---------------- tsconfig.json | 4 +--- types/es-main.d.ts | 10 ---------- yarn.lock | 5 ----- 6 files changed, 3 insertions(+), 38 deletions(-) delete mode 100644 lighthouse-core/test/scripts/i18n/package.json delete mode 100644 types/es-main.d.ts diff --git a/lighthouse-core/test/scripts/i18n/package.json b/lighthouse-core/test/scripts/i18n/package.json deleted file mode 100644 index 1632c2c4df68..000000000000 --- a/lighthouse-core/test/scripts/i18n/package.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "module"} \ No newline at end of file diff --git a/package.json b/package.json index 15928e9cdf9b..0413e87e7734 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "yarn build-report && node ./lighthouse-cli/index.js", - "jest": "node --experimental-vm-modules node_modules/.bin/jest", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "jest \"clients/\"", @@ -43,7 +42,7 @@ "test-legacy-javascript": "bash lighthouse-core/scripts/test-legacy-javascript.sh", "test-docs": "yarn --cwd docs/recipes/auth && jest docs/recipes/integration-test && yarn --cwd docs/recipes/custom-gatherer-puppeteer test", "test-proto": "yarn compile-proto && yarn build-proto-roundtrip", - "unit-core": "yarn jest \"lighthouse-core\"", + "unit-core": "jest \"lighthouse-core\"", "unit-cli": "jest \"lighthouse-cli/\"", "unit-report": "jest \"report/\"", "unit-treemap": "jest \"lighthouse-treemap/.*-test.js\"", @@ -134,7 +133,6 @@ "cross-env": "^7.0.2", "csv-validator": "^0.0.3", "devtools-protocol": "0.0.863986", - "es-main": "^1.0.2", "eslint": "^7.23.0", "eslint-config-google": "^0.9.1", "eslint-plugin-local-rules": "0.1.0", diff --git a/root.js b/root.js index e90666a305aa..ecf261013fbb 100644 --- a/root.js +++ b/root.js @@ -5,19 +5,4 @@ */ 'use strict'; -const fs = require('fs'); -const url = require('url'); - -/** - * @param {string} path - * @param {ImportMeta} importMeta - */ -function importJson(path, importMeta) { - const json = fs.readFileSync(url.fileURLToPath(new URL(path, importMeta.url)), 'utf-8'); - return JSON.parse(json); -} - -module.exports = { - LH_ROOT: __dirname, - importJson, -}; +module.exports.LH_ROOT = __dirname; diff --git a/tsconfig.json b/tsconfig.json index 319cc8ad0e00..c2654b7de1ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,7 @@ { "compilerOptions": { "noEmit": true, - "module": "ES2020", - "moduleResolution": "node", + "module": "commonjs", "target": "ES2020", "allowJs": true, "checkJs": true, @@ -11,7 +10,6 @@ // "noErrorTruncation": true, "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, "diagnostics": true }, "include": [ diff --git a/types/es-main.d.ts b/types/es-main.d.ts deleted file mode 100644 index 8f3235ee0d1b..000000000000 --- a/types/es-main.d.ts +++ /dev/null @@ -1,10 +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. - */ - -declare function esMain(importMeta: ImportMeta): boolean; -declare module 'es-main' { - export = esMain; -} diff --git a/yarn.lock b/yarn.lock index 6e99942b5ccd..119315badee7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3174,11 +3174,6 @@ es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.0" -es-main@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/es-main/-/es-main-1.0.2.tgz#c9030d78796f609f865b66f4125a78d77fec3de3" - integrity sha512-LLgW8Cby/FiyQygrI23q2EswulHiDKoyjWlDRgTGXjQ3iRim2R26VfoehpxI5oKRXSNams3L/80KtggoUdxdDQ== - es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" From a570796759f42dc1c1e46c4a73c71ed029ae3b69 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 1 Jul 2021 21:27:40 -0700 Subject: [PATCH 55/71] update --- .../scripts/i18n/collect-strings.js | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index eb980bdf61be..860cd78eb178 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -8,35 +8,17 @@ /* eslint-disable no-console, max-len */ -<<<<<<< HEAD const fs = require('fs'); const glob = require('glob'); const path = require('path'); const expect = require('expect'); const tsc = require('typescript'); const MessageParser = require('intl-messageformat-parser').default; -const Util = require('../../../report/renderer/util.js'); +const Util = require('../../../lighthouse-core/util-commonjs.js'); const {collectAndBakeCtcStrings} = require('./bake-ctc-to-lhl.js'); const {pruneObsoleteLhlMessages} = require('./prune-obsolete-lhl-messages.js'); const {countTranslatedMessages} = require('./count-translated.js'); const {LH_ROOT} = require('../../../root.js'); -======= -import fs from 'fs'; -import glob from 'glob'; -import path from 'path'; -import expect from 'expect'; -import tsc from 'typescript'; -import MessageParser from 'intl-messageformat-parser'; -import module from 'module'; -import esMain from 'es-main'; -import {Util} from '../../../report/renderer/util.js'; -import {collectAndBakeCtcStrings} from './bake-ctc-to-lhl.js'; -import {pruneObsoleteLhlMessages} from './prune-obsolete-lhl-messages.js'; -import {countTranslatedMessages} from './count-translated.js'; -import {LH_ROOT} from '../../../root.js'; - -const require = module.createRequire(import.meta.url); ->>>>>>> 2e013444f7eaf7d3b1249f5166a607274aa7c2c9 const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s; @@ -59,6 +41,7 @@ const ignoredPathComponents = [ '**/test/**', '**/*-test.js', '**/*-renderer.js', + '**/util-commonjs.js', 'lighthouse-treemap/app/src/main.js', ]; From a4488b49da66aa07b9098a5f93c5d3a2d76be3dc Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 15:19:57 -0700 Subject: [PATCH 56/71] fix async test --- lighthouse-core/test/lib/page-functions-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index 3e9a14b3abb8..09f45b7aeaf1 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -18,7 +18,7 @@ describe('Page Functions', () => { const url = 'http://www.example.com'; let dom; - beforeAll(() => { + beforeAll(async () => { // TODO(esmodules): remove when this file is esm. DOM = await import('../../../report/renderer/dom.js'); const {document, ShadowRoot, Node, HTMLElement} = new jsdom.JSDOM('', {url}).window; From fa1c6dd128385af12f222e7332ccd64c8ca9f5a7 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 15:23:27 -0700 Subject: [PATCH 57/71] yarn jest --- lighthouse-core/test/lib/page-functions-test.js | 2 +- package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index 09f45b7aeaf1..cd26f6fe0853 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -20,7 +20,7 @@ describe('Page Functions', () => { beforeAll(async () => { // TODO(esmodules): remove when this file is esm. - DOM = await import('../../../report/renderer/dom.js'); + DOM = (await import('../../../report/renderer/dom.js')).DOM; const {document, ShadowRoot, Node, HTMLElement} = new jsdom.JSDOM('', {url}).window; global.ShadowRoot = ShadowRoot; global.Node = Node; diff --git a/package.json b/package.json index 63e03e3e746e..c0e17f2ac856 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,14 @@ "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "yarn jest \"$PWD/clients/\"", - "test-viewer": "yarn unit-viewer && jest lighthouse-viewer/test/viewer-test-pptr.js", - "test-treemap": "yarn unit-treemap && jest lighthouse-treemap/test/treemap-test-pptr.js", + "test-viewer": "yarn unit-viewer && yarn jest lighthouse-viewer/test/viewer-test-pptr.js", + "test-treemap": "yarn unit-treemap && yarn jest lighthouse-treemap/test/treemap-test-pptr.js", "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh", "test-legacy-javascript": "bash lighthouse-core/scripts/test-legacy-javascript.sh", - "test-docs": "yarn --cwd docs/recipes/auth && jest docs/recipes/integration-test && yarn --cwd docs/recipes/custom-gatherer-puppeteer test", + "test-docs": "yarn --cwd docs/recipes/auth && yarn jest docs/recipes/integration-test && yarn --cwd docs/recipes/custom-gatherer-puppeteer test", "test-proto": "yarn compile-proto && yarn build-proto-roundtrip", - "unit-core": "jest \"lighthouse-core\"", - "unit-cli": "jest \"lighthouse-cli/\"", + "unit-core": "yarn jest \"lighthouse-core\"", + "unit-cli": "yarn jest \"lighthouse-cli/\"", "unit-report": "yarn jest \"report/\"", "unit-treemap": "jest \"lighthouse-treemap/.*-test.js\"", "unit-viewer": "jest \"lighthouse-viewer/.*-test.js\"", From f9c508e54ab3d390db1ffe902092e82cd08f90bc Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 15:33:09 -0700 Subject: [PATCH 58/71] copy util again --- lighthouse-core/util-commonjs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse-core/util-commonjs.js b/lighthouse-core/util-commonjs.js index e7fb08bed0b7..abf55cafb07a 100644 --- a/lighthouse-core/util-commonjs.js +++ b/lighthouse-core/util-commonjs.js @@ -376,7 +376,7 @@ class Util { /** * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com). * @param {string|URL} url hostname or URL object - * @returns {string} + * @return {string} */ static getRootDomain(url) { const hostname = Util.createOrReturnURL(url).hostname; @@ -582,7 +582,7 @@ Util.UIStrings = { /** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */ thirdPartyResourcesLabel: 'Show 3rd-party resources', - /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */ + /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */ viewTreemapLabel: 'View Treemap', /** Option in a dropdown menu that opens a small, summary report in a print dialog. */ From ebee37176437e7fe353742bc010a9c867a557464 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 15:49:03 -0700 Subject: [PATCH 59/71] fix for windows --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0e17f2ac856..f858514419b7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "yarn build-report --only-standalone && node ./lighthouse-cli/index.js", - "jest": "node --experimental-vm-modules node_modules/.bin/jest", + "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "yarn jest \"$PWD/clients/\"", From c6a6ef8e39f69f46f72429d1cda7b01e0cb49cb4 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 17:19:53 -0700 Subject: [PATCH 60/71] tmp no fail fast --- .github/workflows/unit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 5d922d89f2ab..38cb54653824 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -9,6 +9,7 @@ jobs: # `unit` includes just unit and proto tests. unit: strategy: + fail-fast: false matrix: node: ['12', '14', '16'] runs-on: ubuntu-latest From 0f0fcee73b6b26036531db63645f92be0d18fed3 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 17:27:21 -0700 Subject: [PATCH 61/71] tmp: debuging with runInBand --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f858514419b7..e13348cf0c56 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "yarn build-report --only-standalone && node ./lighthouse-cli/index.js", - "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", + "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "yarn jest \"$PWD/clients/\"", From a983e75838a88365cc5c23dbbb2f05f81342473c Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 8 Jul 2021 17:35:26 -0700 Subject: [PATCH 62/71] lets get that stacktrace --- lighthouse-core/test/gather/driver-test.js | 5 +++++ package.json | 1 + yarn.lock | 25 ++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js index 8af58fb2cc85..4bb1da2b45de 100644 --- a/lighthouse-core/test/gather/driver-test.js +++ b/lighthouse-core/test/gather/driver-test.js @@ -5,6 +5,11 @@ */ 'use strict'; +const SegfaultHandler = require('segfault-handler'); +SegfaultHandler.registerHandler("crash.log", (...args) => { + console.log(JSON.stringify(args)); +}); + const Driver = require('../../gather/driver.js'); const Connection = require('../../gather/connections/connection.js'); const {protocolGetVersionResponse} = require('./fake-driver.js'); diff --git a/package.json b/package.json index e13348cf0c56..287a7cfac8e4 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "rollup": "^2.50.6", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-terser": "^7.0.2", + "segfault-handler": "^1.3.0", "tabulator-tables": "^4.9.3", "terser": "^5.3.8", "typed-query-selector": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 66f880582e3a..227eabf51a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1666,6 +1666,13 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bindings@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -3667,6 +3674,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filename-reserved-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" @@ -6058,6 +6070,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nan@^2.14.0: + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -7295,6 +7312,14 @@ scope-analyzer@^2.0.1: estree-is-function "^1.0.0" get-assigned-identifiers "^1.1.0" +segfault-handler@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/segfault-handler/-/segfault-handler-1.3.0.tgz#054bc847832fa14f218ba6a79e42877501c8870e" + integrity sha512-p7kVHo+4uoYkr0jmIiTBthwV5L2qmWtben/KDunDZ834mbos+tY+iO0//HpAJpOFSQZZ+wxKWuRo4DxV02B7Lg== + dependencies: + bindings "^1.2.1" + nan "^2.14.0" + semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" From 7f5839e6fc9b4b628fc87a9417ff7c92cf9f4f7c Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 9 Jul 2021 13:43:56 -0700 Subject: [PATCH 63/71] node crash workaround --- lighthouse-core/test/gather/driver-test.js | 5 ---- .../test/lib/page-functions-test.js | 17 +++++++++---- package.json | 3 +-- yarn.lock | 25 ------------------- 4 files changed, 13 insertions(+), 37 deletions(-) diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js index 4bb1da2b45de..8af58fb2cc85 100644 --- a/lighthouse-core/test/gather/driver-test.js +++ b/lighthouse-core/test/gather/driver-test.js @@ -5,11 +5,6 @@ */ 'use strict'; -const SegfaultHandler = require('segfault-handler'); -SegfaultHandler.registerHandler("crash.log", (...args) => { - console.log(JSON.stringify(args)); -}); - const Driver = require('../../gather/driver.js'); const Connection = require('../../gather/connections/connection.js'); const {protocolGetVersionResponse} = require('./fake-driver.js'); diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index cd26f6fe0853..224c5612f4fb 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -5,10 +5,14 @@ */ 'use strict'; -const assert = require('assert').strict; -const jsdom = require('jsdom'); -const pageFunctions = require('../../lib/page-functions.js'); - +// TODO(esmodules): remove when this file is esm. + +/** @type {import('assert').strict} */ +let assert; +/** @type {import('jsdom').strict} */ +let jsdom; +/** @type {import('../../lib/page-functions.js')} */ +let pageFunctions; /** @type {import('../../../report/renderer/dom.js').DOM} */ let DOM; @@ -19,8 +23,11 @@ describe('Page Functions', () => { let dom; beforeAll(async () => { - // TODO(esmodules): remove when this file is esm. + assert = (await import('assert')).strict; + jsdom = await import('jsdom'); + pageFunctions = (await import('../../lib/page-functions.js')).default; DOM = (await import('../../../report/renderer/dom.js')).DOM; + const {document, ShadowRoot, Node, HTMLElement} = new jsdom.JSDOM('', {url}).window; global.ShadowRoot = ShadowRoot; global.Node = Node; diff --git a/package.json b/package.json index 287a7cfac8e4..f858514419b7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js", "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "yarn build-report --only-standalone && node ./lighthouse-cli/index.js", - "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand", + "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", "test-bundle": "yarn smoke --runner bundle -j=1 --retries=2 --invert-match forms", "test-clients": "yarn jest \"$PWD/clients/\"", @@ -158,7 +158,6 @@ "rollup": "^2.50.6", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-terser": "^7.0.2", - "segfault-handler": "^1.3.0", "tabulator-tables": "^4.9.3", "terser": "^5.3.8", "typed-query-selector": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 227eabf51a1d..66f880582e3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1666,13 +1666,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bindings@^1.2.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -3674,11 +3667,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - filename-reserved-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" @@ -6070,11 +6058,6 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.14.0: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -7312,14 +7295,6 @@ scope-analyzer@^2.0.1: estree-is-function "^1.0.0" get-assigned-identifiers "^1.1.0" -segfault-handler@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/segfault-handler/-/segfault-handler-1.3.0.tgz#054bc847832fa14f218ba6a79e42877501c8870e" - integrity sha512-p7kVHo+4uoYkr0jmIiTBthwV5L2qmWtben/KDunDZ834mbos+tY+iO0//HpAJpOFSQZZ+wxKWuRo4DxV02B7Lg== - dependencies: - bindings "^1.2.1" - nan "^2.14.0" - semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" From 3b8f9bbbeb532709db678315100c19014a63e777 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 9 Jul 2021 13:52:42 -0700 Subject: [PATCH 64/71] comment --- lighthouse-core/test/lib/page-functions-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js index 224c5612f4fb..fe8a09321062 100644 --- a/lighthouse-core/test/lib/page-functions-test.js +++ b/lighthouse-core/test/lib/page-functions-test.js @@ -5,7 +5,9 @@ */ 'use strict'; -// TODO(esmodules): remove when this file is esm. +// TODO(esmodules): Node 14, 16 crash with `--experimental-vm-modules` if require and import +// are used in the same test file. +// See https://github.com/GoogleChrome/lighthouse/pull/12702#issuecomment-876832620 /** @type {import('assert').strict} */ let assert; From b73d2c64dd19307eefe69651dca65a2842b67906 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Mon, 12 Jul 2021 17:48:22 -0700 Subject: [PATCH 65/71] update --- .github/workflows/unit.yml | 1 - report/clients/package.json | 5 ++++- report/renderer/package.json | 5 ++++- report/test/clients/package.json | 5 ++++- report/test/renderer/package.json | 5 ++++- types/es-main.d.ts | 9 --------- 6 files changed, 16 insertions(+), 14 deletions(-) delete mode 100644 types/es-main.d.ts diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 38cb54653824..5d922d89f2ab 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -9,7 +9,6 @@ jobs: # `unit` includes just unit and proto tests. unit: strategy: - fail-fast: false matrix: node: ['12', '14', '16'] runs-on: ubuntu-latest diff --git a/report/clients/package.json b/report/clients/package.json index 1632c2c4df68..bd346284783c 100644 --- a/report/clients/package.json +++ b/report/clients/package.json @@ -1 +1,4 @@ -{"type": "module"} \ No newline at end of file +{ + "type": "module", + "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module" +} \ No newline at end of file diff --git a/report/renderer/package.json b/report/renderer/package.json index 1632c2c4df68..bd346284783c 100644 --- a/report/renderer/package.json +++ b/report/renderer/package.json @@ -1 +1,4 @@ -{"type": "module"} \ No newline at end of file +{ + "type": "module", + "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module" +} \ No newline at end of file diff --git a/report/test/clients/package.json b/report/test/clients/package.json index 1632c2c4df68..bd346284783c 100644 --- a/report/test/clients/package.json +++ b/report/test/clients/package.json @@ -1 +1,4 @@ -{"type": "module"} \ No newline at end of file +{ + "type": "module", + "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module" +} \ No newline at end of file diff --git a/report/test/renderer/package.json b/report/test/renderer/package.json index 1632c2c4df68..bd346284783c 100644 --- a/report/test/renderer/package.json +++ b/report/test/renderer/package.json @@ -1 +1,4 @@ -{"type": "module"} \ No newline at end of file +{ + "type": "module", + "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module" +} \ No newline at end of file diff --git a/types/es-main.d.ts b/types/es-main.d.ts deleted file mode 100644 index bbc6ec3866f7..000000000000 --- a/types/es-main.d.ts +++ /dev/null @@ -1,9 +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. - */ - -declare module 'es-main' { - export default function(meta: ImportMeta): boolean; -} From 46ca2604e6530ce7102f44eb88c2897090edc5c3 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Jul 2021 11:45:19 -0700 Subject: [PATCH 66/71] comment --- report/renderer/report-renderer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js index 7ad656be4b34..7a8523edfc8b 100644 --- a/report/renderer/report-renderer.js +++ b/report/renderer/report-renderer.js @@ -15,6 +15,7 @@ * limitations under the License. * * Dummy text for ensuring report robustness: pre$`post %%LIGHTHOUSE_JSON%% + * (this is handled by terser) */ 'use strict'; From baf5e55e9e9a365add41e103281455ffb68ab543 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Jul 2021 12:03:10 -0700 Subject: [PATCH 67/71] build: run build-report for vercel deployment --- lighthouse-core/scripts/dogfood-lhci.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lighthouse-core/scripts/dogfood-lhci.sh b/lighthouse-core/scripts/dogfood-lhci.sh index 2e75f55a2970..226aff69256e 100755 --- a/lighthouse-core/scripts/dogfood-lhci.sh +++ b/lighthouse-core/scripts/dogfood-lhci.sh @@ -36,6 +36,7 @@ if ! echo "$CHANGED_FILES" | grep -E 'report|lhci' > /dev/null; then fi # Generate HTML reports in ./dist/now/ +yarn build-report yarn now-build # Install LHCI From 36793132a9dac67e38fa6b913a5abd5d26972dd3 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Jul 2021 12:08:21 -0700 Subject: [PATCH 68/71] fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f858514419b7..fbe56d22e677 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "fast": "yarn start --preset=desktop --throttlingMethod=provided", "deploy-treemap": "yarn build-treemap --deploy", "deploy-viewer": "yarn build-viewer --deploy", - "now-build": "node lighthouse-core/scripts/build-report-for-autodeployment.js && yarn build-viewer && yarn build-treemap && cp -r dist/gh-pages dist/now/gh-pages", + "now-build": "yarn build-report && node lighthouse-core/scripts/build-report-for-autodeployment.js && yarn build-viewer && yarn build-treemap && cp -r dist/gh-pages dist/now/gh-pages", "dogfood-lhci": "./lighthouse-core/scripts/dogfood-lhci.sh", "timing-trace": "node lighthouse-core/scripts/generate-timing-trace.js", "changelog": "conventional-changelog --config ./build/changelog-generator/index.js --infile changelog.md --same-file", From c7dfe920df3e9f93f32c12add3c26d58ded602d4 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Jul 2021 17:10:25 -0700 Subject: [PATCH 69/71] fix standalone bundle --- report/clients/standalone.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/report/clients/standalone.js b/report/clients/standalone.js index b66066f1c8b8..2305b4b66403 100644 --- a/report/clients/standalone.js +++ b/report/clients/standalone.js @@ -63,3 +63,5 @@ function __initLighthouseReport__() { } }); } + +window.__initLighthouseReport__ = __initLighthouseReport__; From 547afdf3c6963b1c44d82837fb4e273c4ccc2058 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 15 Jul 2021 12:12:58 -0700 Subject: [PATCH 70/71] add comment for cdt --- .../devtools/lighthouse/resources/lighthouse-basic.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/resources/lighthouse-basic.css b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/resources/lighthouse-basic.css index fb6a572eb458..b6cae13ed941 100644 --- a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/resources/lighthouse-basic.css +++ b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/resources/lighthouse-basic.css @@ -1,3 +1,9 @@ +/* + * Copyright 2021 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + body { font-size: 10px; } From 88a86fe9bd59eb010ade73bd255dfda9d5301995 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 15 Jul 2021 12:32:08 -0700 Subject: [PATCH 71/71] fix un upstreamed psi fix --- report/clients/psi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/report/clients/psi.js b/report/clients/psi.js index e061c7ab26e0..c243f580a7af 100644 --- a/report/clients/psi.js +++ b/report/clients/psi.js @@ -54,6 +54,7 @@ export function prepareLabData(LHResult, document) { ...reportLHR.i18n.rendererFormattedStrings, }); Util.i18n = i18n; + Util.reportJson = reportLHR; const perfCategory = reportLHR.categories.performance; if (!perfCategory) throw new Error(`No performance category. Can't make lab data section`);