lighthouse 8.6.0-dev.20211028 → 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.
Files changed (102) hide show
  1. package/dist/report/flow.js +7 -7
  2. package/dist/report/standalone.js +7 -7
  3. package/flow-report/src/app.tsx +10 -11
  4. package/flow-report/src/common.tsx +11 -5
  5. package/flow-report/src/header.tsx +3 -3
  6. package/flow-report/src/sidebar/flow.tsx +3 -3
  7. package/flow-report/src/sidebar/sidebar.tsx +3 -3
  8. package/flow-report/src/summary/category.tsx +2 -2
  9. package/flow-report/src/util.ts +12 -12
  10. package/flow-report/src/wrappers/report.tsx +11 -6
  11. package/flow-report/test/common-test.tsx +25 -10
  12. package/flow-report/test/flow-report-pptr-test.ts +46 -0
  13. package/flow-report/test/header-test.tsx +6 -6
  14. package/flow-report/test/summary/category-test.tsx +5 -5
  15. package/flow-report/test/util-test.tsx +32 -11
  16. package/jest.config.js +1 -0
  17. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
  18. package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
  19. package/lighthouse-core/audits/errors-in-console.js +0 -2
  20. package/lighthouse-core/audits/image-aspect-ratio.js +1 -2
  21. package/lighthouse-core/audits/image-size-responsive.js +4 -5
  22. package/lighthouse-core/audits/unsized-images.js +1 -1
  23. package/lighthouse-core/computed/image-records.js +4 -3
  24. package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
  25. package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
  26. package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
  27. package/lighthouse-core/config/config-helpers.js +20 -10
  28. package/lighthouse-core/config/default-config.js +59 -56
  29. package/lighthouse-core/fraggle-rock/config/default-config.js +1 -4
  30. package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
  31. package/lighthouse-core/gather/driver/storage.js +0 -1
  32. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
  33. package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
  34. package/lighthouse-core/lib/asset-saver.js +4 -1
  35. package/lighthouse-core/lib/lh-env.js +2 -0
  36. package/lighthouse-core/lib/tappable-rects.js +8 -15
  37. package/lighthouse-core/runner.js +1 -1
  38. package/lighthouse-core/util-commonjs.js +1 -2
  39. package/package.json +10 -7
  40. package/report/clients/standalone.js +0 -2
  41. package/report/renderer/element-screenshot-renderer.js +2 -2
  42. package/report/renderer/performance-category-renderer.js +20 -5
  43. package/report/renderer/util.js +1 -2
  44. package/report/test/renderer/performance-category-renderer-test.js +34 -8
  45. package/report/test/renderer/report-renderer-test.js +1 -1
  46. package/report/test/renderer/report-ui-features-test.js +3 -0
  47. package/report/test/renderer/util-test.js +13 -31
  48. package/shared/localization/format.js +9 -4
  49. package/shared/localization/locales/ar-XB.json +0 -12
  50. package/shared/localization/locales/ar.json +0 -12
  51. package/shared/localization/locales/bg.json +0 -12
  52. package/shared/localization/locales/ca.json +0 -12
  53. package/shared/localization/locales/cs.json +0 -12
  54. package/shared/localization/locales/da.json +0 -12
  55. package/shared/localization/locales/de.json +0 -12
  56. package/shared/localization/locales/el.json +0 -12
  57. package/shared/localization/locales/en-GB.json +0 -12
  58. package/shared/localization/locales/en-US.json +1 -13
  59. package/shared/localization/locales/en-XA.json +0 -12
  60. package/shared/localization/locales/en-XL.json +1 -13
  61. package/shared/localization/locales/es-419.json +0 -12
  62. package/shared/localization/locales/es.json +0 -12
  63. package/shared/localization/locales/fi.json +0 -12
  64. package/shared/localization/locales/fil.json +0 -12
  65. package/shared/localization/locales/fr.json +0 -12
  66. package/shared/localization/locales/he.json +0 -12
  67. package/shared/localization/locales/hi.json +0 -12
  68. package/shared/localization/locales/hr.json +0 -12
  69. package/shared/localization/locales/hu.json +0 -12
  70. package/shared/localization/locales/id.json +0 -12
  71. package/shared/localization/locales/it.json +0 -12
  72. package/shared/localization/locales/ja.json +0 -12
  73. package/shared/localization/locales/ko.json +0 -12
  74. package/shared/localization/locales/lt.json +0 -12
  75. package/shared/localization/locales/lv.json +0 -12
  76. package/shared/localization/locales/nl.json +0 -12
  77. package/shared/localization/locales/no.json +0 -12
  78. package/shared/localization/locales/pl.json +0 -12
  79. package/shared/localization/locales/pt-PT.json +0 -12
  80. package/shared/localization/locales/pt.json +0 -12
  81. package/shared/localization/locales/ro.json +0 -12
  82. package/shared/localization/locales/ru.json +0 -12
  83. package/shared/localization/locales/sk.json +0 -12
  84. package/shared/localization/locales/sl.json +0 -12
  85. package/shared/localization/locales/sr-Latn.json +0 -12
  86. package/shared/localization/locales/sr.json +0 -12
  87. package/shared/localization/locales/sv.json +0 -12
  88. package/shared/localization/locales/ta.json +0 -12
  89. package/shared/localization/locales/te.json +0 -12
  90. package/shared/localization/locales/th.json +0 -12
  91. package/shared/localization/locales/tr.json +0 -12
  92. package/shared/localization/locales/uk.json +0 -12
  93. package/shared/localization/locales/vi.json +0 -12
  94. package/shared/localization/locales/zh-HK.json +0 -12
  95. package/shared/localization/locales/zh-TW.json +0 -12
  96. package/shared/localization/locales/zh.json +0 -12
  97. package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
  98. package/types/artifacts.d.ts +5 -4
  99. package/types/lhr/flow.d.ts +3 -2
  100. package/types/rollup-plugin-postprocess.d.ts +10 -0
  101. package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
  102. package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
@@ -5,25 +5,24 @@
5
5
  */
6
6
 
7
7
  import {FunctionComponent} from 'preact';
8
- import {useEffect, useRef, useState} from 'preact/hooks';
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, useCurrentLhr, useHashParam} from './util';
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 currentLhr = useCurrentLhr();
21
- const anchor = useHashParam('anchor');
20
+ const hashState = useHashState();
22
21
  const ref = useRef<HTMLDivElement>(null);
23
22
 
24
- useEffect(() => {
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
- }, [anchor, currentLhr]);
34
+ }, [hashState]);
36
35
 
37
36
  return (
38
37
  <div ref={ref} className="Content">
39
38
  {
40
- currentLhr ?
39
+ hashState ?
41
40
  <>
42
- <Header currentLhr={currentLhr}/>
43
- <Report currentLhr={currentLhr}/>
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 screenshot = getFullPageScreenshot(lhr);
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
- if (lhr.gatherMode === 'timespan' && frames && frames.length) {
96
- return <FlowStepAnimatedThumbnail frames={frames} width={width} height={height} />;
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
- screenshot &&
107
+ thumbnail &&
102
108
  <img
103
109
  className="FlowStepThumbnail"
104
- src={screenshot.screenshot.data}
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<{currentLhr: LH.FlowResult.LhrRef}> =
33
- ({currentLhr}) => {
32
+ export const Header: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
33
+ ({hashState}) => {
34
34
  const flowResult = useFlowResult();
35
- const {index} = currentLhr;
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, useCurrentLhr, useFlowResult} from '../util';
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 currentLhr = useCurrentLhr();
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 === (currentLhr && currentLhr.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, useCurrentLhr, useFlowResult} from '../util';
12
+ import {classNames, useHashState, useFlowResult} from '../util';
13
13
  import {SidebarFlow} from './flow';
14
14
 
15
15
  export const SidebarSummary: FunctionComponent = () => {
16
- const currentLhr = useCurrentLhr();
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': currentLhr === null})}
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 without a group are hidden.
68
- (audit.group !== undefined || category.id !== 'performance') &&
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.
@@ -79,27 +79,27 @@ export function useFlowResult(): LH.FlowResult {
79
79
  return flowResult;
80
80
  }
81
81
 
82
- export function useHashParam(param: string) {
83
- const [paramValue, setParamValue] = useState(getHashParam(param));
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 the param changes.
86
+ // Triggers a re-render if any param changes.
87
87
  useEffect(() => {
88
88
  function hashListener() {
89
- const newIndexString = getHashParam(param);
90
- if (newIndexString === paramValue) return;
91
- setParamValue(newIndexString);
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
- }, [paramValue]);
95
+ }, [paramValues]);
96
96
 
97
- return paramValue;
97
+ return paramValues;
98
98
  }
99
99
 
100
- export function useCurrentLhr(): LH.FlowResult.LhrRef|null {
100
+ export function useHashState(): LH.FlowResult.HashState|null {
101
101
  const flowResult = useFlowResult();
102
- const indexString = useHashParam('index');
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 {value: step.lhr, index};
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<{currentLhr: LH.FlowResult.LhrRef}> =
37
- ({currentLhr}) => {
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.value, ref.current);
45
- convertChildAnchors(ref.current, currentLhr.index);
46
- const fullPageScreenshot = getFullPageScreenshot(currentLhr.value);
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, currentLhr]);
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: 'baef01', width: 400, height: 600},
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;
@@ -0,0 +1,46 @@
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 {jest} from '@jest/globals';
8
+ import puppeteer, {Browser, Page} from 'puppeteer';
9
+
10
+ import ReportGenerator from '../../report/generator/report-generator.js';
11
+ import {flowResult} from './sample-flow';
12
+
13
+ jest.setTimeout(35_000);
14
+
15
+ describe('Lighthouse Flow Report', () => {
16
+ console.log('\n✨ Be sure to have recently run this: yarn build-report');
17
+
18
+ let browser: Browser;
19
+ let page: Page;
20
+ const pageErrors: Error[] = [];
21
+
22
+ beforeAll(async () => {
23
+ browser = await puppeteer.launch({
24
+ headless: true,
25
+ });
26
+ page = await browser.newPage();
27
+ page.on('pageerror', pageError => pageErrors.push(pageError));
28
+ });
29
+
30
+ afterAll(async () => {
31
+ if (pageErrors.length > 0) console.error(pageErrors);
32
+
33
+ await browser.close();
34
+ });
35
+
36
+ describe('Renders the flow report', () => {
37
+ beforeAll(async () => {
38
+ const html = ReportGenerator.generateFlowReportHtml(flowResult);
39
+ await page.setContent(html);
40
+ });
41
+
42
+ it('should load with no errors', async () => {
43
+ expect(pageErrors).toHaveLength(0);
44
+ });
45
+ });
46
+ });
@@ -25,8 +25,8 @@ beforeEach(() => {
25
25
  });
26
26
 
27
27
  it('renders all sections for a middle step', () => {
28
- const currentLhr = {index: 1} as any;
29
- const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
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 currentLhr = {index: 0} as any;
39
- const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
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 currentLhr = {index: 3} as any;
49
- const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
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, useCurrentLhr} from '../src/util';
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('useCurrentLhr', () => {
24
+ describe('useHashState', () => {
25
25
  it('gets current lhr index from url hash', () => {
26
26
  global.location.hash = '#index=1';
27
- const {result} = renderHook(() => useCurrentLhr(), {wrapper});
27
+ const {result} = renderHook(() => useHashState(), {wrapper});
28
28
  expect(console.warn).not.toHaveBeenCalled();
29
29
  expect(result.current).toEqual({
30
30
  index: 1,
31
- value: flowResult.steps[1].lhr,
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(() => useCurrentLhr(), {wrapper});
49
+ const render = renderHook(() => useHashState(), {wrapper});
38
50
 
39
51
  expect(render.result.current).toEqual({
40
52
  index: 1,
41
- value: flowResult.steps[1].lhr,
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
- value: flowResult.steps[2].lhr,
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(() => useCurrentLhr(), {wrapper});
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(() => useCurrentLhr(), {wrapper});
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(() => useCurrentLhr(), {wrapper});
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
  });
package/jest.config.js CHANGED
@@ -20,6 +20,7 @@ module.exports = {
20
20
  '**/shared/**/*-test.js',
21
21
  '**/build/**/*-test.js',
22
22
  ],
23
+ modulePathIgnorePatterns: ['<rootDir>/.tmp'],
23
24
  transform: {},
24
25
  prettierPath: null,
25
26
  projects: [
@@ -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
- // @ts-ignore - file exists if `yarn build-all` is run, but not used for types anyways.
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
 
@@ -215,7 +215,7 @@ function logChildProcessError(localConsole, err) {
215
215
  localConsole.adoptStdStrings(err);
216
216
  }
217
217
 
218
- localConsole.log(log.redify('Error: ') + err.message);
218
+ localConsole.log(log.redify('Error: ') + err + '\n' + err.stack);
219
219
  }
220
220
 
221
221
  /**
@@ -88,8 +88,6 @@ class ErrorLogs extends Audit {
88
88
  return {
89
89
  source: item.source,
90
90
  description: item.text,
91
- // TODO: remove for v8 (url is covered in sourceLocation)
92
- url: item.url,
93
91
  sourceLocation: Audit.makeSourceLocationFromConsoleMessage(item),
94
92
  };
95
93
  });
@@ -89,8 +89,7 @@ class ImageAspectRatio extends Audit {
89
89
  // - filter all svgs as they have no natural dimensions to audit
90
90
  // - filter out images that have falsy naturalWidth or naturalHeight
91
91
  return !image.isCss &&
92
- image.mimeType &&
93
- image.mimeType !== 'image/svg+xml' &&
92
+ URL.guessMimeType(image.src) !== 'image/svg+xml' &&
94
93
  image.naturalDimensions &&
95
94
  image.naturalDimensions.height > 5 &&
96
95
  image.naturalDimensions.width > 5 &&
@@ -45,7 +45,7 @@ const LARGE_IMAGE_FACTOR = 0.75;
45
45
  // considered SMALL.
46
46
  const SMALL_IMAGE_THRESHOLD = 64;
47
47
 
48
- /** @typedef {{url: string, node: LH.Audit.Details.NodeValue, elidedUrl: string, displayedSize: string, actualSize: string, actualPixels: number, expectedSize: string, expectedPixels: number}} Result */
48
+ /** @typedef {{url: string, node: LH.Audit.Details.NodeValue, displayedSize: string, actualSize: string, actualPixels: number, expectedSize: string, expectedPixels: number}} Result */
49
49
 
50
50
  /**
51
51
  * @param {{top: number, bottom: number, left: number, right: number}} imageRect
@@ -95,7 +95,7 @@ function isCandidate(image) {
95
95
  ) {
96
96
  return false;
97
97
  }
98
- if (image.mimeType === 'image/svg+xml') {
98
+ if (URL.guessMimeType(image.src) === 'image/svg+xml') {
99
99
  return false;
100
100
  }
101
101
  if (image.isCss) {
@@ -146,9 +146,8 @@ function getResult(image, DPR) {
146
146
  const [expectedWidth, expectedHeight] =
147
147
  expectedImageSize(image.displayedWidth, image.displayedHeight, DPR);
148
148
  return {
149
- url: image.src,
149
+ url: URL.elideDataURI(image.src),
150
150
  node: Audit.makeNodeItem(image.node),
151
- elidedUrl: URL.elideDataURI(image.src),
152
151
  displayedSize: `${image.displayedWidth} x ${image.displayedHeight}`,
153
152
  actualSize: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}`,
154
153
  actualPixels: image.naturalDimensions.width * image.naturalDimensions.height,
@@ -265,7 +264,7 @@ class ImageSizeResponsive extends Audit {
265
264
  /** @type {LH.Audit.Details.Table['headings']} */
266
265
  const headings = [
267
266
  {key: 'node', itemType: 'node', text: ''},
268
- {key: 'elidedUrl', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
267
+ {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
269
268
  {key: 'displayedSize', itemType: 'text', text: str_(UIStrings.columnDisplayed)},
270
269
  {key: 'actualSize', itemType: 'text', text: str_(UIStrings.columnActual)},
271
270
  {key: 'expectedSize', itemType: 'text', text: str_(UIStrings.columnExpected)},
@@ -106,7 +106,7 @@ class UnsizedImages extends Audit {
106
106
  * @return {boolean}
107
107
  */
108
108
  static isNonNetworkSvg(image) {
109
- const isSvg = image.mimeType === 'image/svg+xml';
109
+ const isSvg = URL.guessMimeType(image.src) === 'image/svg+xml';
110
110
  const urlScheme = image.src.slice(0, image.src.indexOf(':'));
111
111
  const isNonNetwork = URL.isNonNetworkProtocol(urlScheme);
112
112
  return isSvg && isNonNetwork;