lighthouse 8.6.0-dev.20211031 → 8.6.0-dev.20211101
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/flow.js +7 -7
- package/dist/report/standalone.js +7 -7
- package/flow-report/src/app.tsx +10 -11
- package/flow-report/src/common.tsx +11 -5
- package/flow-report/src/header.tsx +3 -3
- package/flow-report/src/sidebar/flow.tsx +3 -3
- package/flow-report/src/sidebar/sidebar.tsx +3 -3
- package/flow-report/src/summary/category.tsx +2 -2
- package/flow-report/src/util.ts +12 -12
- package/flow-report/src/wrappers/report.tsx +11 -6
- package/flow-report/test/common-test.tsx +25 -10
- package/flow-report/test/header-test.tsx +6 -6
- package/flow-report/test/summary/category-test.tsx +5 -5
- package/flow-report/test/util-test.tsx +32 -11
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
- package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
- package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
- package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
- package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
- package/lighthouse-core/config/config-helpers.js +20 -10
- package/lighthouse-core/config/default-config.js +59 -53
- package/lighthouse-core/fraggle-rock/config/default-config.js +1 -1
- package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
- package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
- package/lighthouse-core/lib/asset-saver.js +4 -1
- package/lighthouse-core/lib/lh-env.js +2 -0
- package/lighthouse-core/lib/tappable-rects.js +8 -15
- package/lighthouse-core/runner.js +1 -1
- package/lighthouse-core/util-commonjs.js +1 -2
- package/package.json +9 -6
- package/report/clients/standalone.js +0 -2
- package/report/renderer/element-screenshot-renderer.js +2 -2
- package/report/renderer/performance-category-renderer.js +20 -5
- package/report/renderer/util.js +1 -2
- package/report/test/renderer/performance-category-renderer-test.js +34 -8
- package/report/test/renderer/report-renderer-test.js +1 -1
- package/report/test/renderer/report-ui-features-test.js +3 -0
- package/report/test/renderer/util-test.js +13 -31
- package/shared/localization/format.js +9 -4
- package/shared/localization/locales/en-US.json +1 -1
- package/shared/localization/locales/en-XL.json +1 -1
- package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
- package/types/lhr/flow.d.ts +3 -2
- package/types/rollup-plugin-postprocess.d.ts +10 -0
package/flow-report/src/app.tsx
CHANGED
|
@@ -5,25 +5,24 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {FunctionComponent} from 'preact';
|
|
8
|
-
import {
|
|
8
|
+
import {useLayoutEffect, useRef, useState} from 'preact/hooks';
|
|
9
9
|
|
|
10
10
|
import {ReportRendererProvider} from './wrappers/report-renderer';
|
|
11
11
|
import {Sidebar} from './sidebar/sidebar';
|
|
12
12
|
import {Summary} from './summary/summary';
|
|
13
|
-
import {classNames, FlowResultContext,
|
|
13
|
+
import {classNames, FlowResultContext, useHashState} from './util';
|
|
14
14
|
import {Report} from './wrappers/report';
|
|
15
15
|
import {Topbar} from './topbar';
|
|
16
16
|
import {Header} from './header';
|
|
17
17
|
import {I18nProvider} from './i18n/i18n';
|
|
18
18
|
|
|
19
19
|
const Content: FunctionComponent = () => {
|
|
20
|
-
const
|
|
21
|
-
const anchor = useHashParam('anchor');
|
|
20
|
+
const hashState = useHashState();
|
|
22
21
|
const ref = useRef<HTMLDivElement>(null);
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
if (anchor) {
|
|
26
|
-
const el = document.getElementById(anchor);
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
if (hashState && hashState.anchor) {
|
|
25
|
+
const el = document.getElementById(hashState.anchor);
|
|
27
26
|
if (el) {
|
|
28
27
|
el.scrollIntoView({behavior: 'smooth'});
|
|
29
28
|
return;
|
|
@@ -32,15 +31,15 @@ const Content: FunctionComponent = () => {
|
|
|
32
31
|
|
|
33
32
|
// Scroll to top no anchor is found.
|
|
34
33
|
if (ref.current) ref.current.scrollTop = 0;
|
|
35
|
-
}, [
|
|
34
|
+
}, [hashState]);
|
|
36
35
|
|
|
37
36
|
return (
|
|
38
37
|
<div ref={ref} className="Content">
|
|
39
38
|
{
|
|
40
|
-
|
|
39
|
+
hashState ?
|
|
41
40
|
<>
|
|
42
|
-
<Header
|
|
43
|
-
<Report
|
|
41
|
+
<Header hashState={hashState}/>
|
|
42
|
+
<Report hashState={hashState}/>
|
|
44
43
|
</> :
|
|
45
44
|
<Summary/>
|
|
46
45
|
}
|
|
@@ -76,7 +76,7 @@ export const FlowStepThumbnail: FunctionComponent<{
|
|
|
76
76
|
width?: number,
|
|
77
77
|
height?: number,
|
|
78
78
|
}> = ({lhr, width, height}) => {
|
|
79
|
-
const
|
|
79
|
+
const fullPageScreenshot = getFullPageScreenshot(lhr);
|
|
80
80
|
const frames = getFilmstripFrames(lhr);
|
|
81
81
|
|
|
82
82
|
// Resize the image to fit the viewport aspect ratio.
|
|
@@ -92,16 +92,22 @@ export const FlowStepThumbnail: FunctionComponent<{
|
|
|
92
92
|
return <></>;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
let thumbnail;
|
|
96
|
+
if (frames && frames.length) {
|
|
97
|
+
thumbnail = frames[frames.length - 1].data;
|
|
98
|
+
if (lhr.gatherMode === 'timespan') {
|
|
99
|
+
return <FlowStepAnimatedThumbnail frames={frames} width={width} height={height} />;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
thumbnail = fullPageScreenshot?.screenshot.data;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
return <>
|
|
100
106
|
{
|
|
101
|
-
|
|
107
|
+
thumbnail &&
|
|
102
108
|
<img
|
|
103
109
|
className="FlowStepThumbnail"
|
|
104
|
-
src={
|
|
110
|
+
src={thumbnail}
|
|
105
111
|
style={{width, height}}
|
|
106
112
|
alt="Screenshot of a page tested by Lighthouse"
|
|
107
113
|
/>
|
|
@@ -29,10 +29,10 @@ const HeaderThumbnail: FunctionComponent<{
|
|
|
29
29
|
);
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
export const Header: FunctionComponent<{
|
|
33
|
-
({
|
|
32
|
+
export const Header: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
|
|
33
|
+
({hashState}) => {
|
|
34
34
|
const flowResult = useFlowResult();
|
|
35
|
-
const {index} =
|
|
35
|
+
const {index} = hashState;
|
|
36
36
|
|
|
37
37
|
const step = flowResult.steps[index];
|
|
38
38
|
const prevStep = flowResult.steps[index - 1];
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {FunctionComponent} from 'preact';
|
|
8
8
|
|
|
9
9
|
import {FlowSegment} from '../common';
|
|
10
|
-
import {classNames,
|
|
10
|
+
import {classNames, useHashState, useFlowResult} from '../util';
|
|
11
11
|
import {Separator} from '../common';
|
|
12
12
|
|
|
13
13
|
const SidebarFlowStep: FunctionComponent<{
|
|
@@ -41,7 +41,7 @@ const SidebarFlowSeparator: FunctionComponent = () => {
|
|
|
41
41
|
|
|
42
42
|
export const SidebarFlow: FunctionComponent = () => {
|
|
43
43
|
const flowResult = useFlowResult();
|
|
44
|
-
const
|
|
44
|
+
const hashState = useHashState();
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<div className="SidebarFlow">
|
|
@@ -61,7 +61,7 @@ export const SidebarFlow: FunctionComponent = () => {
|
|
|
61
61
|
mode={lhr.gatherMode}
|
|
62
62
|
href={url.href}
|
|
63
63
|
label={name}
|
|
64
|
-
isCurrent={index === (
|
|
64
|
+
isCurrent={index === (hashState && hashState.index)}
|
|
65
65
|
/>
|
|
66
66
|
</>;
|
|
67
67
|
})
|
|
@@ -9,11 +9,11 @@ import {FunctionComponent} from 'preact';
|
|
|
9
9
|
import {Separator} from '../common';
|
|
10
10
|
import {useI18n, useLocalizedStrings} from '../i18n/i18n';
|
|
11
11
|
import {CpuIcon, EnvIcon, SummaryIcon} from '../icons';
|
|
12
|
-
import {classNames,
|
|
12
|
+
import {classNames, useHashState, useFlowResult} from '../util';
|
|
13
13
|
import {SidebarFlow} from './flow';
|
|
14
14
|
|
|
15
15
|
export const SidebarSummary: FunctionComponent = () => {
|
|
16
|
-
const
|
|
16
|
+
const hashState = useHashState();
|
|
17
17
|
const strings = useLocalizedStrings();
|
|
18
18
|
|
|
19
19
|
const url = new URL(location.href);
|
|
@@ -21,7 +21,7 @@ export const SidebarSummary: FunctionComponent = () => {
|
|
|
21
21
|
return (
|
|
22
22
|
<a
|
|
23
23
|
href={url.href}
|
|
24
|
-
className={classNames('SidebarSummary', {'Sidebar--current':
|
|
24
|
+
className={classNames('SidebarSummary', {'Sidebar--current': hashState === null})}
|
|
25
25
|
data-testid="SidebarSummary"
|
|
26
26
|
>
|
|
27
27
|
<div className="SidebarSummary__icon">
|
|
@@ -64,8 +64,8 @@ const SummaryTooltipAudits: FunctionComponent<{category: LH.ReportResult.Categor
|
|
|
64
64
|
return audit.result.score !== null &&
|
|
65
65
|
// Metrics should not be displayed in this group.
|
|
66
66
|
audit.group !== 'metrics' &&
|
|
67
|
-
// Audits in performance
|
|
68
|
-
(audit.group !==
|
|
67
|
+
// Audits in performance group "hidden" should not be counted.
|
|
68
|
+
(audit.group !== 'hidden' || category.id !== 'performance') &&
|
|
69
69
|
// We don't want unweighted audits except for opportunities with potential savings.
|
|
70
70
|
(audit.weight > 0 || getOverallSavings(audit) > 0) &&
|
|
71
71
|
// Passing audits should never be high impact.
|
package/flow-report/src/util.ts
CHANGED
|
@@ -79,27 +79,27 @@ export function useFlowResult(): LH.FlowResult {
|
|
|
79
79
|
return flowResult;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function
|
|
83
|
-
const [
|
|
82
|
+
export function useHashParams(...params: string[]) {
|
|
83
|
+
const [paramValues, setParamValues] = useState(params.map(getHashParam));
|
|
84
84
|
|
|
85
85
|
// Use two-way-binding on the URL hash.
|
|
86
|
-
// Triggers a re-render if
|
|
86
|
+
// Triggers a re-render if any param changes.
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
function hashListener() {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
89
|
+
const newParamValues = params.map(getHashParam);
|
|
90
|
+
if (newParamValues.every((value, i) => value === paramValues[i])) return;
|
|
91
|
+
setParamValues(newParamValues);
|
|
92
92
|
}
|
|
93
93
|
window.addEventListener('hashchange', hashListener);
|
|
94
94
|
return () => window.removeEventListener('hashchange', hashListener);
|
|
95
|
-
}, [
|
|
95
|
+
}, [paramValues]);
|
|
96
96
|
|
|
97
|
-
return
|
|
97
|
+
return paramValues;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
export function
|
|
100
|
+
export function useHashState(): LH.FlowResult.HashState|null {
|
|
101
101
|
const flowResult = useFlowResult();
|
|
102
|
-
const indexString =
|
|
102
|
+
const [indexString, anchor] = useHashParams('index', 'anchor');
|
|
103
103
|
|
|
104
104
|
// Memoize result so a new object is not created on every call.
|
|
105
105
|
return useMemo(() => {
|
|
@@ -117,6 +117,6 @@ export function useCurrentLhr(): LH.FlowResult.LhrRef|null {
|
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
return {
|
|
121
|
-
}, [indexString, flowResult]);
|
|
120
|
+
return {currentLhr: step.lhr, index, anchor};
|
|
121
|
+
}, [indexString, flowResult, anchor]);
|
|
122
122
|
}
|
|
@@ -30,20 +30,25 @@ export function convertChildAnchors(element: HTMLElement, index: number) {
|
|
|
30
30
|
|
|
31
31
|
const nodeId = link.hash.substr(1);
|
|
32
32
|
link.hash = `#index=${index}&anchor=${nodeId}`;
|
|
33
|
+
link.onclick = e => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
const el = document.getElementById(nodeId);
|
|
36
|
+
if (el) el.scrollIntoView();
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
export const Report: FunctionComponent<{
|
|
37
|
-
({
|
|
41
|
+
export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
|
|
42
|
+
({hashState}) => {
|
|
38
43
|
const {dom, reportRenderer} = useReportRenderer();
|
|
39
44
|
const ref = useRef<HTMLDivElement>(null);
|
|
40
45
|
|
|
41
46
|
useLayoutEffect(() => {
|
|
42
47
|
if (ref.current) {
|
|
43
48
|
dom.clearComponentCache();
|
|
44
|
-
reportRenderer.renderReport(currentLhr
|
|
45
|
-
convertChildAnchors(ref.current,
|
|
46
|
-
const fullPageScreenshot = getFullPageScreenshot(currentLhr
|
|
49
|
+
reportRenderer.renderReport(hashState.currentLhr, ref.current);
|
|
50
|
+
convertChildAnchors(ref.current, hashState.index);
|
|
51
|
+
const fullPageScreenshot = getFullPageScreenshot(hashState.currentLhr);
|
|
47
52
|
if (fullPageScreenshot) {
|
|
48
53
|
const container = dom.find('.lh-container', ref.current);
|
|
49
54
|
ElementScreenshotRenderer.installOverlayFeature({
|
|
@@ -60,7 +65,7 @@ export const Report: FunctionComponent<{currentLhr: LH.FlowResult.LhrRef}> =
|
|
|
60
65
|
return () => {
|
|
61
66
|
if (ref.current) ref.current.textContent = '';
|
|
62
67
|
};
|
|
63
|
-
}, [reportRenderer,
|
|
68
|
+
}, [reportRenderer, hashState]);
|
|
64
69
|
|
|
65
70
|
return (
|
|
66
71
|
<div ref={ref} className="lh-root" data-testid="Report"/>
|
|
@@ -24,10 +24,19 @@ describe('FlowStepThumbnail', () => {
|
|
|
24
24
|
'full-page-screenshot': {
|
|
25
25
|
details: {
|
|
26
26
|
type: 'full-page-screenshot',
|
|
27
|
-
screenshot: {data: '
|
|
27
|
+
screenshot: {data: 'FPS', width: 400, height: 600},
|
|
28
28
|
nodes: {},
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
|
+
'screenshot-thumbnails': {
|
|
32
|
+
details: {
|
|
33
|
+
type: 'filmstrip',
|
|
34
|
+
items: [
|
|
35
|
+
{data: 'frame1'},
|
|
36
|
+
{data: 'frame2'},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
31
40
|
},
|
|
32
41
|
} as any;
|
|
33
42
|
});
|
|
@@ -63,17 +72,23 @@ describe('FlowStepThumbnail', () => {
|
|
|
63
72
|
expect(thumbnail.style.height).toEqual('150px');
|
|
64
73
|
});
|
|
65
74
|
|
|
75
|
+
it('uses last filmstrip thumbnail', () => {
|
|
76
|
+
const root = render(<FlowStepThumbnail lhr={lhr} height={150} />);
|
|
77
|
+
|
|
78
|
+
const thumbnail = root.getByAltText(/Screenshot/) as HTMLImageElement;
|
|
79
|
+
expect(thumbnail.src).toContain('frame2');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('uses full page screenshot if filmstrip unavailable', () => {
|
|
83
|
+
delete lhr.audits['screenshot-thumbnails'];
|
|
84
|
+
const root = render(<FlowStepThumbnail lhr={lhr} height={150} />);
|
|
85
|
+
|
|
86
|
+
const thumbnail = root.getByAltText(/Screenshot/) as HTMLImageElement;
|
|
87
|
+
expect(thumbnail.src).toContain('FPS');
|
|
88
|
+
});
|
|
89
|
+
|
|
66
90
|
it('renders animated thumbnail for timespan', async () => {
|
|
67
91
|
lhr.gatherMode = 'timespan';
|
|
68
|
-
lhr.audits['screenshot-thumbnails'] = {
|
|
69
|
-
details: {
|
|
70
|
-
type: 'filmstrip',
|
|
71
|
-
items: [
|
|
72
|
-
{data: 'frame1'},
|
|
73
|
-
{data: 'frame2'},
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
} as any;
|
|
77
92
|
const root = render(<FlowStepThumbnail lhr={lhr} height={150} />);
|
|
78
93
|
|
|
79
94
|
const thumbnail = root.getByAltText(/Animated/) as HTMLImageElement;
|
|
@@ -25,8 +25,8 @@ beforeEach(() => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('renders all sections for a middle step', () => {
|
|
28
|
-
const
|
|
29
|
-
const root = render(<Header
|
|
28
|
+
const hashState = {index: 1} as any;
|
|
29
|
+
const root = render(<Header hashState={hashState}/>, {wrapper});
|
|
30
30
|
|
|
31
31
|
expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
|
|
32
32
|
expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
|
|
@@ -35,8 +35,8 @@ it('renders all sections for a middle step', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('renders only next section for first step', () => {
|
|
38
|
-
const
|
|
39
|
-
const root = render(<Header
|
|
38
|
+
const hashState = {index: 0} as any;
|
|
39
|
+
const root = render(<Header hashState={hashState}/>, {wrapper});
|
|
40
40
|
|
|
41
41
|
expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeFalsy();
|
|
42
42
|
expect(root.baseElement.querySelector('.Header__prev-title')).toBeFalsy();
|
|
@@ -45,8 +45,8 @@ it('renders only next section for first step', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it('renders only previous section for last step', () => {
|
|
48
|
-
const
|
|
49
|
-
const root = render(<Header
|
|
48
|
+
const hashState = {index: 3} as any;
|
|
49
|
+
const root = render(<Header hashState={hashState}/>, {wrapper});
|
|
50
50
|
|
|
51
51
|
expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
|
|
52
52
|
expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
|
|
@@ -51,7 +51,7 @@ describe('SummaryTooltip', () => {
|
|
|
51
51
|
expect(() => root.getByText(/^[0-9]+$/)).toThrow();
|
|
52
52
|
expect(root.getByText('2 audits passed')).toBeTruthy();
|
|
53
53
|
expect(root.getByText('3 passable audits')).toBeTruthy();
|
|
54
|
-
expect(root.getByText('https://example.com'));
|
|
54
|
+
expect(root.getByText('https://example.com')).toBeTruthy();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('renders tooltip without rating', async () => {
|
|
@@ -76,7 +76,7 @@ describe('SummaryTooltip', () => {
|
|
|
76
76
|
expect(() => root.getByText(/^[0-9]+$/)).toThrow();
|
|
77
77
|
expect(root.getByText('2 audits passed')).toBeTruthy();
|
|
78
78
|
expect(root.getByText('3 passable audits')).toBeTruthy();
|
|
79
|
-
expect(root.getByText('https://example.com'));
|
|
79
|
+
expect(root.getByText('https://example.com')).toBeTruthy();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it('renders scored category tooltip with score', async () => {
|
|
@@ -101,7 +101,7 @@ describe('SummaryTooltip', () => {
|
|
|
101
101
|
expect(root.getByText('100')).toBeTruthy();
|
|
102
102
|
expect(root.getByText('2 audits passed')).toBeTruthy();
|
|
103
103
|
expect(root.getByText('3 passable audits')).toBeTruthy();
|
|
104
|
-
expect(root.getByText('https://example.com'));
|
|
104
|
+
expect(root.getByText('https://example.com')).toBeTruthy();
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
it('renders informative audit count if any', async () => {
|
|
@@ -127,7 +127,7 @@ describe('SummaryTooltip', () => {
|
|
|
127
127
|
expect(root.getByText('2 audits passed')).toBeTruthy();
|
|
128
128
|
expect(root.getByText('2 passable audits')).toBeTruthy();
|
|
129
129
|
expect(root.getByText('1 informative audit')).toBeTruthy();
|
|
130
|
-
expect(root.getByText('https://example.com'));
|
|
130
|
+
expect(root.getByText('https://example.com')).toBeTruthy();
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
it('renders highest impact audits', async () => {
|
|
@@ -192,7 +192,7 @@ describe('SummaryTooltip', () => {
|
|
|
192
192
|
auditRefs: [
|
|
193
193
|
/* eslint-disable max-len */
|
|
194
194
|
{result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'diagnostics'},
|
|
195
|
-
{result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1},
|
|
195
|
+
{result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1, group: 'hidden'},
|
|
196
196
|
{result: {score: null, scoreDisplayMode: 'informative', title: 'Audit 3'}, weight: 1, group: 'diagnostics'},
|
|
197
197
|
/* eslint-enable max-len */
|
|
198
198
|
],
|
|
@@ -9,7 +9,7 @@ import {renderHook} from '@testing-library/preact-hooks';
|
|
|
9
9
|
import {FunctionComponent} from 'preact';
|
|
10
10
|
import {act} from 'preact/test-utils';
|
|
11
11
|
|
|
12
|
-
import {FlowResultContext,
|
|
12
|
+
import {FlowResultContext, useHashState} from '../src/util';
|
|
13
13
|
import {flowResult} from './sample-flow';
|
|
14
14
|
|
|
15
15
|
let wrapper: FunctionComponent;
|
|
@@ -21,54 +21,75 @@ beforeEach(() => {
|
|
|
21
21
|
);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
describe('
|
|
24
|
+
describe('useHashState', () => {
|
|
25
25
|
it('gets current lhr index from url hash', () => {
|
|
26
26
|
global.location.hash = '#index=1';
|
|
27
|
-
const {result} = renderHook(() =>
|
|
27
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
28
28
|
expect(console.warn).not.toHaveBeenCalled();
|
|
29
29
|
expect(result.current).toEqual({
|
|
30
30
|
index: 1,
|
|
31
|
-
|
|
31
|
+
currentLhr: flowResult.steps[1].lhr,
|
|
32
|
+
anchor: null,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gets anchor id from url hash', () => {
|
|
37
|
+
global.location.hash = '#index=1&anchor=seo';
|
|
38
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
39
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
40
|
+
expect(result.current).toEqual({
|
|
41
|
+
index: 1,
|
|
42
|
+
currentLhr: flowResult.steps[1].lhr,
|
|
43
|
+
anchor: 'seo',
|
|
32
44
|
});
|
|
33
45
|
});
|
|
34
46
|
|
|
35
47
|
it('changes on navigation', async () => {
|
|
36
48
|
global.location.hash = '#index=1';
|
|
37
|
-
const render = renderHook(() =>
|
|
49
|
+
const render = renderHook(() => useHashState(), {wrapper});
|
|
38
50
|
|
|
39
51
|
expect(render.result.current).toEqual({
|
|
40
52
|
index: 1,
|
|
41
|
-
|
|
53
|
+
currentLhr: flowResult.steps[1].lhr,
|
|
54
|
+
anchor: null,
|
|
42
55
|
});
|
|
43
56
|
|
|
44
57
|
await act(() => {
|
|
45
|
-
global.location.hash = '#index=2';
|
|
58
|
+
global.location.hash = '#index=2&anchor=seo';
|
|
46
59
|
});
|
|
47
60
|
await render.waitForNextUpdate();
|
|
48
61
|
|
|
49
62
|
expect(console.warn).not.toHaveBeenCalled();
|
|
50
63
|
expect(render.result.current).toEqual({
|
|
51
64
|
index: 2,
|
|
52
|
-
|
|
65
|
+
currentLhr: flowResult.steps[2].lhr,
|
|
66
|
+
anchor: 'seo',
|
|
53
67
|
});
|
|
54
68
|
});
|
|
55
69
|
|
|
56
70
|
it('return null if lhr index is unset', () => {
|
|
57
|
-
const {result} = renderHook(() =>
|
|
71
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
58
72
|
expect(console.warn).not.toHaveBeenCalled();
|
|
59
73
|
expect(result.current).toBeNull();
|
|
60
74
|
});
|
|
61
75
|
|
|
62
76
|
it('return null if lhr index is out of bounds', () => {
|
|
63
77
|
global.location.hash = '#index=5';
|
|
64
|
-
const {result} = renderHook(() =>
|
|
78
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
65
79
|
expect(console.warn).toHaveBeenCalled();
|
|
66
80
|
expect(result.current).toBeNull();
|
|
67
81
|
});
|
|
68
82
|
|
|
69
83
|
it('returns null for invalid value', () => {
|
|
70
84
|
global.location.hash = '#index=OHNO';
|
|
71
|
-
const {result} = renderHook(() =>
|
|
85
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
86
|
+
expect(console.warn).toHaveBeenCalled();
|
|
87
|
+
expect(result.current).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns null for invalid value with valid anchor', () => {
|
|
91
|
+
global.location.hash = '#index=OHNO&anchor=seo';
|
|
92
|
+
const {result} = renderHook(() => useHashState(), {wrapper});
|
|
72
93
|
expect(console.warn).toHaveBeenCalled();
|
|
73
94
|
expect(result.current).toBeNull();
|
|
74
95
|
});
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* Currently uses `lighthouse-dt-bundle.js`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
|
|
14
16
|
import ChromeLauncher from 'chrome-launcher';
|
|
15
17
|
|
|
16
18
|
import ChromeProtocol from '../../../../lighthouse-core/gather/connections/cri.js';
|
|
19
|
+
import {LH_ROOT} from '../../../../root.js';
|
|
17
20
|
|
|
18
21
|
const originalRequire = global.require;
|
|
19
22
|
if (typeof globalThis === 'undefined') {
|
|
@@ -22,8 +25,7 @@ if (typeof globalThis === 'undefined') {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
// Load bundle, which creates a `global.runBundledLighthouse`.
|
|
25
|
-
|
|
26
|
-
import '../../../../dist/lighthouse-dt-bundle.js'; // eslint-disable-line
|
|
28
|
+
eval(fs.readFileSync(LH_ROOT + '/dist/lighthouse-dt-bundle.js', 'utf-8'));
|
|
27
29
|
|
|
28
30
|
global.require = originalRequire;
|
|
29
31
|
|
|
@@ -10,6 +10,7 @@ const LanternMetric = require('./lantern-metric.js');
|
|
|
10
10
|
const BaseNode = require('../../lib/dependency-graph/base-node.js');
|
|
11
11
|
const LanternFirstContentfulPaint = require('./lantern-first-contentful-paint.js');
|
|
12
12
|
const LanternInteractive = require('./lantern-interactive.js');
|
|
13
|
+
const {BLOCKING_TIME_THRESHOLD, calculateSumOfBlockingTime} = require('./tbt-utils.js');
|
|
13
14
|
|
|
14
15
|
/** @typedef {BaseNode.Node} Node */
|
|
15
16
|
|
|
@@ -66,9 +67,7 @@ class LanternTotalBlockingTime extends LanternMetric {
|
|
|
66
67
|
? extras.interactiveResult.optimisticEstimate.timeInMs
|
|
67
68
|
: extras.interactiveResult.pessimisticEstimate.timeInMs;
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
const TotalBlockingTime = require('./total-blocking-time.js');
|
|
71
|
-
const minDurationMs = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
|
|
70
|
+
const minDurationMs = BLOCKING_TIME_THRESHOLD;
|
|
72
71
|
|
|
73
72
|
const events = LanternTotalBlockingTime.getTopLevelEvents(
|
|
74
73
|
simulation.nodeTimings,
|
|
@@ -76,7 +75,7 @@ class LanternTotalBlockingTime extends LanternMetric {
|
|
|
76
75
|
);
|
|
77
76
|
|
|
78
77
|
return {
|
|
79
|
-
timeInMs:
|
|
78
|
+
timeInMs: calculateSumOfBlockingTime(
|
|
80
79
|
events,
|
|
81
80
|
fcpTimeInMs,
|
|
82
81
|
interactiveTimeMs
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const BLOCKING_TIME_THRESHOLD = 50;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
|
|
12
|
+
* @param {number} startTimeMs
|
|
13
|
+
* @param {number} endTimeMs
|
|
14
|
+
* @return {number}
|
|
15
|
+
*/
|
|
16
|
+
function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
17
|
+
if (endTimeMs <= startTimeMs) return 0;
|
|
18
|
+
|
|
19
|
+
const threshold = BLOCKING_TIME_THRESHOLD;
|
|
20
|
+
let sumBlockingTime = 0;
|
|
21
|
+
for (const event of topLevelEvents) {
|
|
22
|
+
// Early exit for small tasks, which should far outnumber long tasks.
|
|
23
|
+
if (event.duration < threshold) continue;
|
|
24
|
+
|
|
25
|
+
// We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
|
|
26
|
+
// FCP is picked as the lower bound because there is little risk of user input happening
|
|
27
|
+
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
28
|
+
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
29
|
+
if (event.end < startTimeMs) continue;
|
|
30
|
+
|
|
31
|
+
// TTI is picked as the upper bound because we want a well defined end point for page load.
|
|
32
|
+
if (event.start > endTimeMs) continue;
|
|
33
|
+
|
|
34
|
+
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
|
|
35
|
+
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
|
|
36
|
+
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
|
|
37
|
+
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
|
|
38
|
+
// after FCP.
|
|
39
|
+
const clippedStart = Math.max(event.start, startTimeMs);
|
|
40
|
+
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
41
|
+
const clippedDuration = clippedEnd - clippedStart;
|
|
42
|
+
if (clippedDuration < threshold) continue;
|
|
43
|
+
|
|
44
|
+
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
|
|
45
|
+
// Example:
|
|
46
|
+
// [ 250ms Task ]
|
|
47
|
+
// | First 50ms | Blocking Region (200ms) |
|
|
48
|
+
sumBlockingTime += clippedDuration - threshold;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sumBlockingTime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
BLOCKING_TIME_THRESHOLD,
|
|
56
|
+
calculateSumOfBlockingTime,
|
|
57
|
+
};
|