From db6b887e532cba8611654ecb187db2a493fda01c Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Fri, 4 May 2018 13:53:34 -0700 Subject: [PATCH] report: implement new design for opportunities (#5115) --- .../renderer/performance-category-renderer.js | 63 +++------- lighthouse-core/report/html/renderer/util.js | 10 ++ lighthouse-core/report/html/report-styles.css | 118 ++++++++---------- lighthouse-core/report/html/templates.html | 35 +++++- .../performance-category-renderer-test.js | 27 ++-- 5 files changed, 126 insertions(+), 127 deletions(-) diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js index 5f6f68ccade5..db5917f9b0d2 100644 --- a/lighthouse-core/report/html/renderer/performance-category-renderer.js +++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js @@ -47,64 +47,38 @@ class PerformanceCategoryRenderer extends CategoryRenderer { * @return {!Element} */ _renderOpportunity(audit, index, scale) { - const element = this.dom.createElement('details', [ - 'lh-load-opportunity', - `lh-load-opportunity--${Util.calculateRating(audit.result.score)}`, - 'lh-expandable-details', - ].join(' ')); + 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)}`); element.id = audit.result.name; - // TODO(paulirish): use a template instead. - const summary = this.dom.createChildOf(element, 'summary', 'lh-load-opportunity__summary ' + - 'lh-expandable-details__summary'); - const titleEl = this.dom.createChildOf(summary, 'div', 'lh-load-opportunity__title'); + const summary = this.dom.find('.lh-load-opportunity__summary', tmpl); + const titleEl = this.dom.find('.lh-load-opportunity__title', tmpl); titleEl.textContent = audit.result.description; + this.dom.find('.lh-audit__index', element).textContent = `${index + 1}`; - this.dom.createChildOf(summary, 'div', 'lh-toggle-arrow', {title: 'See resources'}); - - if (audit.result.error) { + if (audit.result.debugString || audit.result.error) { const debugStrEl = this.dom.createChildOf(summary, 'div', 'lh-debug'); debugStrEl.textContent = audit.result.debugString || 'Audit error'; - return element; } + if (audit.result.error) return element; const details = audit.result.details; const summaryInfo = /** @type {!DetailsRenderer.OpportunitySummary} */ (details && details.summary); - // eslint-disable-next-line no-console - console.assert(summaryInfo, 'Missing `summary` for load-opportunities audit'); - // eslint-disable-next-line no-console - console.assert(typeof summaryInfo.wastedMs === 'number', - 'Missing numeric `summary.wastedMs` for load-opportunities audit'); if (!summaryInfo || !summaryInfo.wastedMs) { return element; } - const elemAttrs = {title: Util.formatDisplayValue(audit.result.displayValue)}; - const sparklineContainerEl = this.dom.createChildOf(summary, 'div', - 'lh-load-opportunity__sparkline', elemAttrs); - const sparklineEl = this.dom.createChildOf(sparklineContainerEl, 'div', 'lh-sparkline'); - const sparklineBarEl = this.dom.createChildOf(sparklineEl, 'div', 'lh-sparkline__bar'); - sparklineBarEl.style.width = summaryInfo.wastedMs / scale * 100 + '%'; - - const statsEl = this.dom.createChildOf(summary, 'div', 'lh-load-opportunity__stats', elemAttrs); - const statsMsEl = this.dom.createChildOf(statsEl, 'div', 'lh-load-opportunity__primary-stat'); - statsMsEl.textContent = Util.formatMilliseconds(summaryInfo.wastedMs); - - if (summaryInfo.wastedBytes) { - const statsKbEl = this.dom.createChildOf(statsEl, 'div', - 'lh-load-opportunity__secondary-stat'); - statsKbEl.textContent = Util.formatBytesToKB(summaryInfo.wastedBytes); - } - - const descriptionEl = this.dom.createChildOf(element, 'div', - 'lh-load-opportunity__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; - } + const displayValue = Util.formatDisplayValue(audit.result.displayValue); + const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`; + const wastedMs = Util.formatSeconds(summaryInfo.wastedMs, 0.01); + const auditDescription = this.dom.convertMarkdownLinkSnippets(audit.result.helpText); + 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); // If there's no `type`, then we only used details for `summary` if (details.type) { @@ -163,6 +137,9 @@ class PerformanceCategoryRenderer extends CategoryRenderer { const maxWaste = Math.max(...opportunityAudits.map(audit => audit.result.rawValue)); const scale = Math.ceil(maxWaste / 1000) * 1000; 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.open = true; diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js index 32e888fe58e8..3b9021321a4b 100644 --- a/lighthouse-core/report/html/renderer/util.js +++ b/lighthouse-core/report/html/renderer/util.js @@ -116,6 +116,16 @@ class Util { return `${coarseTime.toLocaleString()}${NBSP}ms`; } + /** + * @param {number} ms + * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1 + * @return {string} + */ + static formatSeconds(ms, granularity = 0.1) { + const coarseTime = Math.round(ms / 1000 / granularity) * granularity; + return `${coarseTime.toLocaleString()}${NBSP}s`; + } + /** * Format time. * @param {string} date diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css index 649a2b1f370f..ba55c7df91ac 100644 --- a/lighthouse-core/report/html/report-styles.css +++ b/lighthouse-core/report/html/report-styles.css @@ -31,6 +31,7 @@ --pass-color: hsl(139, 70%, 30%); --informative-color: #0c50c7; --medium-75-gray: #757575; + --medium-50-gray: hsl(210, 17%, 98%); --warning-color: #ffab00; /* md amber a700 */ --report-border-color: #ccc; --report-secondary-border-color: #ebebeb; @@ -279,7 +280,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 > .lh-toggle-arrow, +.lh-expandable-details[open] > .lh-expandable-details__summary > div > .lh-toggle-arrow { transform: rotateZ(-90deg); } @@ -406,22 +408,43 @@ .lh-load-opportunity { padding-top: var(--lh-audit-vpadding); padding-bottom: var(--lh-audit-vpadding); - border-top: 1px solid var(--report-secondary-border-color); + border-bottom: 1px solid var(--report-secondary-border-color); } .lh-load-opportunity:last-of-type { border-bottom: none; } -.lh-load-opportunity__summary { +.lh-load-opportunity__cols { display: flex; align-items: flex-start; - flex-wrap: wrap; - min-height: calc(var(--body-line-height) + var(--caption-line-height)); } -.lh-load-opportunity__summary .lh-toggle-arrow { - margin-top: calc((var(--subheader-line-height) - 12px) / 2); +.lh-load-opportunity__header .lh-load-opportunity__col { + background-color: var(--medium-50-gray); + color: var(--medium-75-gray); + text-align: center; + display: unset; + line-height: calc(3 * var(--body-font-size)); +} + +.lh-load-opportunity__summary { + padding-right: var(--text-indent); +} + +.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; +} +.lh-load-opportunity__col--two { + flex: 4; } .lh-load-opportunity__summary .lh-debug { @@ -434,38 +457,20 @@ flex: 10; } -.lh-load-opportunity__sparkline { - flex: 0 0 50%; - margin-top: calc((var(--body-line-height) - var(--lh-sparkline-height)) / 2); -} - -.lh-load-opportunity__sparkline .lh-sparkline { - width: 100%; - float: right; - margin: 0; -} -.lh-load-opportunity__stats { +.lh-load-opportunity__wasted-stat { text-align: right; - flex: 0 0 calc(5 * var(--body-font-size)); -} - -.lh-load-opportunity__primary-stat { + flex: 0 0 calc(3 * var(--body-font-size)); font-size: var(--body-font-size); line-height: var(--body-line-height); } -.lh-load-opportunity__secondary-stat { - font-size: var(--caption-font-size); - line-height: var(--caption-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__stats { +.lh-load-opportunity--pass .lh-load-opportunity__wasted-stat { color: var(--pass-color); } @@ -477,7 +482,7 @@ background: var(--average-color); } -.lh-load-opportunity--average .lh-load-opportunity__stats { +.lh-load-opportunity--average .lh-load-opportunity__wasted-stat { color: var(--average-color); } @@ -485,10 +490,30 @@ background: var(--fail-color); } -.lh-load-opportunity--fail .lh-load-opportunity__stats { +.lh-load-opportunity--fail .lh-load-opportunity__wasted-stat { color: var(--fail-color); } + +/* Sparkline */ + +.lh-load-opportunity__sparkline { + flex: 1; + margin-top: calc((var(--body-line-height) - var(--lh-sparkline-height)) / 2); +} + +.lh-sparkline { + height: var(--lh-sparkline-height); + width: 100%; +} + +.lh-sparkline__bar { + background: var(--informative-color); + height: 100%; + float: right; +} + + /* Filmstrip */ .lh-filmstrip { @@ -527,39 +552,6 @@ max-width: 60px; } -/* Sparkline */ - -.lh-sparkline { - margin: 5px; - height: var(--lh-sparkline-height); - width: 100%; -} - -.lh-sparkline--thin { - height: calc(var(--lh-sparkline-height) / 2); -} - -.lh-sparkline__bar { - background: var(--warning-color); - height: 100%; - float: right; - position: relative; -} - -/* correlate metric end location with sparkline */ -.lh-metric:hover .lh-sparkline__bar::after { - content: ''; - height: 100vh; - width: 2px; - background: inherit; - position: absolute; - right: 0; - bottom: 0; - opacity: 0; - animation: fadeIn 150ms; - animation-fill-mode: forwards; -} - /* Audit */ .lh-audit { diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html index 37a9c39d7d6c..fff99ad8d3d0 100644 --- a/lighthouse-core/report/html/templates.html +++ b/lighthouse-core/report/html/templates.html @@ -43,7 +43,6 @@ + + + + + + diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js index 88e3e3ea3f9c..5135624ec26c 100644 --- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js @@ -89,11 +89,14 @@ describe('PerfCategoryRenderer', () => { 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'); - assert.ok(oppElement.querySelector('.lh-load-opportunity__title'), 'did not render title'); - assert.ok(oppSparklineElement, 'did not render sparkline'); - assert.ok(oppElement.querySelector('.lh-load-opportunity__stats'), 'did not render stats'); - assert.ok(oppSparklineElement.title, 'did not render tooltip'); + const oppTitleElement = oppElement.querySelector('.lh-load-opportunity__title'); + const oppWastedElement = oppElement.querySelector('.lh-load-opportunity__wasted-stat'); + 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'); + assert.ok(oppSparklineElement.title, 'did not set tooltip on sparkline'); }); it('renders the performance opportunities with a debug string', () => { @@ -132,22 +135,6 @@ describe('PerfCategoryRenderer', () => { assert.ok(debugEl, 'did not render debug'); }); - it('throws if a performance opportunities is missing summary.wastedMs', () => { - const auditWithDebug = { - score: 0, - group: 'load-opportunities', - result: { - rawValue: 100, description: 'Bug', - helpText: '', score: 0.32, - }, - }; - - const fakeCategory = Object.assign({}, category, {audits: [auditWithDebug]}); - assert.throws(_ => { - renderer.render(fakeCategory, sampleResults.reportGroups); - }); - }); - it('renders the failing diagnostics', () => { const categoryDOM = renderer.render(category, sampleResults.reportGroups); const diagnosticSection = categoryDOM.querySelectorAll('.lh-category > .lh-audit-group')[2];