report: DRY up audit & opportunity rendering (#5136)

This commit is contained in:
Paul Irish 2018-05-17 16:07:20 -07:00 коммит произвёл GitHub
Родитель 46e49f4eef
Коммит a99c07b890
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 125 добавлений и 143 удалений

Просмотреть файл

@ -38,6 +38,17 @@ class CategoryRenderer {
*/
renderAudit(audit, index) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext);
return this.populateAuditValues(audit, index, tmpl);
}
/**
* Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity
* @param {AuditJSON} audit
* @param {number} index
* @param {DocumentFragment} tmpl
* @return {Element}
*/
populateAuditValues(audit, index, tmpl) {
const auditEl = this.dom.find('.lh-audit', tmpl);
auditEl.id = audit.result.id;
const scoreDisplayMode = audit.result.scoreDisplayMode;
@ -49,10 +60,11 @@ class CategoryRenderer {
const titleEl = this.dom.find('.lh-audit__title', auditEl);
titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title));
this.dom.find('.lh-audit__description', auditEl)
.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
if (audit.result.description) {
this.dom.find('.lh-audit__description', auditEl)
.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
}
// Append audit details to header section so the entire audit is within a <details>.
const header = /** @type {HTMLDetailsElement} */ (this.dom.find('details', auditEl));
if (audit.result.details && audit.result.details.type) {
const elem = this.detailsRenderer.render(audit.result.details);
@ -60,8 +72,6 @@ class CategoryRenderer {
header.appendChild(elem);
}
auditEl.classList.add(`lh-audit--${audit.result.scoreDisplayMode}`);
this.dom.find('.lh-audit__index', auditEl).textContent = `${index + 1}`;
this._setRatingClass(auditEl, audit.result.score, scoreDisplayMode);
@ -71,7 +81,7 @@ class CategoryRenderer {
const textEl = this.dom.find('.lh-audit__display-text', auditEl);
textEl.textContent = 'Error!';
textEl.classList.add('tooltip-boundary');
const tooltip = this.dom.createChildOf(textEl, 'div', 'lh-error-tooltip-content tooltip');
const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip lh-debug');
tooltip.textContent = audit.result.errorMessage || 'Report error: no audit information';
} else if (audit.result.explanation) {
const explanationEl = this.dom.createChildOf(titleEl, 'div', 'lh-debug');

Просмотреть файл

@ -38,7 +38,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
if (audit.result.scoreDisplayMode === 'error') {
descriptionEl.textContent = '';
valueEl.textContent = 'Error!';
const tooltip = this.dom.createChildOf(descriptionEl, 'span', 'lh-error-tooltip-content');
const tooltip = this.dom.createChildOf(descriptionEl, 'span');
tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
}
@ -52,45 +52,30 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
* @return {Element}
*/
_renderOpportunity(audit, index, scale) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.dom.find('.lh-load-opportunity', tmpl);
element.classList.add(`lh-load-opportunity--${Util.calculateRating(audit.result.score)}`);
const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.populateAuditValues(audit, index, oppTmpl);
element.id = audit.result.id;
const titleEl = this.dom.find('.lh-load-opportunity__title', tmpl);
titleEl.textContent = audit.result.title;
this.dom.find('.lh-audit__index', element).textContent = `${index + 1}`;
if (audit.result.errorMessage || audit.result.explanation) {
const debugStrEl = this.dom.createChildOf(titleEl, 'div', 'lh-debug');
debugStrEl.textContent = audit.result.errorMessage || audit.result.explanation || null;
}
if (audit.result.scoreDisplayMode === 'error') return element;
const details = audit.result.details;
if (!details) {
return element;
}
const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
if (!summaryInfo || !summaryInfo.wastedMs) {
if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
return element;
}
const displayValue = Util.formatDisplayValue(audit.result.displayValue);
// Overwrite the displayValue with opportunity's wastedMs
const displayEl = this.dom.find('.lh-audit__display-text', element);
const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`;
const wastedMs = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
const auditDescription = this.dom.convertMarkdownLinkSnippets(audit.result.description);
this.dom.find('.lh-load-opportunity__sparkline', tmpl).title = displayValue;
this.dom.find('.lh-load-opportunity__wasted-stat', tmpl).title = displayValue;
this.dom.find('.lh-sparkline__bar', tmpl).style.width = sparklineWidthPct;
this.dom.find('.lh-load-opportunity__wasted-stat', tmpl).textContent = wastedMs;
this.dom.find('.lh-load-opportunity__description', tmpl).appendChild(auditDescription);
this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
// If there's no `type`, then we only used details for `summary`
if (details.type) {
const detailsElem = this.detailsRenderer.render(details);
detailsElem.classList.add('lh-details');
element.appendChild(detailsElem);
// 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;

Просмотреть файл

@ -32,11 +32,11 @@ class Util {
* @return {string}
*/
static formatDisplayValue(displayValue) {
if (typeof displayValue === 'undefined') return '';
if (typeof displayValue === 'string') return displayValue;
if (!displayValue) return '';
const replacementRegex = /%([0-9]*(\.[0-9]+)?d|s)/;
const template = /** @type {string} */ (displayValue.shift());
const template = /** @type {string} */ (displayValue[0]);
if (typeof template !== 'string') {
// First value should always be the format string, but we don't want to fail to build
// a report, return a placeholder.
@ -44,7 +44,7 @@ class Util {
}
let output = template;
for (const replacement of displayValue) {
for (const replacement of displayValue.slice(1)) {
if (!replacementRegex.test(output)) {
// eslint-disable-next-line no-console
console.warn('Too many replacements given');

Просмотреть файл

@ -141,9 +141,6 @@
color: #0c50c7;
}
.lh-root details summary {
cursor: pointer;
}
.lh-audit__description,
.lh-load-opportunity__description,
@ -259,14 +256,19 @@
margin: calc(var(--default-padding) / 2) 0 var(--default-padding);
}
.lh-audit__header > div,
.lh-audit__header > span {
.lh-audit__index,
.lh-audit__title,
.lh-audit__display-text,
.lh-audit__score-icon,
.lh-load-opportunity__sparkline,
.lh-toggle-arrow {
margin: 0 var(--audit-item-gap);
}
.lh-audit__header > div:first-child, .lh-audit__header > span:first-child {
.lh-audit__index {
margin-left: 0;
}
.lh-audit__header > div:last-child, .lh-audit__header > span:last-child {
.lh-toggle-arrow {
margin-right: 0;
}
@ -296,19 +298,16 @@
}
/* Expandable Details (Audit Groups, Audits) */
.lh-expandable-details {
}
.lh-expandable-details__summary {
cursor: pointer;
}
.lh-audit__header {
display: flex;
padding: var(--lh-audit-vpadding) var(--text-indent);
cursor: pointer;
}
.lh-audit--load-opportunity .lh-audit__header {
display: block;
}
.lh-audit__header:hover {
background-color: #F8F9FA;
}
@ -316,7 +315,8 @@
.lh-audit-group[open] > .lh-audit-group__summary > .lh-toggle-arrow,
.lh-expandable-details[open] > .lh-expandable-details__summary > .lh-toggle-arrow,
.lh-expandable-details[open] > .lh-expandable-details__summary > div > .lh-toggle-arrow {
.lh-expandable-details[open] > .lh-expandable-details__summary > div > .lh-toggle-arrow,
.lh-expandable-details[open] > .lh-expandable-details__summary > div > div > .lh-toggle-arrow {
transform: rotateZ(-90deg);
}
@ -428,14 +428,6 @@
/* Perf load opportunity */
.lh-load-opportunity {
border-bottom: 1px solid var(--report-secondary-border-color);
}
.lh-load-opportunity:last-of-type {
border-bottom: none;
}
.lh-load-opportunity__cols {
display: flex;
align-items: flex-start;
@ -449,20 +441,11 @@
line-height: calc(2.3 * var(--body-font-size));
}
.lh-load-opportunity__summary {
padding: var(--lh-audit-vpadding) var(--text-indent);
}
.lh-load-opportunity__summary:hover {
background-color: #F8F9FA;
}
.lh-load-opportunity__col {
display: flex;
justify-content: space-between;
}
.lh-load-opportunity__col > * {
margin: 0 5px;
}
.lh-load-opportunity__col--one {
flex: 5;
margin-right: 2px;
@ -471,46 +454,9 @@
flex: 4;
}
.lh-load-opportunity__title {
font-size: var(--body-font-size);
flex: 10;
}
.lh-load-opportunity__wasted-stat {
.lh-audit--load-opportunity .lh-audit__display-text {
text-align: right;
flex: 0 0 calc(3 * var(--body-font-size));
font-size: var(--body-font-size);
line-height: var(--body-line-height);
}
.lh-load-opportunity__description {
color: var(--secondary-text-color);
margin-top: calc(var(--default-padding) / 2);
}
.lh-load-opportunity--pass .lh-load-opportunity__wasted-stat {
color: var(--pass-color);
}
.lh-load-opportunity--pass .lh-sparkline__bar {
background: var(--pass-color);
}
.lh-load-opportunity--average .lh-sparkline__bar {
background: var(--average-color);
}
.lh-load-opportunity--average .lh-load-opportunity__wasted-stat {
color: var(--average-color);
}
.lh-load-opportunity--fail .lh-sparkline__bar {
background: var(--fail-color);
}
.lh-load-opportunity--fail .lh-load-opportunity__wasted-stat {
color: var(--fail-color);
}
@ -531,6 +477,19 @@
float: right;
}
.lh-audit--pass .lh-sparkline__bar {
background: var(--pass-color);
}
.lh-audit--average .lh-sparkline__bar {
background: var(--average-color);
}
.lh-audit--fail .lh-sparkline__bar {
background: var(--fail-color);
}
/* Filmstrip */
@ -562,7 +521,6 @@
.lh-audit {
border-bottom: 1px solid var(--report-secondary-border-color);
font-size: var(--body-font-size);
}
.lh-audit:last-child {
@ -664,7 +622,7 @@
font-size: var(--caption-font-size);
line-height: var(--caption-line-height);
color: var(--fail-color);
margin-top: 3px;
margin-bottom: 3px;
}
@ -973,6 +931,12 @@ summary.lh-passed-audits-summary {
position: absolute;
display: none; /* Don't retain these layers when not needed */
opacity: 0;
background: #ffffff;
min-width: 23em;
padding: 15px;
border-radius: 5px;
text-align: initial;
}
.tooltip-boundary:hover {
@ -984,10 +948,6 @@ summary.lh-passed-audits-summary {
animation: fadeInTooltip 250ms;
animation-fill-mode: forwards;
animation-delay: 850ms;
min-width: 23em;
background: #ffffff;
padding: 15px;
border-radius: 5px;
bottom: 100%;
z-index: 1;
will-change: opacity;

Просмотреть файл

@ -54,24 +54,26 @@
<!-- Lighthouse perf opportunity -->
<template id="tmpl-lh-opportunity">
<details class="lh-load-opportunity lh-expandable-details">
<summary class="lh-load-opportunity__summary lh-expandable-details__summary">
<div class="lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">
<span class="lh-audit__index"></span>
<div class="lh-load-opportunity__title"></div>
</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">
<div class="lh-load-opportunity__sparkline">
<div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
<div class="lh-audit lh-audit--load-opportunity">
<details class="lh-expandable-details">
<summary class="lh-audit__header lh-expandable-details__summary">
<div class="lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">
<span class="lh-audit__index"></span>
<div class="lh-audit__title"></div>
</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">
<div class="lh-load-opportunity__sparkline">
<div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
</div>
<div class="lh-audit__display-text"></div>
<div class="lh-toggle-arrow" title="See resources"></div>
</div>
<div class="lh-load-opportunity__wasted-stat"></div>
<div class="lh-toggle-arrow" title="See resources"></div>
</div>
</div>
</summary>
<div class="lh-load-opportunity__description"></div>
</details>
</summary>
<div class="lh-audit__description"></div>
</details>
</div>
</template>

Просмотреть файл

@ -86,14 +86,14 @@ describe('PerfCategoryRenderer', () => {
const oppAudits = category.auditRefs.filter(audit => audit.group === 'load-opportunities' &&
audit.result.score !== 1);
const oppElements = categoryDOM.querySelectorAll('.lh-load-opportunity');
const oppElements = categoryDOM.querySelectorAll('.lh-audit--load-opportunity');
assert.equal(oppElements.length, oppAudits.length);
const oppElement = oppElements[0];
const oppSparklineBarElement = oppElement.querySelector('.lh-sparkline__bar');
const oppSparklineElement = oppElement.querySelector('.lh-load-opportunity__sparkline');
const oppTitleElement = oppElement.querySelector('.lh-load-opportunity__title');
const oppWastedElement = oppElement.querySelector('.lh-load-opportunity__wasted-stat');
const oppTitleElement = oppElement.querySelector('.lh-audit__title');
const oppWastedElement = oppElement.querySelector('.lh-audit__display-text');
assert.ok(oppTitleElement.textContent, 'did not render title');
assert.ok(oppSparklineBarElement.style.width, 'did not set sparkline width');
assert.ok(oppWastedElement.textContent, 'did not render stats');
@ -105,15 +105,33 @@ describe('PerfCategoryRenderer', () => {
score: 0,
group: 'load-opportunities',
result: {
score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', description: 'Bug #2',
score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', title: 'Bug #2',
},
};
const fakeCategory = Object.assign({}, category, {auditRefs: [auditWithDebug]});
const categoryDOM = renderer.render(fakeCategory, sampleResults.categoryGroups);
const tooltipEl = categoryDOM.querySelector('.lh-audit--load-opportunity .lh-debug');
assert.ok(tooltipEl, 'did not render debug');
assert.ok(/Yikes!!/.test(tooltipEl.textContent));
});
it('renders performance opportunities\' explanation', () => {
const auditWithDebug = {
score: 0,
group: 'load-opportunities',
result: {
score: 0, scoreDisplayMode: 'numeric',
rawValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
},
};
const fakeCategory = Object.assign({}, category, {auditRefs: [auditWithDebug]});
const categoryDOM = renderer.render(fakeCategory, sampleResults.categoryGroups);
const debugEl = categoryDOM.querySelector('.lh-load-opportunity .lh-debug');
assert.ok(debugEl, 'did not render debug');
const tooltipEl = categoryDOM.querySelector('.lh-audit--load-opportunity .lh-debug');
assert.ok(tooltipEl, 'did not render debug');
assert.ok(/Yikes!!/.test(tooltipEl.textContent));
});
it('renders the failing diagnostics', () => {
@ -144,7 +162,7 @@ describe('PerfCategoryRenderer', () => {
group: 'load-opportunities',
result: {
error: true, score: 0,
rawValue: 100, debugString: 'Yikes!!', description: 'Bug #2',
rawValue: 100, debugString: 'Yikes!!', title: 'Bug #2',
},
};
const wastedMs = renderer._getWastedMs(auditWithDebug);

Просмотреть файл

@ -135,4 +135,11 @@ describe('util helpers', () => {
'Too many replacements given',
]);
});
it('does not mutate the provided array', () => {
const displayValue = ['one:%s, two:%s', 'foo', 'bar'];
const cloned = JSON.parse(JSON.stringify(displayValue));
Util.formatDisplayValue(displayValue);
assert.deepStrictEqual(displayValue, cloned, 'displayValue was mutated');
});
});

Просмотреть файл

@ -117,8 +117,8 @@ describe('Lighthouse chrome extension', function() {
const selectors = {
audits: '.lh-audit,.lh-metric,.lh-load-opportunity',
titles: '.lh-audit__title, .lh-load-opportunity__title, .lh-metric__title',
audits: '.lh-audit, .lh-metric',
titles: '.lh-audit__title, .lh-metric__title',
};
it('should contain all categories', async () => {

Просмотреть файл

@ -78,8 +78,8 @@ describe('Lighthouse Viewer', function() {
const selectors = {
audits: '.lh-audit, .lh-metric, .lh-load-opportunity',
titles: '.lh-audit__title, .lh-load-opportunity__title, .lh-metric__title',
audits: '.lh-audit, .lh-metric',
titles: '.lh-audit__title, .lh-metric__title',
};
it('should load with no errors', async () => {