From 54fff0790acdc5a36dbc1341e344f9fc430a2a79 Mon Sep 17 00:00:00 2001 From: Adam Raine <6752989+adamraine@users.noreply.github.com> Date: Wed, 22 Sep 2021 15:55:09 -0400 Subject: [PATCH] report(flow): category tooltips (#13043) --- flow-report/assets/styles.css | 53 ++++++++++++- flow-report/src/summary/category.tsx | 92 ++++++++++++++++++++++ flow-report/src/summary/summary.tsx | 25 +----- flow-report/test/summary/category-test.tsx | 68 ++++++++++++++++ lighthouse-core/util-commonjs.js | 22 ++++++ report/renderer/category-renderer.js | 11 +-- report/renderer/util.js | 22 ++++++ report/test/renderer/util-test.js | 26 ++++++ 8 files changed, 285 insertions(+), 34 deletions(-) create mode 100644 flow-report/src/summary/category.tsx create mode 100644 flow-report/test/summary/category-test.tsx diff --git a/flow-report/assets/styles.css b/flow-report/assets/styles.css index 2c22e7b49e8f..0686631279ad 100644 --- a/flow-report/assets/styles.css +++ b/flow-report/assets/styles.css @@ -19,6 +19,7 @@ --summary-flow-step-label-font-size: 16px; --summary-subtitle-font-size: 16px; --summary-title-font-size: 32px; + --summary-tooltip-box-shadow-color: rgba(0, 0, 0, 0.25); --topbar-title-font-size: 14px; } @@ -292,9 +293,59 @@ text-decoration: underline; } -.SummaryCategory { +.SummaryCategory__content { + position: relative; margin: var(--half-base-spacing); } +.SummaryCategory__content:hover, +.SummaryCategory__content:focus-within { + background-color: var(--color-gray-100); +} +.SummaryCategory__content:hover .SummaryTooltip, +.SummaryCategory__content:focus-within .SummaryTooltip { + display: block; +} + +.SummaryTooltip { + display: none; + position: absolute; + min-width: 200%; + max-width: 300%; + width: max-content; + background-color: var(--report-background-color); + border: 1px solid var(--color-gray-900); + border-radius: 5px; + padding: var(--base-spacing); + right: 0; + box-shadow: 0px 4px 4px var(--summary-tooltip-box-shadow-color); + z-index: 1; +} + +.SummaryTooltip__title { + font-weight: bold; + margin-bottom: var(--half-base-spacing); +} + +.SummaryTooltip__category { + font-weight: bold; + display: flex; + margin-top: var(--half-base-spacing); +} + +.SummaryTooltip__category-title { + flex-grow: 1; +} + +.SummaryTooltip__rating--pass { + color: var(--color-pass); +} +.SummaryTooltip__rating--average { + color: var(--color-average); +} +.SummaryTooltip__rating--fail, +.SummaryTooltip__rating--error { + color: var(--color-fail); +} .SummaryNavigationHeader { font-size: var(--summary-flow-step-label-font-size); diff --git a/flow-report/src/summary/category.tsx b/flow-report/src/summary/category.tsx new file mode 100644 index 000000000000..b80223f05954 --- /dev/null +++ b/flow-report/src/summary/category.tsx @@ -0,0 +1,92 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +import {FunctionComponent} from 'preact'; + +import {Util} from '../../../report/renderer/util'; +import {Separator} from '../common'; +import {CategoryScore} from '../wrappers/category-score'; + +const GATHER_MODE_LABELS: Record = { + 'navigation': 'Navigation report', + 'timespan': 'Timespan report', + 'snapshot': 'Snapshot report', +}; + +const RATING_LABELS: Record = { + 'pass': 'Good', + 'fail': 'Poor', + 'average': 'Average', + 'error': 'Error', +}; + +export const SummaryTooltip: FunctionComponent<{ + category: LH.ReportResult.Category, + gatherMode: LH.Result.GatherMode +}> = ({category, gatherMode}) => { + const {numPassed, numAudits, totalWeight} = Util.calculateCategoryFraction(category); + + const displayAsFraction = Util.shouldDisplayAsFraction(gatherMode); + const rating = displayAsFraction ? + Util.calculateRating(numPassed / numAudits) : + Util.calculateRating(category.score); + + return ( +
+
{GATHER_MODE_LABELS[gatherMode]}
+ +
+
+ {category.title} +
+ { + totalWeight !== 0 && +
+ {RATING_LABELS[rating]} + { + !displayAsFraction && category.score && <> + · + {category.score * 100} + + } +
+ } +
+
+ {`${numPassed} audits passed / ${numAudits} audits run`} +
+
+ ); +}; + +export const SummaryCategory: FunctionComponent<{ + category: LH.ReportResult.Category|undefined, + href: string, + gatherMode: LH.Result.GatherMode, +}> = ({category, href, gatherMode}) => { + return ( +
+ { + category ? +
+ + +
: +
+ } +
+ ); +}; diff --git a/flow-report/src/summary/summary.tsx b/flow-report/src/summary/summary.tsx index d62e803e0222..edb8d9d9e975 100644 --- a/flow-report/src/summary/summary.tsx +++ b/flow-report/src/summary/summary.tsx @@ -10,7 +10,7 @@ import {useMemo} from 'preact/hooks'; import {FlowSegment, Separator} from '../common'; import {getScreenDimensions, getScreenshot, useFlowResult} from '../util'; import {Util} from '../../../report/renderer/util'; -import {CategoryScore} from '../wrappers/category-score'; +import {SummaryCategory} from './category'; const DISPLAYED_CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo']; const THUMBNAIL_WIDTH = 50; @@ -33,29 +33,6 @@ const SummaryNavigationHeader: FunctionComponent<{url: string}> = ({url}) => { ); }; -const SummaryCategory: FunctionComponent<{ - category: LH.ReportResult.Category|undefined, - href: string, - gatherMode: LH.Result.GatherMode, -}> = ({category, href, gatherMode}) => { - return ( -
- { - category ? - : -
- } -
- ); -}; - /** * The div should behave like a JSX <>.... This still allows us to identify "rows" with CSS selectors. */ diff --git a/flow-report/test/summary/category-test.tsx b/flow-report/test/summary/category-test.tsx new file mode 100644 index 000000000000..ef841377daa7 --- /dev/null +++ b/flow-report/test/summary/category-test.tsx @@ -0,0 +1,68 @@ +/** + * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +import fs from 'fs'; +import {dirname} from 'path'; +import {fileURLToPath} from 'url'; + +import {render} from '@testing-library/preact'; + +import {SummaryTooltip} from '../../src/summary/category'; +import {Util} from '../../../report/renderer/util'; + +const flowResult: LH.FlowResult = JSON.parse( + fs.readFileSync( + // eslint-disable-next-line max-len + `${dirname(fileURLToPath(import.meta.url))}/../../../lighthouse-core/test/fixtures/fraggle-rock/reports/sample-lhrs.json`, + 'utf-8' + ) +); + +describe('SummaryTooltip', () => { + it('renders tooltip with rating', async () => { + // Snapshot SEO + const reportResult = Util.prepareReportResult(flowResult.steps[2].lhr); + const category = reportResult.categories['seo']; + + const root = render( + + ); + + const rating = root.getByTestId('SummaryTooltip__rating'); + expect(rating.classList).toContain('SummaryTooltip__rating--average'); + + expect(root.getByText('9 audits passed / 11 audits run')).toBeTruthy(); + }); + + it('renders tooltip without rating', async () => { + // Snapshot performance + const reportResult = Util.prepareReportResult(flowResult.steps[2].lhr); + const category = reportResult.categories['performance']; + + const root = render( + + ); + + expect(() => root.getByTestId('SummaryTooltip__rating')).toThrow(); + expect(root.getByText('2 audits passed / 4 audits run')).toBeTruthy(); + }); + + it('renders scored category tooltip with score', async () => { + // Navigation performance + const reportResult = Util.prepareReportResult(flowResult.steps[0].lhr); + const category = reportResult.categories['performance']; + + const root = render( + + ); + + const rating = root.getByTestId('SummaryTooltip__rating'); + expect(rating.classList).toContain('SummaryTooltip__rating--pass'); + expect(rating.textContent).toEqual('Good · 94'); + + expect(root.getByText('41 audits passed / 58 audits run')).toBeTruthy(); + }); +}); diff --git a/lighthouse-core/util-commonjs.js b/lighthouse-core/util-commonjs.js index acec1271222c..4ce534006e24 100644 --- a/lighthouse-core/util-commonjs.js +++ b/lighthouse-core/util-commonjs.js @@ -509,6 +509,28 @@ class Util { static isPluginCategory(categoryId) { return categoryId.startsWith('lighthouse-plugin-'); } + + /** + * @param {LH.Result.GatherMode} gatherMode + */ + static shouldDisplayAsFraction(gatherMode) { + return gatherMode === 'timespan' || gatherMode === 'snapshot'; + } + + /** + * @param {LH.ReportResult.Category} category + */ + static calculateCategoryFraction(category) { + const numAudits = category.auditRefs.length; + + let numPassed = 0; + let totalWeight = 0; + for (const auditRef of category.auditRefs) { + totalWeight += auditRef.weight; + if (Util.showAsPassed(auditRef.result)) numPassed++; + } + return {numPassed, numAudits, totalWeight}; + } } /** diff --git a/report/renderer/category-renderer.js b/report/renderer/category-renderer.js index bbb550eee214..64f6ad7acee6 100644 --- a/report/renderer/category-renderer.js +++ b/report/renderer/category-renderer.js @@ -319,7 +319,7 @@ export class CategoryRenderer { * @return {DocumentFragment} */ renderCategoryScore(category, groupDefinitions, options) { - if (options && (options.gatherMode === 'snapshot' || options.gatherMode === 'timespan')) { + if (options && Util.shouldDisplayAsFraction(options.gatherMode)) { return this.renderCategoryFraction(category); } return this.renderScoreGauge(category, groupDefinitions); @@ -376,14 +376,7 @@ export class CategoryRenderer { const wrapper = this.dom.find('a.lh-fraction__wrapper', tmpl); this.dom.safelySetHref(wrapper, `#${category.id}`); - const numAudits = category.auditRefs.length; - - let numPassed = 0; - let totalWeight = 0; - for (const auditRef of category.auditRefs) { - totalWeight += auditRef.weight; - if (Util.showAsPassed(auditRef.result)) numPassed++; - } + const {numPassed, numAudits, totalWeight} = Util.calculateCategoryFraction(category); const fraction = numPassed / numAudits; const content = this.dom.find('.lh-fraction__content', tmpl); diff --git a/report/renderer/util.js b/report/renderer/util.js index eed704243ba4..f4e09046db93 100644 --- a/report/renderer/util.js +++ b/report/renderer/util.js @@ -506,6 +506,28 @@ export class Util { static isPluginCategory(categoryId) { return categoryId.startsWith('lighthouse-plugin-'); } + + /** + * @param {LH.Result.GatherMode} gatherMode + */ + static shouldDisplayAsFraction(gatherMode) { + return gatherMode === 'timespan' || gatherMode === 'snapshot'; + } + + /** + * @param {LH.ReportResult.Category} category + */ + static calculateCategoryFraction(category) { + const numAudits = category.auditRefs.length; + + let numPassed = 0; + let totalWeight = 0; + for (const auditRef of category.auditRefs) { + totalWeight += auditRef.weight; + if (Util.showAsPassed(auditRef.result)) numPassed++; + } + return {numPassed, numAudits, totalWeight}; + } } /** diff --git a/report/test/renderer/util-test.js b/report/test/renderer/util-test.js index 5b0f6087c2e4..b55a4d99143d 100644 --- a/report/test/renderer/util-test.js +++ b/report/test/renderer/util-test.js @@ -386,4 +386,30 @@ describe('util helpers', () => { ]); }); }); + + describe('#shouldDisplayAsFraction', () => { + it('returns true for timespan and snapshot', () => { + expect(Util.shouldDisplayAsFraction('navigation')).toEqual(false); + expect(Util.shouldDisplayAsFraction('timespan')).toEqual(true); + expect(Util.shouldDisplayAsFraction('snapshot')).toEqual(true); + expect(Util.shouldDisplayAsFraction(undefined)).toEqual(false); + }); + }); + + describe('#calculateCategoryFraction', () => { + it('returns passed audits and total audits', () => { + const category = { + auditRefs: [ + {weight: 3, result: {score: 1, scoreDisplayMode: 'binary'}}, + {weight: 2, result: {score: 1, scoreDisplayMode: 'binary'}}, + {weight: 0, result: {score: 1, scoreDisplayMode: 'binary'}}, + {weight: 1, result: {score: 0, scoreDisplayMode: 'binary'}}, + ], + }; + const {numPassed, numAudits, totalWeight} = Util.calculateCategoryFraction(category); + expect(numPassed).toEqual(3); + expect(numAudits).toEqual(4); + expect(totalWeight).toEqual(6); + }); + }); });