report: DRY up audit & opportunity rendering (#5136)
This commit is contained in:
Родитель
46e49f4eef
Коммит
a99c07b890
|
@ -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 () => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче