lighthouse 9.0.0-dev.20211117 → 9.0.0-dev.20211121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,12 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ // The exact double values for the max and min scores possible in each range.
9
+ const MIN_PASSING_SCORE = 0.90000000000000002220446049250313080847263336181640625;
10
+ const MAX_AVERAGE_SCORE = 0.899999999999999911182158029987476766109466552734375;
11
+ const MIN_AVERAGE_SCORE = 0.5;
12
+ const MAX_FAILING_SCORE = 0.499999999999999944488848768742172978818416595458984375;
13
+
8
14
  /**
9
15
  * Approximates the Gauss error function, the probability that a random variable
10
16
  * from the standard normal distribution lies within [-x, x]. Moved from
@@ -28,37 +34,6 @@ function erf(x) {
28
34
  return sign * (1 - y * Math.exp(-x * x));
29
35
  }
30
36
 
31
- /**
32
- * Creates a log-normal distribution à la traceviewer's statistics package.
33
- * Specified by providing the median value, at which the score will be 0.5,
34
- * and the falloff, the initial point of diminishing returns where any
35
- * improvement in value will yield increasingly smaller gains in score. Both
36
- * values should be in the same units (e.g. milliseconds). See
37
- * https://www.desmos.com/calculator/tx1wcjk8ch
38
- * for an interactive view of the relationship between these parameters and
39
- * the typical parameterization (location and shape) of the log-normal
40
- * distribution.
41
- * @param {number} median
42
- * @param {number} falloff
43
- * @return {{computeComplementaryPercentile: function(number): number}}
44
- */
45
- function getLogNormalDistribution(median, falloff) {
46
- const location = Math.log(median);
47
-
48
- // The "falloff" value specified the location of the smaller of the positive
49
- // roots of the third derivative of the log-normal CDF. Calculate the shape
50
- // parameter in terms of that value and the median.
51
- const logRatio = Math.log(falloff / median);
52
- const shape = Math.sqrt(1 - 3 * logRatio - Math.sqrt((logRatio - 3) * (logRatio - 3) - 8)) / 2;
53
-
54
- return {
55
- computeComplementaryPercentile(x) {
56
- const standardizedX = (Math.log(x) - location) / (Math.SQRT2 * shape);
57
- return (1 - erf(standardizedX)) / 2;
58
- },
59
- };
60
- }
61
-
62
37
  /**
63
38
  * Returns the score (1 - percentile) of `value` in a log-normal distribution
64
39
  * specified by the `median` value, at which the score will be 0.5, and a 10th
@@ -76,24 +51,37 @@ function getLogNormalScore({median, p10}, value) {
76
51
  // Required for the log-normal distribution.
77
52
  if (median <= 0) throw new Error('median must be greater than zero');
78
53
  if (p10 <= 0) throw new Error('p10 must be greater than zero');
79
- // Not required, but if p10 > median, it flips around and becomes the p90 point.
54
+ // Not strictly required, but if p10 > median, it flips around and becomes the p90 point.
80
55
  if (p10 >= median) throw new Error('p10 must be less than the median');
81
56
 
82
57
  // Non-positive values aren't in the distribution, so always 1.
83
58
  if (value <= 0) return 1;
84
59
 
85
- // Closest double to `erfc-1(2 * 1/10)`.
60
+ // Closest double to `erfc-1(1/5)`.
86
61
  const INVERSE_ERFC_ONE_FIFTH = 0.9061938024368232;
87
62
 
88
- // Shape (σ) is `log(p10/median) / (sqrt(2)*erfc^-1(2 * 1/10))` and
63
+ // Shape (σ) is `|log(p10/median) / (sqrt(2)*erfc^-1(1/5))|` and
89
64
  // standardizedX is `1/2 erfc(log(value/median) / (sqrt(2)*σ))`, so simplify a bit.
90
- const xLogRatio = Math.log(value / median);
91
- const p10LogRatio = -Math.log(p10 / median); // negate to keep σ positive.
65
+ const xRatio = Math.max(Number.MIN_VALUE, value / median); // value and median are > 0, so is ratio.
66
+ const xLogRatio = Math.log(xRatio);
67
+ const p10Ratio = Math.max(Number.MIN_VALUE, p10 / median); // p10 and median are > 0, so is ratio.
68
+ const p10LogRatio = -Math.log(p10Ratio); // negate to keep σ positive.
92
69
  const standardizedX = xLogRatio * INVERSE_ERFC_ONE_FIFTH / p10LogRatio;
93
70
  const complementaryPercentile = (1 - erf(standardizedX)) / 2;
94
71
 
95
- // Clamp to [0, 1] to avoid any floating-point out-of-bounds issues.
96
- return Math.min(1, Math.max(0, complementaryPercentile));
72
+ // Clamp to avoid floating-point out-of-bounds issues and keep score in expected range.
73
+ let score;
74
+ if (value <= p10) {
75
+ // Passing. Clamp to [0.9, 1].
76
+ score = Math.max(MIN_PASSING_SCORE, Math.min(1, complementaryPercentile));
77
+ } else if (value <= median) {
78
+ // Average. Clamp to [0.5, 0.9).
79
+ score = Math.max(MIN_AVERAGE_SCORE, Math.min(MAX_AVERAGE_SCORE, complementaryPercentile));
80
+ } else {
81
+ // Failing. Clamp to [0, 0.5).
82
+ score = Math.max(0, Math.min(MAX_FAILING_SCORE, complementaryPercentile));
83
+ }
84
+ return score;
97
85
  }
98
86
 
99
87
  /**
@@ -112,6 +100,5 @@ function linearInterpolation(x0, y0, x1, y1, x) {
112
100
 
113
101
  module.exports = {
114
102
  linearInterpolation,
115
- getLogNormalDistribution,
116
103
  getLogNormalScore,
117
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.0.0-dev.20211117",
3
+ "version": "9.0.0-dev.20211121",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -9,13 +9,15 @@
9
9
  import {DOM} from '../renderer/dom.js';
10
10
  import {ReportRenderer} from '../renderer/report-renderer.js';
11
11
  import {ReportUIFeatures} from '../renderer/report-ui-features.js';
12
+ import {CategoryRenderer} from './category-renderer.js';
13
+ import {DetailsRenderer} from './details-renderer.js';
12
14
 
13
15
  /**
14
16
  * @param {LH.Result} lhr
15
17
  * @param {LH.Renderer.Options} opts
16
18
  * @return {HTMLElement}
17
19
  */
18
- export function renderReport(lhr, opts = {}) {
20
+ function renderReport(lhr, opts = {}) {
19
21
  const rootEl = document.createElement('article');
20
22
  rootEl.classList.add('lh-root', 'lh-vars');
21
23
 
@@ -30,3 +32,35 @@ export function renderReport(lhr, opts = {}) {
30
32
  features.initFeatures(lhr);
31
33
  return rootEl;
32
34
  }
35
+
36
+ /**
37
+ * @param {LH.ReportResult.Category} category
38
+ * @param {Parameters<CategoryRenderer['renderCategoryScore']>[2]=} options
39
+ * @return {DocumentFragment}
40
+ */
41
+ function renderCategoryScore(category, options) {
42
+ const dom = new DOM(document, document.documentElement);
43
+ const detailsRenderer = new DetailsRenderer(dom);
44
+ const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
45
+ return categoryRenderer.renderCategoryScore(category, {}, options);
46
+ }
47
+
48
+ /**
49
+ * @param {Blob} blob
50
+ * @param {string} filename
51
+ */
52
+ function saveFile(blob, filename) {
53
+ const dom = new DOM(document, document.documentElement);
54
+ dom.saveFile(blob, filename);
55
+ }
56
+
57
+ /**
58
+ * @param {string} markdownText
59
+ * @return {Element}
60
+ */
61
+ function convertMarkdownCodeSnippets(markdownText) {
62
+ const dom = new DOM(document, document.documentElement);
63
+ return dom.convertMarkdownCodeSnippets(markdownText);
64
+ }
65
+
66
+ export {renderReport, renderCategoryScore, saveFile, convertMarkdownCodeSnippets};
@@ -348,14 +348,28 @@ export class CategoryRenderer {
348
348
  /**
349
349
  * @param {LH.ReportResult.Category} category
350
350
  * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
351
- * @param {{gatherMode: LH.Result.GatherMode}=} options
351
+ * @param {{gatherMode: LH.Result.GatherMode, omitLabel?: boolean, onPageAnchorRendered?: (link: HTMLAnchorElement) => void}=} options
352
352
  * @return {DocumentFragment}
353
353
  */
354
354
  renderCategoryScore(category, groupDefinitions, options) {
355
+ let categoryScore;
355
356
  if (options && Util.shouldDisplayAsFraction(options.gatherMode)) {
356
- return this.renderCategoryFraction(category);
357
+ categoryScore = this.renderCategoryFraction(category);
358
+ } else {
359
+ categoryScore = this.renderScoreGauge(category, groupDefinitions);
360
+ }
361
+
362
+ if (options?.omitLabel) {
363
+ const label = this.dom.find('.lh-gauge__label,.lh-fraction__label', categoryScore);
364
+ label.remove();
357
365
  }
358
- return this.renderScoreGauge(category, groupDefinitions);
366
+
367
+ if (options?.onPageAnchorRendered) {
368
+ const anchor = this.dom.find('a', categoryScore);
369
+ options.onPageAnchorRendered(anchor);
370
+ }
371
+
372
+ return categoryScore;
359
373
  }
360
374
 
361
375
  /**
@@ -224,6 +224,7 @@ export class ReportRenderer {
224
224
  e.preventDefault();
225
225
  destEl.scrollIntoView();
226
226
  });
227
+ this._opts.onPageAnchorRendered?.(gaugeWrapperEl);
227
228
  }
228
229
 
229
230
 
@@ -500,6 +500,34 @@ describe('CategoryRenderer', () => {
500
500
  });
501
501
  });
502
502
 
503
+ describe('renderCategoryScore', () => {
504
+ it('removes label if omitLabel is true', () => {
505
+ const options = {omitLabel: true};
506
+ const categoryScore = renderer.renderCategoryScore(
507
+ sampleResults.categories.performance,
508
+ {},
509
+ options
510
+ );
511
+ const label = categoryScore.querySelector('.lh-gauge__label,.lh-fraction__label');
512
+ assert.ok(!label);
513
+ });
514
+
515
+ it('uses custom callback if present', () => {
516
+ const options = {
517
+ onPageAnchorRendered: link => {
518
+ link.href = '#index=0&anchor=performance';
519
+ },
520
+ };
521
+ const categoryScore = renderer.renderCategoryScore(
522
+ sampleResults.categories.performance,
523
+ {},
524
+ options
525
+ );
526
+ const link = categoryScore.querySelector('a');
527
+ assert.equal(link.hash, '#index=0&anchor=performance');
528
+ });
529
+ });
530
+
503
531
  it('renders audits by weight', () => {
504
532
  const defaultAuditRef = {
505
533
  title: '',
@@ -122,6 +122,35 @@ describe('ReportRenderer', () => {
122
122
  }
123
123
  });
124
124
 
125
+ it('renders score gauges with custom callback', () => {
126
+ const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
127
+
128
+ const opts = {
129
+ onPageAnchorRendered: link => {
130
+ const id = link.hash.substring(1);
131
+ link.hash = `#index=0&anchor=${id}`;
132
+ },
133
+ };
134
+ const container = renderer._dom.document().body;
135
+ const output = renderer.renderReport(sampleResultsCopy, container, opts);
136
+ const anchors = output.querySelectorAll('a.lh-gauge__wrapper, a.lh-fraction__wrapper');
137
+ const hashes = Array.from(anchors).map(anchor => anchor.hash).filter(hash => hash);
138
+
139
+ // One set for the sticky header, on set for the gauges at the top.
140
+ assert.deepStrictEqual(hashes, [
141
+ '#index=0&anchor=performance',
142
+ '#index=0&anchor=accessibility',
143
+ '#index=0&anchor=best-practices',
144
+ '#index=0&anchor=seo',
145
+ '#index=0&anchor=pwa',
146
+ '#index=0&anchor=performance',
147
+ '#index=0&anchor=accessibility',
148
+ '#index=0&anchor=best-practices',
149
+ '#index=0&anchor=seo',
150
+ '#index=0&anchor=pwa',
151
+ ]);
152
+ });
153
+
125
154
  it('renders plugin score gauge', () => {
126
155
  const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
127
156
  sampleResultsCopy.categories['lighthouse-plugin-someplugin'] = {
@@ -17,6 +17,11 @@ declare module Renderer {
17
17
 
18
18
  /** Disable the topbar UI component */
19
19
  omitTopbar?: boolean;
20
+ /**
21
+ * Convert report anchor links to a different format.
22
+ * Flow report uses this to convert `#seo` to `#index=0&anchor=seo`.
23
+ */
24
+ onPageAnchorRendered?: (link: HTMLAnchorElement) => void;
20
25
  }
21
26
  }
22
27
 
@@ -1,53 +0,0 @@
1
- /**
2
- * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
3
- * 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
4
- * 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.
5
- */
6
-
7
- import {createContext, FunctionComponent} from 'preact';
8
- import {useContext, useMemo} from 'preact/hooks';
9
-
10
- import {CategoryRenderer} from '../../../report/renderer/category-renderer';
11
- import {DetailsRenderer} from '../../../report/renderer/details-renderer';
12
- import {DOM} from '../../../report/renderer/dom';
13
- import {ReportRenderer} from '../../../report/renderer/report-renderer';
14
-
15
- interface ReportRendererGlobals {
16
- dom: DOM,
17
- detailsRenderer: DetailsRenderer,
18
- categoryRenderer: CategoryRenderer,
19
- reportRenderer: ReportRenderer,
20
- }
21
-
22
- const ReportRendererContext = createContext<ReportRendererGlobals|undefined>(undefined);
23
-
24
- function useReportRenderer() {
25
- const globals = useContext(ReportRendererContext);
26
- if (!globals) throw Error('Globals not defined');
27
- return globals;
28
- }
29
-
30
- const ReportRendererProvider: FunctionComponent = ({children}) => {
31
- const globals = useMemo(() => {
32
- // @ts-expect-error Still using legacy
33
- const dom = new DOM(document);
34
- const detailsRenderer = new DetailsRenderer(dom);
35
- const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
36
- const reportRenderer = new ReportRenderer(dom);
37
- return {
38
- dom,
39
- detailsRenderer,
40
- categoryRenderer,
41
- reportRenderer,
42
- };
43
- }, []);
44
- return (
45
- <ReportRendererContext.Provider value={globals}>{children}</ReportRendererContext.Provider>
46
- );
47
- };
48
-
49
- export {
50
- ReportRendererContext,
51
- ReportRendererProvider,
52
- useReportRenderer,
53
- };