diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js index 5b4b7b0c21a2..f455b9b2de46 100644 --- a/lighthouse-core/report/html/renderer/category-renderer.js +++ b/lighthouse-core/report/html/renderer/category-renderer.js @@ -159,13 +159,14 @@ class CategoryRenderer { /** * @param {LH.ReportResult.Category} category + * @param {Record} groupDefinitions * @return {Element} */ - renderCategoryHeader(category) { + renderCategoryHeader(category, groupDefinitions) { const tmpl = this.dom.cloneTemplate('#tmpl-lh-category-header', this.templateContext); const gaugeContainerEl = this.dom.find('.lh-score__gauge', tmpl); - const gaugeEl = this.renderScoreGauge(category); + const gaugeEl = this.renderScoreGauge(category, groupDefinitions); gaugeContainerEl.appendChild(gaugeEl); this.dom.find('.lh-category-header__title', tmpl).appendChild( @@ -329,9 +330,10 @@ class CategoryRenderer { /** * @param {LH.ReportResult.Category} category + * @param {Record} groupDefinitions * @return {DocumentFragment} */ - renderScoreGauge(category) { + renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge', this.templateContext); const wrapper = /** @type {HTMLAnchorElement} */ (this.dom.find('.lh-gauge__wrapper', tmpl)); wrapper.href = `#${category.id}`; @@ -387,7 +389,7 @@ class CategoryRenderer { render(category, groupDefinitions = {}) { const element = this.dom.createElement('div', 'lh-category'); this.createPermalinkSpan(element, category.id); - element.appendChild(this.renderCategoryHeader(category)); + element.appendChild(this.renderCategoryHeader(category, groupDefinitions)); // Top level clumps for audits, in order they will appear in the report. /** @type {Map>} */ diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js index 327fa164be60..252b667016bb 100644 --- a/lighthouse-core/report/html/renderer/performance-category-renderer.js +++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js @@ -120,11 +120,11 @@ class PerformanceCategoryRenderer extends CategoryRenderer { const element = this.dom.createElement('div', 'lh-category'); if (environment === 'PSI') { const gaugeEl = this.dom.createElement('div', 'lh-score__gauge'); - gaugeEl.appendChild(this.renderScoreGauge(category)); + gaugeEl.appendChild(this.renderScoreGauge(category, groups)); element.appendChild(gaugeEl); } else { this.createPermalinkSpan(element, category.id); - element.appendChild(this.renderCategoryHeader(category)); + element.appendChild(this.renderCategoryHeader(category, groups)); } // Metrics diff --git a/lighthouse-core/report/html/renderer/pwa-category-renderer.js b/lighthouse-core/report/html/renderer/pwa-category-renderer.js index 7c9be675aaba..2b547b738c9c 100644 --- a/lighthouse-core/report/html/renderer/pwa-category-renderer.js +++ b/lighthouse-core/report/html/renderer/pwa-category-renderer.js @@ -27,7 +27,7 @@ class PwaCategoryRenderer extends CategoryRenderer { render(category, groupDefinitions = {}) { const categoryElem = this.dom.createElement('div', 'lh-category'); this.createPermalinkSpan(categoryElem, category.id); - categoryElem.appendChild(this.renderCategoryHeader(category)); + categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions)); const auditRefs = category.auditRefs; @@ -48,12 +48,13 @@ class PwaCategoryRenderer extends CategoryRenderer { /** * @param {LH.ReportResult.Category} category + * @param {Record} groupDefinitions * @return {DocumentFragment} */ - renderScoreGauge(category) { + renderScoreGauge(category, groupDefinitions) { // Defer to parent-gauge style if category error. if (category.score === null) { - return super.renderScoreGauge(category); + return super.renderScoreGauge(category, groupDefinitions); } const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge--pwa', this.templateContext); @@ -73,6 +74,7 @@ class PwaCategoryRenderer extends CategoryRenderer { } this.dom.find('.lh-gauge__label', tmpl).textContent = category.title; + wrapper.title = this._getGaugeTooltip(category.auditRefs, groupDefinitions); return tmpl; } @@ -104,6 +106,28 @@ class PwaCategoryRenderer extends CategoryRenderer { return uniqueGroupIds; } + /** + * Returns a tooltip string summarizing group pass rates. + * @param {Array} auditRefs + * @param {Record} groupDefinitions + * @return {string} + */ + _getGaugeTooltip(auditRefs, groupDefinitions) { + const groupIds = this._getGroupIds(auditRefs); + + const tips = []; + for (const groupId of groupIds) { + const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId); + const auditCount = groupAuditRefs.length; + const passedCount = groupAuditRefs.filter(ref => Util.showAsPassed(ref.result)).length; + + const title = groupDefinitions[groupId].title; + tips.push(`${title}: ${passedCount}/${auditCount}`); + } + + return tips.join(', '); + } + /** * Render non-manual audits in groups, giving a badge to any group that has * all passing audits. diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js index c95d7e2ba274..8b9559fa4814 100644 --- a/lighthouse-core/report/html/renderer/report-renderer.js +++ b/lighthouse-core/report/html/renderer/report-renderer.js @@ -211,7 +211,7 @@ class ReportRenderer { const customGauges = []; for (const category of report.reportCategories) { const renderer = specificCategoryRenderers[category.id] || categoryRenderer; - const categoryGauge = renderer.renderScoreGauge(category); + const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {}); // Group gauges that aren't default at the end of the header if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) { diff --git a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js index 03043112d248..5698032e9e12 100644 --- a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js @@ -119,6 +119,7 @@ describe('PwaCategoryRenderer', () => { const targetGroupId = groupIds[2]; assert.ok(targetGroupId); + const targetGroupTitle = sampleResults.categoryGroups[targetGroupId].title; const targetAuditRefs = auditRefs.filter(ref => ref.group === targetGroupId); // Try every permutation of audit scoring. @@ -134,13 +135,25 @@ describe('PwaCategoryRenderer', () => { const badgedScoreGauge = categoryElem.querySelector('.lh-gauge--pwa__wrapper[class*="lh-badged--"]'); + const tooltip = categoryElem.querySelector('.lh-gauge--pwa__wrapper').title; + const targetGroupTip = tooltip.split(', ').find(tip => tip.startsWith(targetGroupTitle)); + assert.ok(targetGroupTip); + // Only expect a badge (and badged gauge) on last permutation (all bits are set). if (i !== totalPermutations - 1) { assert.strictEqual(badgedElems.length, 0); assert.strictEqual(badgedScoreGauge, null); + + // Tooltip ends with passing/total. + const passingCount = categoryElem.querySelectorAll( + `.lh-audit-group--${targetGroupId} .lh-audit--pass`).length; + assert.ok(targetGroupTip.endsWith(`${passingCount}/${targetAuditRefs.length}`)); } else { assert.strictEqual(badgedElems.length, 1); assert.ok(badgedScoreGauge.classList.contains(`lh-badged--${targetGroupId}`)); + + // Tooltip ends with total/total. + assert.ok(targetGroupTip.endsWith(`${targetAuditRefs.length}/${targetAuditRefs.length}`)); } } }); @@ -152,7 +165,17 @@ describe('PwaCategoryRenderer', () => { const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups); assert.strictEqual(categoryElem.querySelectorAll('.lh-badged').length, groupIds.length); - assert.ok(categoryElem.querySelector('.lh-gauge--pwa__wrapper.lh-badged--all')); + + // Score gauge. + const gaugeElem = categoryElem.querySelector('.lh-gauge--pwa__wrapper'); + assert.ok(gaugeElem.classList.contains('lh-badged--all')); + + // All tooltips should have x/x audits passed. + const tips = gaugeElem.title.split(', '); + assert.strictEqual(tips.length, groupIds.length); + for (const tip of tips) { + assert.ok(/(\d+)\/\1$/.test(tip)); + } }); it('renders no badges when no audit groups are passing', () => { @@ -162,8 +185,17 @@ describe('PwaCategoryRenderer', () => { const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups); assert.strictEqual(categoryElem.querySelectorAll('.lh-badged').length, 0); - assert.strictEqual(categoryElem.querySelector('.lh-gauge--pwa__wrapper[class*="lh-badged-"]'), - null); + + // Score gauge. + const gaugeElem = categoryElem.querySelector('.lh-gauge--pwa__wrapper'); + assert.ok(!gaugeElem.matches('.lh-gauge--pwa__wrapper[class*="lh-badged-"]')); + + // All tooltips should have 0/x audits passed. + const tips = gaugeElem.title.split(', '); + assert.strictEqual(tips.length, groupIds.length); + for (const tip of tips) { + assert.ok(/0\/\d+$/.test(tip)); + } }); it('renders all but one badge when all groups but one are passing', () => { @@ -176,13 +208,33 @@ describe('PwaCategoryRenderer', () => { const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups); const gaugeElem = categoryElem.querySelector('.lh-gauge--pwa__wrapper'); + const tips = gaugeElem.title.split(', '); + assert.strictEqual(tips.length, groupIds.length); + for (const groupId of groupIds) { const expectedCount = groupId === failingGroupId ? 0 : 1; + // Individual group badges. const groupElems = categoryElem.querySelectorAll(`.lh-audit-group--${groupId}.lh-badged`); assert.strictEqual(groupElems.length, expectedCount); - gaugeElem.classList.contains(`lh-badged--${groupId}`); + // Score gauge. + if (groupId !== failingGroupId) { + assert.ok(gaugeElem.classList.contains(`lh-badged--${groupId}`)); + } + + // Map back from groupId to groupTitle (used in tooltip). + const groupTitle = sampleResults.categoryGroups[groupId].title; + const groupTip = tips.find(tip => tip.startsWith(groupTitle)); + assert.ok(groupTip); + + // All tooltips should be x/x except for failingGroup, which should be (x-1)/x. + if (groupId !== failingGroupId) { + assert.ok(/(\d+)\/\1$/.test(groupTip)); + } else { + const [, passingCount, totalCount] = /(\d+)\/(\d+)$/.exec(groupTip); + assert.strictEqual(Number(passingCount) + 1, Number(totalCount)); + } } }); });