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.
- package/dist/report/bundle.esm.js +18 -3
- package/dist/report/flow.js +88 -32
- package/dist/report/standalone.js +2 -2
- package/flow-report/assets/standalone-flow-template.html +2 -4
- package/flow-report/src/app.tsx +7 -10
- package/flow-report/src/topbar.tsx +4 -7
- package/flow-report/src/util.ts +28 -1
- package/flow-report/src/wrappers/category-score.tsx +9 -24
- package/flow-report/src/wrappers/markdown.tsx +4 -13
- package/flow-report/src/wrappers/report.tsx +25 -57
- package/flow-report/standalone-flow.tsx +2 -2
- package/flow-report/test/setup/env-setup.ts +11 -5
- package/flow-report/test/summary/category-test.tsx +3 -6
- package/flow-report/test/summary/summary-test.tsx +3 -6
- package/flow-report/test/topbar-test.tsx +14 -14
- package/flow-report/test/util-test.tsx +38 -1
- package/flow-report/test/wrappers/category-score-test.tsx +3 -6
- package/flow-report/test/wrappers/markdown-test.tsx +1 -13
- package/lighthouse-core/lib/statistics.js +26 -39
- package/package.json +1 -1
- package/report/renderer/api.js +35 -1
- package/report/renderer/category-renderer.js +17 -3
- package/report/renderer/report-renderer.js +1 -0
- package/report/test/renderer/category-renderer-test.js +28 -0
- package/report/test/renderer/report-renderer-test.js +29 -0
- package/report/types/report-renderer.d.ts +5 -0
- package/flow-report/src/wrappers/report-renderer.tsx +0 -53
|
@@ -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(
|
|
60
|
+
// Closest double to `erfc-1(1/5)`.
|
|
86
61
|
const INVERSE_ERFC_ONE_FIFTH = 0.9061938024368232;
|
|
87
62
|
|
|
88
|
-
// Shape (σ) is
|
|
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
|
|
91
|
-
const
|
|
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
|
|
96
|
-
|
|
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
package/report/renderer/api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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
|
-
};
|