blob: 71b16678cb022d5801b1830489de334c84bf8e67 [file] [log] [blame]
/**
* @license Copyright 2018 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';
/* globals self, Util, CategoryRenderer */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./report-renderer.js').CategoryJSON} CategoryJSON */
/** @typedef {import('./report-renderer.js').GroupJSON} GroupJSON */
/** @typedef {import('./report-renderer.js').AuditJSON} AuditJSON */
/** @typedef {import('./details-renderer.js').OpportunitySummary} OpportunitySummary */
/** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {AuditJSON} 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 = Util.formatDisplayValue(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 {AuditJSON} audit
* @param {number} index
* @param {number} scale
* @return {Element}
*/
_renderOpportunity(audit, index, scale) {
const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.populateAuditValues(audit, index, oppTmpl);
element.id = audit.result.id;
const details = audit.result.details;
if (!details) {
return element;
}
const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
return element;
}
// Overwrite the displayValue with opportunity's wastedMs
const displayEl = this.dom.find('.lh-audit__display-text', element);
const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`;
this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
// Set [title] tooltips
if (audit.result.displayValue) {
const displayValue = Util.formatDisplayValue(audit.result.displayValue);
this.dom.find('.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
* Opportunties with an error won't have a summary object, so MIN_VALUE is returned to keep any
* erroring opportunities last in sort order.
* @param {AuditJSON} audit
* @return {number}
*/
_getWastedMs(audit) {
if (
audit.result.details &&
audit.result.details.summary &&
typeof audit.result.details.summary.wastedMs === 'number'
) {
return audit.result.details.summary.wastedMs;
} else {
return Number.MIN_VALUE;
}
}
/**
* @param {CategoryJSON} category
* @param {Object<string, GroupJSON>} groups
* @return {Element}
* @override
*/
render(category, groups) {
const element = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category));
// Metrics
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
const metricAuditsEl = this.renderAuditGroup(groups['metrics'], {expandable: false});
const keyMetrics = metricAudits.filter(a => a.weight >= 3);
const otherMetrics = metricAudits.filter(a => a.weight < 3);
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metric-container');
const metricsColumn1El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
const metricsColumn2El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
keyMetrics.forEach(item => {
metricsColumn1El.appendChild(this._renderMetric(item));
});
otherMetrics.forEach(item => {
metricsColumn2El.appendChild(this._renderMetric(item));
});
const estValuesEl = this.dom.createChildOf(metricsColumn2El, 'div',
'lh-metrics__disclaimer lh-metrics__disclaimer');
estValuesEl.textContent = 'Values are estimated and may vary.';
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);
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));
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'], {expandable: false});
const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
groupEl.appendChild(headerEl);
opportunityAudits.forEach((item, i) =>
groupEl.appendChild(this._renderOpportunity(item, i, scale)));
groupEl.classList.add('lh-audit-group--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'], {expandable: false});
diagnosticAudits.forEach((item, i) => groupEl.appendChild(this.renderAudit(item, i)));
groupEl.classList.add('lh-audit-group--diagnostics');
element.appendChild(groupEl);
}
// Passed audits
const passedElements = category.auditRefs
.filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
Util.showAsPassed(audit.result))
.map((audit, i) => this.renderAudit(audit, i));
if (!passedElements.length) return element;
const passedElem = this.renderPassedAuditsSection(passedElements);
element.appendChild(passedElem);
return element;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = PerformanceCategoryRenderer;
} else {
self.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
}