lighthouse 8.5.1 → 8.6.0-dev.20211016

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 (173) hide show
  1. package/changelog.md +5490 -0
  2. package/dist/report/flow.js +35 -12
  3. package/dist/report/standalone.js +20 -21
  4. package/flow-report/.eslintrc.cjs +49 -0
  5. package/flow-report/assets/styles.css +272 -9
  6. package/flow-report/jest.config.js +1 -1
  7. package/flow-report/package.json +4 -0
  8. package/flow-report/src/app.tsx +31 -9
  9. package/flow-report/src/common.tsx +86 -7
  10. package/flow-report/src/header.tsx +88 -0
  11. package/flow-report/src/help-dialog.tsx +120 -0
  12. package/flow-report/src/i18n/i18n.tsx +52 -0
  13. package/flow-report/src/i18n/localized-strings.js +11 -0
  14. package/flow-report/src/i18n/ui-strings.js +134 -0
  15. package/flow-report/src/icons.tsx +20 -3
  16. package/flow-report/src/sidebar/sidebar.tsx +11 -16
  17. package/flow-report/src/summary/category.tsx +39 -24
  18. package/flow-report/src/summary/summary.tsx +35 -32
  19. package/flow-report/src/topbar.tsx +54 -4
  20. package/flow-report/src/util.ts +26 -1
  21. package/flow-report/src/wrappers/report-renderer.tsx +1 -1
  22. package/flow-report/src/wrappers/report.tsx +6 -21
  23. package/flow-report/test/app-test.tsx +17 -13
  24. package/flow-report/test/common-test.tsx +89 -0
  25. package/flow-report/test/header-test.tsx +55 -0
  26. package/flow-report/test/sample-flow.ts +17 -0
  27. package/flow-report/test/setup/env-setup.ts +1 -0
  28. package/flow-report/test/sidebar/flow-test.tsx +6 -12
  29. package/flow-report/test/sidebar/sidebar-test.tsx +7 -13
  30. package/flow-report/test/summary/category-test.tsx +83 -34
  31. package/flow-report/test/summary/summary-test.tsx +12 -18
  32. package/flow-report/test/topbar-test.tsx +68 -0
  33. package/flow-report/test/util-test.tsx +1 -12
  34. package/flow-report/tsconfig.json +2 -0
  35. package/flow-report/types/flow-report.d.ts +2 -1
  36. package/jest.config.js +1 -15
  37. package/lighthouse-cli/bin.js +5 -0
  38. package/lighthouse-cli/cli-flags.js +10 -3
  39. package/lighthouse-cli/commands/commands.js +1 -0
  40. package/lighthouse-cli/commands/list-locales.js +16 -0
  41. package/lighthouse-cli/run.js +2 -2
  42. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +0 -0
  43. package/lighthouse-cli/test/smokehouse/report-assert.js +20 -4
  44. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -1
  45. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +1 -1
  46. package/lighthouse-core/audits/seo/crawlable-anchors.js +10 -12
  47. package/lighthouse-core/computed/js-bundles.js +1 -2
  48. package/lighthouse-core/computed/unused-javascript-summary.js +3 -3
  49. package/lighthouse-core/config/config-helpers.js +1 -0
  50. package/lighthouse-core/config/config.js +2 -2
  51. package/lighthouse-core/config/default-config.js +5 -0
  52. package/lighthouse-core/fraggle-rock/api.js +10 -0
  53. package/lighthouse-core/fraggle-rock/config/filters.js +22 -3
  54. package/lighthouse-core/fraggle-rock/gather/driver.js +4 -0
  55. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +2 -2
  56. package/lighthouse-core/fraggle-rock/user-flow.js +35 -6
  57. package/lighthouse-core/gather/driver/navigation.js +5 -0
  58. package/lighthouse-core/gather/driver/prepare.js +14 -0
  59. package/lighthouse-core/gather/driver/storage.js +5 -0
  60. package/lighthouse-core/gather/gather-runner.js +2 -2
  61. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +5 -1
  62. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +30 -24
  63. package/lighthouse-core/lib/cdt/Platform.js +55 -0
  64. package/lighthouse-core/lib/cdt/SDK.js +3 -123
  65. package/lighthouse-core/lib/cdt/generated/SourceMap.js +148 -312
  66. package/lighthouse-core/lib/csp-evaluator.js +2 -1
  67. package/lighthouse-core/lib/i18n/README.md +6 -6
  68. package/lighthouse-core/lib/i18n/i18n.js +26 -348
  69. package/lighthouse-core/lib/page-functions.js +4 -4
  70. package/lighthouse-core/lib/stack-packs.js +1 -13
  71. package/lighthouse-core/runner.js +6 -6
  72. package/lighthouse-core/scripts/manual-chrome-launcher.js +4 -1
  73. package/lighthouse-core/scripts/package.json +4 -0
  74. package/lighthouse-core/util-commonjs.js +20 -4
  75. package/package.json +19 -16
  76. package/readme.md +1 -1
  77. package/report/assets/standalone-template.html +0 -1
  78. package/report/assets/styles.css +16 -41
  79. package/report/assets/templates.html +42 -4
  80. package/report/clients/psi.js +1 -0
  81. package/report/clients/standalone.js +1 -2
  82. package/report/generator/file-namer.js +17 -8
  83. package/report/generator/report-generator.js +0 -1
  84. package/report/renderer/category-renderer.js +4 -16
  85. package/report/renderer/components.js +107 -71
  86. package/report/renderer/dom.js +19 -0
  87. package/report/renderer/logger.js +35 -2
  88. package/report/renderer/performance-category-renderer.js +23 -21
  89. package/report/renderer/pwa-category-renderer.js +1 -2
  90. package/report/renderer/report-renderer.js +21 -0
  91. package/report/renderer/report-ui-features.js +7 -19
  92. package/report/renderer/swap-locale-feature.js +82 -0
  93. package/report/renderer/util.js +20 -4
  94. package/report/test/clients/psi-test.js +0 -4
  95. package/report/test/generator/file-namer-test.js +2 -2
  96. package/report/test/renderer/category-renderer-test.js +1 -1
  97. package/report/test/renderer/performance-category-renderer-test.js +12 -0
  98. package/report/test/renderer/report-renderer-axe-test.js +86 -0
  99. package/report/test/renderer/report-renderer-test.js +0 -55
  100. package/report/test/renderer/util-test.js +68 -8
  101. package/report/test-assets/faux-psi-template.html +0 -1
  102. package/report/tsconfig.json +2 -1
  103. package/report/types/html-renderer.d.ts +2 -1
  104. package/root.js +18 -0
  105. package/shared/localization/format.js +455 -0
  106. package/{lighthouse-core/lib/cdt/Common.js → shared/localization/i18n-module.js} +3 -8
  107. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ar-XB.json +139 -4
  108. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ar.json +141 -6
  109. package/{lighthouse-core/lib/i18n → shared/localization}/locales/bg.json +139 -4
  110. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ca.json +140 -5
  111. package/{lighthouse-core/lib/i18n → shared/localization}/locales/cs.json +139 -4
  112. package/{lighthouse-core/lib/i18n → shared/localization}/locales/da.json +139 -4
  113. package/{lighthouse-core/lib/i18n → shared/localization}/locales/de.json +139 -4
  114. package/{lighthouse-core/lib/i18n → shared/localization}/locales/el.json +141 -6
  115. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-GB.json +139 -4
  116. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-US.json +129 -0
  117. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-XA.json +139 -4
  118. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-XL.json +129 -0
  119. package/{lighthouse-core/lib/i18n → shared/localization}/locales/es-419.json +139 -4
  120. package/{lighthouse-core/lib/i18n → shared/localization}/locales/es.json +139 -4
  121. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fi.json +139 -4
  122. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fil.json +139 -4
  123. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fr.json +139 -4
  124. package/{lighthouse-core/lib/i18n → shared/localization}/locales/he.json +139 -4
  125. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hi.json +142 -7
  126. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hr.json +139 -4
  127. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hu.json +139 -4
  128. package/{lighthouse-core/lib/i18n → shared/localization}/locales/id.json +139 -4
  129. package/{lighthouse-core/lib/i18n → shared/localization}/locales/it.json +139 -4
  130. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ja.json +139 -4
  131. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ko.json +139 -4
  132. package/{lighthouse-core/lib/i18n → shared/localization}/locales/lt.json +139 -4
  133. package/{lighthouse-core/lib/i18n → shared/localization}/locales/lv.json +139 -4
  134. package/{lighthouse-core/lib/i18n → shared/localization}/locales/nl.json +139 -4
  135. package/{lighthouse-core/lib/i18n → shared/localization}/locales/no.json +139 -4
  136. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pl.json +139 -4
  137. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pt-PT.json +139 -4
  138. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pt.json +139 -4
  139. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ro.json +139 -4
  140. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ru.json +139 -4
  141. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sk.json +139 -4
  142. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sl.json +139 -4
  143. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sr-Latn.json +139 -4
  144. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sr.json +139 -4
  145. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sv.json +139 -4
  146. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ta.json +146 -11
  147. package/{lighthouse-core/lib/i18n → shared/localization}/locales/te.json +179 -44
  148. package/{lighthouse-core/lib/i18n → shared/localization}/locales/th.json +139 -4
  149. package/{lighthouse-core/lib/i18n → shared/localization}/locales/tr.json +139 -4
  150. package/{lighthouse-core/lib/i18n → shared/localization}/locales/uk.json +139 -4
  151. package/{lighthouse-core/lib/i18n → shared/localization}/locales/vi.json +139 -4
  152. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh-HK.json +139 -4
  153. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh-TW.json +139 -4
  154. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh.json +139 -4
  155. package/{lighthouse-core/lib/i18n → shared/localization}/locales.js +8 -1
  156. package/shared/localization/swap-flow-locale.js +20 -0
  157. package/{lighthouse-core/lib/i18n → shared/localization}/swap-locale.js +9 -7
  158. package/shared/test/localization/format-test.js +421 -0
  159. package/shared/test/localization/locales-test.js +47 -0
  160. package/shared/test/localization/swap-locale-test.js +140 -0
  161. package/shared/tsconfig.json +22 -0
  162. package/{lighthouse-core/lib → shared}/type-verifiers.js +0 -0
  163. package/shared/types/shared.d.ts +24 -0
  164. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +5 -3
  165. package/third-party/chromium-synchronization/installability-errors-test.js +2 -1
  166. package/tsconfig-all.json +1 -0
  167. package/tsconfig-base.json +1 -1
  168. package/tsconfig.json +2 -3
  169. package/types/config.d.ts +1 -0
  170. package/types/es-main.d.ts +18 -0
  171. package/types/externs.d.ts +2 -0
  172. package/types/lhr/flow.d.ts +7 -0
  173. package/types/lhr/i18n.d.ts +0 -3
@@ -7,28 +7,36 @@
7
7
  import {FunctionComponent} from 'preact';
8
8
  import {useMemo} from 'preact/hooks';
9
9
 
10
- import {FlowSegment, Separator} from '../common';
11
- import {getScreenDimensions, getScreenshot, useFlowResult} from '../util';
10
+ import {FlowSegment, FlowStepThumbnail, Separator} from '../common';
11
+ import {getModeDescription, useFlowResult} from '../util';
12
12
  import {Util} from '../../../report/renderer/util';
13
13
  import {SummaryCategory} from './category';
14
+ import {useStringFormatter, useUIStrings} from '../i18n/i18n';
14
15
 
15
16
  const DISPLAYED_CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo'];
16
17
  const THUMBNAIL_WIDTH = 50;
17
- const MODE_DESCRIPTIONS: Record<LH.Result.GatherMode, string> = {
18
- 'navigation': 'Page load',
19
- 'timespan': 'User interactions',
20
- 'snapshot': 'Captured state of page',
21
- };
22
18
 
23
- const SummaryNavigationHeader: FunctionComponent<{url: string}> = ({url}) => {
19
+ const SummaryNavigationHeader: FunctionComponent<{lhr: LH.Result}> = ({lhr}) => {
20
+ const strings = useUIStrings();
21
+
24
22
  return (
25
23
  <div className="SummaryNavigationHeader" data-testid="SummaryNavigationHeader">
26
24
  <FlowSegment/>
27
- <div className="SummaryNavigationHeader__url">{url}</div>
28
- <div className="SummaryNavigationHeader__category">Performance</div>
29
- <div className="SummaryNavigationHeader__category">Accessibility</div>
30
- <div className="SummaryNavigationHeader__category">Best Practices</div>
31
- <div className="SummaryNavigationHeader__category">SEO</div>
25
+ <div className="SummaryNavigationHeader__url">
26
+ <a rel="noopener" target="_blank" href={lhr.finalUrl}>{lhr.finalUrl}</a>
27
+ </div>
28
+ <div className="SummaryNavigationHeader__category">
29
+ {strings.categoryPerformance}
30
+ </div>
31
+ <div className="SummaryNavigationHeader__category">
32
+ {strings.categoryAccessibility}
33
+ </div>
34
+ <div className="SummaryNavigationHeader__category">
35
+ {strings.categoryBestPractices}
36
+ </div>
37
+ <div className="SummaryNavigationHeader__category">
38
+ {strings.categorySeo}
39
+ </div>
32
40
  </div>
33
41
  );
34
42
  };
@@ -42,32 +50,23 @@ export const SummaryFlowStep: FunctionComponent<{
42
50
  hashIndex: number,
43
51
  }> = ({lhr, label, hashIndex}) => {
44
52
  const reportResult = useMemo(() => Util.prepareReportResult(lhr), [lhr]);
45
-
46
- const screenshot = reportResult.gatherMode !== 'timespan' ? getScreenshot(reportResult) : null;
47
-
48
- // Crop the displayed image to the viewport dimensions.
49
- const {width, height} = getScreenDimensions(reportResult);
50
- const thumbnailHeight = height * THUMBNAIL_WIDTH / width;
53
+ const strings = useUIStrings();
54
+ const modeDescription = getModeDescription(lhr.gatherMode, strings);
51
55
 
52
56
  return (
53
57
  <div className="SummaryFlowStep">
54
58
  {
55
59
  lhr.gatherMode === 'navigation' || hashIndex === 0 ?
56
- <SummaryNavigationHeader url={lhr.finalUrl}/> :
60
+ <SummaryNavigationHeader lhr={lhr}/> :
57
61
  <div className="SummaryFlowStep__separator">
58
62
  <FlowSegment/>
59
63
  <Separator/>
60
64
  </div>
61
65
  }
62
- <img
63
- className="SummaryFlowStep__screenshot"
64
- data-testid="SummaryFlowStep__screenshot"
65
- src={screenshot || undefined}
66
- style={{width: THUMBNAIL_WIDTH, maxHeight: thumbnailHeight}}
67
- />
66
+ <FlowStepThumbnail reportResult={reportResult} width={THUMBNAIL_WIDTH}/>
68
67
  <FlowSegment mode={lhr.gatherMode}/>
69
68
  <div className="SummaryFlowStep__label">
70
- <div className="SummaryFlowStep__mode">{MODE_DESCRIPTIONS[lhr.gatherMode]}</div>
69
+ <div className="SummaryFlowStep__mode">{modeDescription}</div>
71
70
  <a className="SummaryFlowStep__link" href={`#index=${hashIndex}`}>{label}</a>
72
71
  </div>
73
72
  {
@@ -108,6 +107,8 @@ const SummaryFlow: FunctionComponent = () => {
108
107
 
109
108
  export const SummaryHeader: FunctionComponent = () => {
110
109
  const flowResult = useFlowResult();
110
+ const strings = useUIStrings();
111
+ const str_ = useStringFormatter();
111
112
 
112
113
  let numNavigation = 0;
113
114
  let numTimespan = 0;
@@ -127,14 +128,14 @@ export const SummaryHeader: FunctionComponent = () => {
127
128
  }
128
129
 
129
130
  const subtitleCounts = [];
130
- if (numNavigation) subtitleCounts.push(`${numNavigation} navigation reports`);
131
- if (numTimespan) subtitleCounts.push(`${numTimespan} timespan reports`);
132
- if (numSnapshot) subtitleCounts.push(`${numSnapshot} snapshot reports`);
131
+ if (numNavigation) subtitleCounts.push(str_(strings.navigationReportCount, {numNavigation}));
132
+ if (numTimespan) subtitleCounts.push(str_(strings.timespanReportCount, {numTimespan}));
133
+ if (numSnapshot) subtitleCounts.push(str_(strings.snapshotReportCount, {numSnapshot}));
133
134
  const subtitle = subtitleCounts.join(' · ');
134
135
 
135
136
  return (
136
137
  <div className="SummaryHeader">
137
- <div className="SummaryHeader__title">Summary</div>
138
+ <div className="SummaryHeader__title">{strings.summary}</div>
138
139
  <div className="SummaryHeader__subtitle">{subtitle}</div>
139
140
  </div>
140
141
  );
@@ -150,11 +151,13 @@ const SummarySectionHeader: FunctionComponent = ({children}) => {
150
151
  };
151
152
 
152
153
  export const Summary: FunctionComponent = () => {
154
+ const strings = useUIStrings();
155
+
153
156
  return (
154
157
  <div className="Summary" data-testid="Summary">
155
158
  <SummaryHeader/>
156
159
  <Separator/>
157
- <SummarySectionHeader>ALL REPORTS</SummarySectionHeader>
160
+ <SummarySectionHeader>{strings.allReports}</SummarySectionHeader>
158
161
  <SummaryFlow/>
159
162
  </div>
160
163
  );
@@ -5,8 +5,27 @@
5
5
  */
6
6
 
7
7
  import {FunctionComponent, JSX} from 'preact';
8
+ import {useState} from 'preact/hooks';
8
9
 
10
+ import {HelpDialog} from './help-dialog';
11
+ import {getFilenamePrefix} from '../../report/generator/file-namer';
12
+ import {useUIStrings} from './i18n/i18n';
9
13
  import {HamburgerIcon} from './icons';
14
+ import {useFlowResult} from './util';
15
+ import {useReportRenderer} from './wrappers/report-renderer';
16
+
17
+ import type {DOM} from '../../report/renderer/dom';
18
+
19
+ function saveHtml(flowResult: LH.FlowResult, dom: DOM) {
20
+ const htmlStr = document.documentElement.outerHTML;
21
+ const blob = new Blob([htmlStr], {type: 'text/html'});
22
+
23
+ const lhr = flowResult.steps[0].lhr;
24
+ const name = flowResult.name.replace(/\s/g, '-');
25
+ const filename = getFilenamePrefix(name, lhr.fetchTime);
26
+
27
+ dom.saveFile(blob, filename);
28
+ }
10
29
 
11
30
  /* eslint-disable max-len */
12
31
  const Logo: FunctionComponent = () => {
@@ -44,17 +63,48 @@ const Logo: FunctionComponent = () => {
44
63
  };
45
64
  /* eslint-enable max-len */
46
65
 
47
- export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLDivElement>}> =
66
+ const TopbarButton: FunctionComponent<{
67
+ onClick: JSX.MouseEventHandler<HTMLButtonElement>,
68
+ label: string,
69
+ }> =
70
+ ({onClick, label, children}) => {
71
+ return (
72
+ <button className="TopbarButton" onClick={onClick} aria-label={label}>
73
+ {children}
74
+ </button>
75
+ );
76
+ };
77
+
78
+ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLButtonElement>}> =
48
79
  ({onMenuClick}) => {
80
+ const flowResult = useFlowResult();
81
+ const {dom} = useReportRenderer();
82
+ const strings = useUIStrings();
83
+ const [showHelpDialog, setShowHelpDialog] = useState(false);
84
+
49
85
  return (
50
86
  <div className="Topbar">
51
- <div className="Topbar__menu" onClick={onMenuClick} role="button">
87
+ <TopbarButton onClick={onMenuClick} label="Button that opens and closes the sidebar">
52
88
  <HamburgerIcon/>
53
- </div>
89
+ </TopbarButton>
54
90
  <div className="Topbar__logo">
55
91
  <Logo/>
56
92
  </div>
57
- <div className="Topbar__title">Lighthouse User Flow Report</div>
93
+ <div className="Topbar__title">{strings.title}</div>
94
+ <TopbarButton
95
+ onClick={() => saveHtml(flowResult, dom)}
96
+ label="Button that saves the report as HTML"
97
+ >{strings.save}</TopbarButton>
98
+ <div style={{flexGrow: 1}} />
99
+ <TopbarButton
100
+ onClick={() => setShowHelpDialog(previous => !previous)}
101
+ label="Button that toggles the help dialog">
102
+ {strings.helpLabel}
103
+ </TopbarButton>
104
+ {showHelpDialog ?
105
+ <HelpDialog onClose={() => setShowHelpDialog(false)} /> :
106
+ null
107
+ }
58
108
  </div>
59
109
  );
60
110
  };
@@ -7,6 +7,8 @@
7
7
  import {createContext} from 'preact';
8
8
  import {useContext, useEffect, useMemo, useState} from 'preact/hooks';
9
9
 
10
+ import type {UIStringsType} from './i18n/ui-strings';
11
+
10
12
  export const FlowResultContext = createContext<LH.FlowResult|undefined>(undefined);
11
13
 
12
14
  function getHashParam(param: string): string|null {
@@ -41,6 +43,7 @@ export function getScreenDimensions(reportResult: LH.ReportResult) {
41
43
  export function getScreenshot(reportResult: LH.ReportResult) {
42
44
  const fullPageScreenshotAudit = reportResult.audits['full-page-screenshot'];
43
45
  const fullPageScreenshot =
46
+ fullPageScreenshotAudit &&
44
47
  fullPageScreenshotAudit.details &&
45
48
  fullPageScreenshotAudit.details.type === 'full-page-screenshot' &&
46
49
  fullPageScreenshotAudit.details.screenshot.data;
@@ -48,6 +51,28 @@ export function getScreenshot(reportResult: LH.ReportResult) {
48
51
  return fullPageScreenshot || null;
49
52
  }
50
53
 
54
+ export function getFilmstripFrames(
55
+ reportResult: LH.ReportResult
56
+ ): Array<{data: string}> | undefined {
57
+ const filmstripAudit = reportResult.audits['screenshot-thumbnails'];
58
+ if (!filmstripAudit) return undefined;
59
+
60
+ const frameItems =
61
+ filmstripAudit.details &&
62
+ filmstripAudit.details.type === 'filmstrip' &&
63
+ filmstripAudit.details.items;
64
+
65
+ return frameItems || undefined;
66
+ }
67
+
68
+ export function getModeDescription(mode: LH.Result.GatherMode, strings: UIStringsType) {
69
+ switch (mode) {
70
+ case 'navigation': return strings.navigationDescription;
71
+ case 'timespan': return strings.timespanDescription;
72
+ case 'snapshot': return strings.snapshotDescription;
73
+ }
74
+ }
75
+
51
76
  export function useFlowResult(): LH.FlowResult {
52
77
  const flowResult = useContext(FlowResultContext);
53
78
  if (!flowResult) throw Error('useFlowResult must be called in the FlowResultContext');
@@ -77,7 +102,7 @@ export function useHashParam(param: string) {
77
102
  return paramValue;
78
103
  }
79
104
 
80
- export function useCurrentLhr(): {value: LH.Result, index: number}|null {
105
+ export function useCurrentLhr(): LH.FlowResult.LhrRef|null {
81
106
  const flowResult = useFlowResult();
82
107
  const indexString = useHashParam('index');
83
108
 
@@ -19,7 +19,7 @@ interface ReportRendererGlobals {
19
19
  reportRenderer: ReportRenderer,
20
20
  }
21
21
 
22
- const ReportRendererContext = createContext<ReportRendererGlobals|undefined>(undefined);
22
+ export const ReportRendererContext = createContext<ReportRendererGlobals|undefined>(undefined);
23
23
 
24
24
  export function useReportRenderer() {
25
25
  const globals = useContext(ReportRendererContext);
@@ -5,9 +5,8 @@
5
5
  */
6
6
 
7
7
  import {FunctionComponent} from 'preact';
8
- import {useEffect, useLayoutEffect, useRef} from 'preact/hooks';
8
+ import {useLayoutEffect, useRef} from 'preact/hooks';
9
9
 
10
- import {useCurrentLhr, useHashParam} from '../util';
11
10
  import {useReportRenderer} from './report-renderer';
12
11
 
13
12
  /**
@@ -16,7 +15,7 @@ import {useReportRenderer} from './report-renderer';
16
15
  * e.g. <a href="#link"> -> <a href="#index=0&anchor=link">
17
16
  */
18
17
  export function convertChildAnchors(element: HTMLElement, index: number) {
19
- const links = element.querySelectorAll('a') as NodeListOf<HTMLAnchorElement>;
18
+ const links = element.querySelectorAll('a[href]') as NodeListOf<HTMLAnchorElement>;
20
19
  for (const link of links) {
21
20
  // Check if the link destination is in the report.
22
21
  const currentUrl = new URL(location.href);
@@ -32,19 +31,18 @@ export function convertChildAnchors(element: HTMLElement, index: number) {
32
31
  }
33
32
  }
34
33
 
35
- export const Report: FunctionComponent = () => {
34
+ export const Report: FunctionComponent<{currentLhr: LH.FlowResult.LhrRef}> =
35
+ ({currentLhr}) => {
36
36
  const {dom, reportRenderer} = useReportRenderer();
37
37
  const ref = useRef<HTMLDivElement>(null);
38
- const anchor = useHashParam('anchor');
39
- const currentLhr = useCurrentLhr();
40
38
 
41
39
  useLayoutEffect(() => {
42
- if (!currentLhr) return;
43
-
44
40
  if (ref.current) {
45
41
  dom.clearComponentCache();
46
42
  reportRenderer.renderReport(currentLhr.value, ref.current);
47
43
  convertChildAnchors(ref.current, currentLhr.index);
44
+ const topbar = ref.current.querySelector('.lh-topbar');
45
+ if (topbar) topbar.remove();
48
46
  }
49
47
 
50
48
  return () => {
@@ -52,19 +50,6 @@ export const Report: FunctionComponent = () => {
52
50
  };
53
51
  }, [reportRenderer, currentLhr]);
54
52
 
55
- useEffect(() => {
56
- if (anchor) {
57
- const el = document.getElementById(anchor);
58
- if (el) {
59
- el.scrollIntoView({behavior: 'smooth'});
60
- return;
61
- }
62
- }
63
-
64
- // Scroll to top no anchor is found.
65
- if (ref.current) ref.current.scrollIntoView();
66
- }, [anchor, currentLhr]);
67
-
68
53
  return (
69
54
  <div ref={ref} className="lh-root" data-testid="Report"/>
70
55
  );
@@ -4,21 +4,10 @@
4
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
5
  */
6
6
 
7
- import fs from 'fs';
8
- import {dirname} from 'path';
9
- import {fileURLToPath} from 'url';
10
-
11
- import {render} from '@testing-library/preact';
7
+ import {act, render} from '@testing-library/preact';
12
8
 
13
9
  import {App} from '../src/app';
14
-
15
- const flowResult = JSON.parse(
16
- fs.readFileSync(
17
- // eslint-disable-next-line max-len
18
- `${dirname(fileURLToPath(import.meta.url))}/../../lighthouse-core/test/fixtures/fraggle-rock/reports/sample-lhrs.json`,
19
- 'utf-8'
20
- )
21
- );
10
+ import {flowResult} from './sample-flow';
22
11
 
23
12
  it('renders a standalone report with summary', async () => {
24
13
  const root = render(<App flowResult={flowResult}/>);
@@ -46,3 +35,18 @@ it('renders the snapshot step', async () => {
46
35
 
47
36
  expect(root.getByTestId('Report')).toBeTruthy();
48
37
  });
38
+
39
+ it('toggles collapsed mode when hamburger button clicked', async () => {
40
+ const root = render(<App flowResult={flowResult}/>);
41
+
42
+ const app = root.getByTestId('App');
43
+ const hamburgerButton = root.getByLabelText('Button that opens and closes the sidebar');
44
+
45
+ expect(app.classList).not.toContain('App--collapsed');
46
+
47
+ await act(() => {
48
+ hamburgerButton.click();
49
+ });
50
+
51
+ expect(app.classList).toContain('App--collapsed');
52
+ });
@@ -0,0 +1,89 @@
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 {act, render} from '@testing-library/preact';
9
+
10
+ import {FlowStepThumbnail} from '../src/common';
11
+
12
+ let lhr: LH.ReportResult;
13
+
14
+ jest.useFakeTimers();
15
+
16
+ describe('FlowStepThumbnail', () => {
17
+ beforeEach(() => {
18
+ global.console.warn = jest.fn();
19
+
20
+ lhr = {
21
+ gatherMode: 'navigation',
22
+ configSettings: {screenEmulation: {width: 400, height: 600}},
23
+ audits: {
24
+ 'full-page-screenshot': {
25
+ details: {
26
+ type: 'full-page-screenshot',
27
+ screenshot: {data: 'baef01', width: 400, height: 600},
28
+ nodes: {},
29
+ },
30
+ },
31
+ },
32
+ } as any;
33
+ });
34
+
35
+ it('renders a thumbnail', () => {
36
+ const root = render(<FlowStepThumbnail reportResult={lhr} width={200} height={200} />);
37
+
38
+ const thumbnail = root.getByAltText(/Screenshot/);
39
+ expect(thumbnail.style.width).toEqual('200px');
40
+ expect(thumbnail.style.height).toEqual('200px');
41
+ });
42
+
43
+ it('renders nothing without dimensions', () => {
44
+ const root = render(<FlowStepThumbnail reportResult={lhr} />);
45
+
46
+ expect(() => root.getByAltText(/Screenshot/)).toThrow();
47
+ expect(global.console.warn).toHaveBeenCalled();
48
+ });
49
+
50
+ it('interpolates height', () => {
51
+ const root = render(<FlowStepThumbnail reportResult={lhr} width={200} />);
52
+
53
+ const thumbnail = root.getByAltText(/Screenshot/);
54
+ expect(thumbnail.style.width).toEqual('200px');
55
+ expect(thumbnail.style.height).toEqual('300px');
56
+ });
57
+
58
+ it('interpolates width', () => {
59
+ const root = render(<FlowStepThumbnail reportResult={lhr} height={150} />);
60
+
61
+ const thumbnail = root.getByAltText(/Screenshot/);
62
+ expect(thumbnail.style.width).toEqual('100px');
63
+ expect(thumbnail.style.height).toEqual('150px');
64
+ });
65
+
66
+ it('renders animated thumbnail for timespan', async () => {
67
+ 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
+ const root = render(<FlowStepThumbnail reportResult={lhr} height={150} />);
78
+
79
+ const thumbnail = root.getByAltText(/Animated/) as HTMLImageElement;
80
+ expect(thumbnail.style.width).toEqual('100px');
81
+ expect(thumbnail.style.height).toEqual('150px');
82
+
83
+ expect(thumbnail.src).toContain('frame1');
84
+ await act(() => {
85
+ jest.advanceTimersByTime(251);
86
+ });
87
+ expect(thumbnail.src).toContain('frame2');
88
+ });
89
+ });
@@ -0,0 +1,55 @@
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 {FunctionComponent} from 'preact';
8
+ import {render} from '@testing-library/preact';
9
+
10
+ import {Header} from '../src/header';
11
+ import {FlowResultContext} from '../src/util';
12
+ import {flowResult} from './sample-flow';
13
+ import {I18nProvider} from '../src/i18n/i18n';
14
+
15
+ let wrapper: FunctionComponent;
16
+
17
+ beforeEach(() => {
18
+ wrapper = ({children}) => (
19
+ <FlowResultContext.Provider value={flowResult}>
20
+ <I18nProvider>
21
+ {children}
22
+ </I18nProvider>
23
+ </FlowResultContext.Provider>
24
+ );
25
+ });
26
+
27
+ it('renders all sections for a middle step', () => {
28
+ const currentLhr = {index: 1} as any;
29
+ const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
30
+
31
+ expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
32
+ expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
33
+ expect(root.baseElement.querySelector('.Header__next-thumbnail')).toBeTruthy();
34
+ expect(root.baseElement.querySelector('.Header__next-title')).toBeTruthy();
35
+ });
36
+
37
+ it('renders only next section for first step', () => {
38
+ const currentLhr = {index: 0} as any;
39
+ const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
40
+
41
+ expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeFalsy();
42
+ expect(root.baseElement.querySelector('.Header__prev-title')).toBeFalsy();
43
+ expect(root.baseElement.querySelector('.Header__next-thumbnail')).toBeTruthy();
44
+ expect(root.baseElement.querySelector('.Header__next-title')).toBeTruthy();
45
+ });
46
+
47
+ it('renders only previous section for last step', () => {
48
+ const currentLhr = {index: 3} as any;
49
+ const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
50
+
51
+ expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
52
+ expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
53
+ expect(root.baseElement.querySelector('.Header__next-thumbnail')).toBeFalsy();
54
+ expect(root.baseElement.querySelector('.Header__next-title')).toBeFalsy();
55
+ });
@@ -0,0 +1,17 @@
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 fs from 'fs';
8
+ import {dirname} from 'path';
9
+ import {fileURLToPath} from 'url';
10
+
11
+ export const flowResult: LH.FlowResult = JSON.parse(
12
+ fs.readFileSync(
13
+ // eslint-disable-next-line max-len
14
+ `${dirname(fileURLToPath(import.meta.url))}/../../lighthouse-core/test/fixtures/fraggle-rock/reports/sample-flow-result.json`,
15
+ 'utf-8'
16
+ )
17
+ );
@@ -17,6 +17,7 @@ export function setupJsDom() {
17
17
  global.window = window as any;
18
18
  global.document = window.document;
19
19
  global.location = window.location;
20
+ global.Blob = window.Blob;
20
21
 
21
22
  // Function not implemented in JSDOM.
22
23
  window.Element.prototype.scrollIntoView = jest.fn();
@@ -1,21 +1,15 @@
1
- import fs from 'fs';
2
- import {dirname} from 'path';
3
- import {fileURLToPath} from 'url';
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
+ */
4
6
 
5
7
  import {render} from '@testing-library/preact';
6
8
  import {FunctionComponent} from 'preact';
7
9
 
8
10
  import {SidebarFlow} from '../../src/sidebar/flow';
9
11
  import {FlowResultContext} from '../../src/util';
10
-
11
-
12
- const flowResult = JSON.parse(
13
- fs.readFileSync(
14
- // eslint-disable-next-line max-len
15
- `${dirname(fileURLToPath(import.meta.url))}/../../../lighthouse-core/test/fixtures/fraggle-rock/reports/sample-lhrs.json`,
16
- 'utf-8'
17
- )
18
- );
12
+ import {flowResult} from '../sample-flow';
19
13
 
20
14
  let wrapper: FunctionComponent;
21
15
 
@@ -4,30 +4,24 @@
4
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
5
  */
6
6
 
7
- import fs from 'fs';
8
- import {dirname} from 'path';
9
- import {fileURLToPath} from 'url';
10
-
11
7
  import {render} from '@testing-library/preact';
12
8
  import {FunctionComponent} from 'preact';
13
9
 
10
+ import {I18nProvider} from '../../src/i18n/i18n';
14
11
  import {SidebarHeader, SidebarSummary} from '../../src/sidebar/sidebar';
15
12
  import {FlowResultContext} from '../../src/util';
13
+ import {flowResult} from '../sample-flow';
16
14
 
17
15
 
18
- const flowResult = JSON.parse(
19
- fs.readFileSync(
20
- // eslint-disable-next-line max-len
21
- `${dirname(fileURLToPath(import.meta.url))}/../../../lighthouse-core/test/fixtures/fraggle-rock/reports/sample-lhrs.json`,
22
- 'utf-8'
23
- )
24
- );
25
-
26
16
  let wrapper: FunctionComponent;
27
17
 
28
18
  beforeEach(() => {
29
19
  wrapper = ({children}) => (
30
- <FlowResultContext.Provider value={flowResult}>{children}</FlowResultContext.Provider>
20
+ <FlowResultContext.Provider value={flowResult}>
21
+ <I18nProvider>
22
+ {children}
23
+ </I18nProvider>
24
+ </FlowResultContext.Provider>
31
25
  );
32
26
  });
33
27