/** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; /** * @fileoverview 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%% */ /** @typedef {import('./dom.js')} DOM */ /* globals self, Util, DetailsRenderer, CategoryRenderer, PerformanceCategoryRenderer, PwaCategoryRenderer */ 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) { // Mutate the UIStrings if necessary (while saving originals) const originalUIStrings = JSON.parse(JSON.stringify(Util.UIStrings)); this._dom.setLighthouseChannel(result.configSettings.channel || 'unknown'); const report = Util.prepareReportResult(result); container.textContent = ''; // Remove previous report. container.appendChild(this._renderReport(report)); // put the UIStrings back into original state Util.updateAllUIStrings(originalUIStrings); 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 = /** @type {HTMLAnchorElement} */ (this._dom.find('.lh-topbar__url', el)); metadataUrl.href = metadataUrl.textContent = 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); /** @type {HTMLDivElement} */ (placeholder.parentNode).replaceChild(domFragment, placeholder); 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'; const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {}); [ {name: 'URL', description: report.finalUrl}, {name: 'Fetch time', description: Util.formatDateTime(report.fetchTime)}, ...envValues, {name: 'User agent (host)', description: report.userAgent}, {name: 'User agent (network)', description: report.environment && report.environment.networkUserAgent}, {name: 'CPU/Memory Power', description: report.environment && report.environment.benchmarkIndex.toFixed(0)}, ].forEach(runtime => { if (!runtime.description) return; 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', 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.UIStrings.toplevelWarningsMessage; const warnings = this._dom.find('ul', container); for (const warningString of report.runWarnings) { const warning = warnings.appendChild(this._dom.createElement('li')); warning.textContent = 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 detailsRenderer = new DetailsRenderer(this._dom); 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)); return reportFragment; } } /** @type {LH.I18NRendererStrings} */ ReportRenderer._UIStringsStash = {}; if (typeof module !== 'undefined' && module.exports) { module.exports = ReportRenderer; } else { self.ReportRenderer = ReportRenderer; }