Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

report(category): enable all categories to show audit groups #4278

Merged
merged 12 commits into from
Feb 21, 2018
307 changes: 63 additions & 244 deletions lighthouse-core/report/v2/renderer/category-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ class CategoryRenderer {
* @param {!DetailsRenderer} detailsRenderer
*/
constructor(dom, detailsRenderer) {
/** @private {!DOM} */
/** @protected {!DOM} */
this._dom = dom;
/** @private {!DetailsRenderer} */
/** @protected {!DetailsRenderer} */
this._detailsRenderer = detailsRenderer;
/** @private {!Document|!Element} */
/** @protected {!Document|!Element} */
this._templateContext = this._dom.document();

this._detailsRenderer.setTemplateContext(this._templateContext);
Expand Down Expand Up @@ -108,96 +108,6 @@ class CategoryRenderer {
return element;
}

/**
* @param {!ReportRenderer.AuditJSON} audit
* @param {number} scale
* @return {!Element}
*/
_renderTimelineMetricAudit(audit, scale) {
const tmpl = this._dom.cloneTemplate('#tmpl-lh-timeline-metric', this._templateContext);
const element = this._dom.find('.lh-timeline-metric', tmpl);
element.classList.add(`lh-timeline-metric--${Util.calculateRating(audit.score)}`);

const titleEl = this._dom.find('.lh-timeline-metric__title', tmpl);
titleEl.textContent = audit.result.description;

const valueEl = this._dom.find('.lh-timeline-metric__value', tmpl);
valueEl.textContent = audit.result.displayValue;

const descriptionEl = this._dom.find('.lh-timeline-metric__description', tmpl);
descriptionEl.appendChild(this._dom.convertMarkdownLinkSnippets(audit.result.helpText));

if (typeof audit.result.rawValue !== 'number') {
const debugStrEl = this._dom.createChildOf(element, 'div', 'lh-debug');
debugStrEl.textContent = audit.result.debugString || 'Report error: no metric information';
return element;
}

const sparklineBarEl = this._dom.find('.lh-sparkline__bar', tmpl);
sparklineBarEl.style.width = `${audit.result.rawValue / scale * 100}%`;

return element;
}

/**
* @param {!ReportRenderer.AuditJSON} audit
* @param {number} scale
* @return {!Element}
*/
_renderPerfHintAudit(audit, scale) {
const extendedInfo = /** @type {!CategoryRenderer.PerfHintExtendedInfo}
*/ (audit.result.extendedInfo);
const tooltipAttrs = {title: audit.result.displayValue};

const element = this._dom.createElement('details', [
'lh-perf-hint',
`lh-perf-hint--${Util.calculateRating(audit.score)}`,
'lh-expandable-details',
].join(' '));

const summary = this._dom.createChildOf(element, 'summary', 'lh-perf-hint__summary ' +
'lh-expandable-details__summary');
const titleEl = this._dom.createChildOf(summary, 'div', 'lh-perf-hint__title');
titleEl.textContent = audit.result.description;

this._dom.createChildOf(summary, 'div', 'lh-toggle-arrow', {title: 'See resources'});

if (!extendedInfo || typeof audit.result.rawValue !== 'number') {
const debugStrEl = this._dom.createChildOf(summary, 'div', 'lh-debug');
debugStrEl.textContent = audit.result.debugString || 'Report error: no extended information';
return element;
}

const sparklineContainerEl = this._dom.createChildOf(summary, 'div', 'lh-perf-hint__sparkline',
tooltipAttrs);
const sparklineEl = this._dom.createChildOf(sparklineContainerEl, 'div', 'lh-sparkline');
const sparklineBarEl = this._dom.createChildOf(sparklineEl, 'div', 'lh-sparkline__bar');
sparklineBarEl.style.width = audit.result.rawValue / scale * 100 + '%';

const statsEl = this._dom.createChildOf(summary, 'div', 'lh-perf-hint__stats', tooltipAttrs);
const statsMsEl = this._dom.createChildOf(statsEl, 'div', 'lh-perf-hint__primary-stat');
statsMsEl.textContent = Util.formatMilliseconds(audit.result.rawValue);

if (extendedInfo.value.wastedKb) {
const statsKbEl = this._dom.createChildOf(statsEl, 'div', 'lh-perf-hint__secondary-stat');
statsKbEl.textContent = Util.formatNumber(extendedInfo.value.wastedKb) + ' KB';
}

const descriptionEl = this._dom.createChildOf(element, 'div', 'lh-perf-hint__description');
descriptionEl.appendChild(this._dom.convertMarkdownLinkSnippets(audit.result.helpText));

if (audit.result.debugString) {
const debugStrEl = this._dom.createChildOf(summary, 'div', 'lh-debug');
debugStrEl.textContent = audit.result.debugString;
}

if (audit.result.details) {
element.appendChild(this._detailsRenderer.render(audit.result.details));
}

return element;
}

/**
* Renders the group container for a group of audits. Individual audit elements can be added
* directly to the returned element.
Expand Down Expand Up @@ -245,6 +155,19 @@ class CategoryRenderer {
}
}

/**
* @param {!Array<!Element>} elements
* @return {!Element}
*/
_renderFailedAuditsSection(elements) {
const failedElem = this._renderAuditGroup({
title: `${this._getTotalAuditsLength(elements)} Failed Audits`,
}, {expandable: false});
failedElem.classList.add('lh-failed-audits');
elements.forEach(elem => failedElem.appendChild(elem));
return failedElem;
}

/**
* @param {!Array<!Element>} elements
* @return {!Element}
Expand Down Expand Up @@ -335,177 +258,74 @@ class CategoryRenderer {
return tmpl;
}

/**
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groups
* @return {!Element}
*/
render(category, groups) {
switch (category.id) {
case 'performance':
return this._renderPerformanceCategory(category, groups);
case 'accessibility':
return this._renderAccessibilityCategory(category, groups);
default:
return this._renderDefaultCategory(category, groups);
}
}

/**
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groupDefinitions
* @return {!Element}
*/
_renderDefaultCategory(category, groupDefinitions) {
render(category, groupDefinitions) {
const element = this._dom.createElement('div', 'lh-category');
this._createPermalinkSpan(element, category.id);
element.appendChild(this._renderCategoryScore(category));

const manualAudits = category.audits.filter(audit => audit.result.manual);
const nonManualAudits = category.audits.filter(audit => !manualAudits.includes(audit));
const passedAudits = nonManualAudits.filter(audit => audit.score === 100 &&
!audit.result.debugString);
const nonPassedAudits = nonManualAudits.filter(audit => !passedAudits.includes(audit));

const nonPassedElem = this._renderAuditGroup({
title: `${nonPassedAudits.length} Failed Audits`,
}, {expandable: false});
nonPassedElem.classList.add('lh-failed-audits');
nonPassedAudits.forEach(audit => nonPassedElem.appendChild(this._renderAudit(audit)));
element.appendChild(nonPassedElem);

// Create a passed section if there are passing audits.
if (passedAudits.length) {
const passedElem = this._renderPassedAuditsSection(
passedAudits.map(audit => this._renderAudit(audit))
);
element.appendChild(passedElem);
}

// Render manual audits after passing.
this._renderManualAudits(manualAudits, groupDefinitions, element);

return element;
}

/**
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groups
* @return {!Element}
*/
_renderPerformanceCategory(category, groups) {
const element = this._dom.createElement('div', 'lh-category');
this._createPermalinkSpan(element, category.id);
element.appendChild(this._renderCategoryScore(category));

const metricAudits = category.audits.filter(audit => audit.group === 'perf-metric');
const metricAuditsEl = this._renderAuditGroup(groups['perf-metric'], {expandable: false});
const timelineContainerEl = this._dom.createChildOf(metricAuditsEl, 'div',
'lh-timeline-container');
const timelineEl = this._dom.createChildOf(timelineContainerEl, 'div', 'lh-timeline');
const auditsGroupedByGroup = /** @type {!Object<string,
{passed: !Array<!ReportRenderer.AuditJSON>,
failed: !Array<!ReportRenderer.AuditJSON>,
notApplicable: !Array<!ReportRenderer.AuditJSON>}>} */ ({});
const auditsUngrouped = {passed: [], failed: [], notApplicable: []};

let perfTimelineScale = 0;
metricAudits.forEach(audit => {
if (typeof audit.result.rawValue === 'number' && audit.result.rawValue) {
perfTimelineScale = Math.max(perfTimelineScale, audit.result.rawValue);
}
});
nonManualAudits.forEach(audit => {
let group;

const thumbnailAudit = category.audits.find(audit => audit.id === 'screenshot-thumbnails');
const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
if (thumbnailResult && thumbnailResult.details) {
const thumbnailDetails = /** @type {!DetailsRenderer.FilmstripDetails} */
(thumbnailResult.details);
perfTimelineScale = Math.max(perfTimelineScale, thumbnailDetails.scale);
const filmstripEl = this._detailsRenderer.render(thumbnailDetails);
timelineEl.appendChild(filmstripEl);
}
if (audit.group) {
const groupId = audit.group;

metricAudits.forEach(item => {
if (item.id === 'speed-index-metric' || item.id === 'estimated-input-latency') {
return metricAuditsEl.appendChild(this._renderAudit(item));
if (auditsGroupedByGroup[groupId]) {
group = auditsGroupedByGroup[groupId];
} else {
group = {passed: [], failed: [], notApplicable: []};
auditsGroupedByGroup[groupId] = group;
}
} else {
group = auditsUngrouped;
}

timelineEl.appendChild(this._renderTimelineMetricAudit(item, perfTimelineScale));
});

metricAuditsEl.open = true;
element.appendChild(metricAuditsEl);

const hintAudits = category.audits
.filter(audit => audit.group === 'perf-hint' && audit.score < 100)
.sort((auditA, auditB) => auditB.result.rawValue - auditA.result.rawValue);
if (hintAudits.length) {
const maxWaste = Math.max(...hintAudits.map(audit => audit.result.rawValue));
const scale = Math.ceil(maxWaste / 1000) * 1000;
const hintAuditsEl = this._renderAuditGroup(groups['perf-hint'], {expandable: false});
hintAudits.forEach(item => hintAuditsEl.appendChild(this._renderPerfHintAudit(item, scale)));
hintAuditsEl.open = true;
element.appendChild(hintAuditsEl);
}

const infoAudits = category.audits
.filter(audit => audit.group === 'perf-info' && audit.score < 100);
if (infoAudits.length) {
const infoAuditsEl = this._renderAuditGroup(groups['perf-info'], {expandable: false});
infoAudits.forEach(item => infoAuditsEl.appendChild(this._renderAudit(item)));
infoAuditsEl.open = true;
element.appendChild(infoAuditsEl);
}

const passedElements = category.audits
.filter(audit => (audit.group === 'perf-hint' || audit.group === 'perf-info') &&
audit.score === 100)
.map(audit => this._renderAudit(audit));

if (!passedElements.length) return element;

const passedElem = this._renderPassedAuditsSection(passedElements);
element.appendChild(passedElem);
return element;
}

/**
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groupDefinitions
* @return {!Element}
*/
_renderAccessibilityCategory(category, groupDefinitions) {
const element = this._dom.createElement('div', 'lh-category');
this._createPermalinkSpan(element, category.id);
element.appendChild(this._renderCategoryScore(category));

const manualAudits = category.audits.filter(audit => audit.result.manual);
const nonManualAudits = category.audits.filter(audit => !manualAudits.includes(audit));
const auditsGroupedByGroup = /** @type {!Object<string,
{passed: !Array<!ReportRenderer.AuditJSON>,
failed: !Array<!ReportRenderer.AuditJSON>,
notApplicable: !Array<!ReportRenderer.AuditJSON>}>} */ ({});
nonManualAudits.forEach(audit => {
const groupId = audit.group;
const groups = auditsGroupedByGroup[groupId] || {passed: [], failed: [], notApplicable: []};

if (audit.result.notApplicable) {
groups.notApplicable.push(audit);
} else if (audit.score === 100) {
groups.passed.push(audit);
group.notApplicable.push(audit);
} else if (audit.score === 100 && !audit.result.debugString) {
group.passed.push(audit);
} else {
groups.failed.push(audit);
group.failed.push(audit);
}

auditsGroupedByGroup[groupId] = groups;
});

const failedElements = /** @type {!Array<!Element>} */ ([]);
const passedElements = /** @type {!Array<!Element>} */ ([]);
const notApplicableElements = /** @type {!Array<!Element>} */ ([]);

auditsUngrouped.failed.forEach((/** @type {!ReportRenderer.AuditJSON} */ audit) =>
failedElements.push(this._renderAudit(audit)));
auditsUngrouped.passed.forEach((/** @type {!ReportRenderer.AuditJSON} */ audit) =>
passedElements.push(this._renderAudit(audit)));
auditsUngrouped.notApplicable.forEach((/** @type {!ReportRenderer.AuditJSON} */ audit) =>
notApplicableElements.push(this._renderAudit(audit)));

let hasFailedGroups = false;

Object.keys(auditsGroupedByGroup).forEach(groupId => {
const group = groupDefinitions[groupId];
const groups = auditsGroupedByGroup[groupId];

if (groups.failed.length) {
const auditGroupElem = this._renderAuditGroup(group, {expandable: false});
groups.failed.forEach(item => auditGroupElem.appendChild(this._renderAudit(item)));
auditGroupElem.open = true;
element.appendChild(auditGroupElem);
failedElements.push(auditGroupElem);

hasFailedGroups = true;
}

if (groups.passed.length) {
Expand All @@ -521,6 +341,16 @@ class CategoryRenderer {
}
});

if (failedElements.length) {
// if failed audits are grouped skip the 'X Failed Audits' header
if (hasFailedGroups) {
failedElements.forEach(elem => element.appendChild(elem));
} else {
const failedElem = this._renderFailedAuditsSection(failedElements);
element.appendChild(failedElem);
}
}

if (passedElements.length) {
const passedElem = this._renderPassedAuditsSection(passedElements);
element.appendChild(passedElem);
Expand Down Expand Up @@ -553,14 +383,3 @@ if (typeof module !== 'undefined' && module.exports) {
} else {
self.CategoryRenderer = CategoryRenderer;
}


/**
* @typedef {{
* value: {
* wastedMs: (number|undefined),
* wastedKb: (number|undefined),
* }
* }}
*/
CategoryRenderer.PerfHintExtendedInfo; // eslint-disable-line no-unused-expressions
Loading