lighthouse 9.0.0-dev.20211123 → 9.1.0-dev.20211126

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 (48) hide show
  1. package/changelog.md +70 -0
  2. package/dist/report/bundle.esm.js +16 -2
  3. package/dist/report/flow.js +34 -23
  4. package/dist/report/standalone.js +2 -2
  5. package/flow-report/api.ts +18 -0
  6. package/flow-report/{standalone-flow.tsx → clients/standalone.ts} +4 -5
  7. package/flow-report/src/app.tsx +20 -12
  8. package/flow-report/src/sidebar/flow.tsx +1 -3
  9. package/flow-report/src/sidebar/sidebar.tsx +1 -3
  10. package/flow-report/src/topbar.tsx +22 -13
  11. package/flow-report/src/util.ts +7 -0
  12. package/flow-report/src/wrappers/report.tsx +1 -0
  13. package/flow-report/src/wrappers/styles.tsx +16 -0
  14. package/flow-report/test/topbar-test.tsx +22 -6
  15. package/flow-report/types/flow-report.d.ts +4 -0
  16. package/lighthouse-core/lib/stack-packs.js +12 -8
  17. package/package.json +2 -2
  18. package/report/generator/file-namer.js +12 -1
  19. package/report/renderer/api.js +15 -1
  20. package/report/renderer/report-renderer.js +4 -1
  21. package/report/types/report-renderer.d.ts +2 -0
  22. package/shared/localization/locales/bg.json +17 -17
  23. package/shared/localization/locales/da.json +16 -16
  24. package/shared/localization/locales/en-GB.json +19 -19
  25. package/shared/localization/locales/en-US.json +30 -0
  26. package/shared/localization/locales/en-XL.json +30 -0
  27. package/shared/localization/locales/es-419.json +17 -17
  28. package/shared/localization/locales/fi.json +16 -16
  29. package/shared/localization/locales/fil.json +33 -33
  30. package/shared/localization/locales/fr.json +16 -16
  31. package/shared/localization/locales/he.json +16 -16
  32. package/shared/localization/locales/hr.json +16 -16
  33. package/shared/localization/locales/hu.json +16 -16
  34. package/shared/localization/locales/id.json +16 -16
  35. package/shared/localization/locales/it.json +37 -37
  36. package/shared/localization/locales/ja.json +16 -16
  37. package/shared/localization/locales/ko.json +16 -16
  38. package/shared/localization/locales/lt.json +17 -17
  39. package/shared/localization/locales/nl.json +16 -16
  40. package/shared/localization/locales/no.json +17 -17
  41. package/shared/localization/locales/pl.json +16 -16
  42. package/shared/localization/locales/pt-PT.json +37 -37
  43. package/shared/localization/locales/pt.json +16 -16
  44. package/shared/localization/locales/ru.json +16 -16
  45. package/shared/localization/locales/sr-Latn.json +16 -16
  46. package/shared/localization/locales/sr.json +16 -16
  47. package/shared/localization/locales/sv.json +17 -17
  48. package/shared/localization/locales/uk.json +17 -17
@@ -145,7 +145,7 @@ class s{constructor(e,t){this._document=e,this._lighthouseChannel="unknown",this
145
145
  *
146
146
  * Dummy text for ensuring report robustness: <\/script> pre$`post %%LIGHTHOUSE_JSON%%
147
147
  * (this is handled by terser)
148
- */class N{constructor(e){this._dom=e,this._opts={}}renderReport(e,t,n){if(!this._dom.rootEl&&t){console.warn("Please adopt the new report API in renderer/api.js.");const e=t.closest(".lh-root");e?this._dom.rootEl=e:(t.classList.add("lh-root","lh-vars"),this._dom.rootEl=t)}else this._dom.rootEl&&t&&(this._dom.rootEl=t);n&&(this._opts=n),this._dom.setLighthouseChannel(e.configSettings.channel||"unknown");const r=l.prepareReportResult(e);return this._dom.rootEl.textContent="",this._dom.rootEl.appendChild(this._renderReport(r)),this._dom.rootEl}_renderReportTopbar(e){const t=this._dom.createComponent("topbar"),n=this._dom.find("a.lh-topbar__url",t);return n.textContent=e.finalUrl,n.title=e.finalUrl,this._dom.safelySetHref(n,e.finalUrl),t}_renderReportHeader(){const e=this._dom.createComponent("heading"),t=this._dom.createComponent("scoresWrapper");return this._dom.find(".lh-scores-wrapper-placeholder",e).replaceWith(t),e}_renderReportFooter(e){const t=this._dom.createComponent("footer");return this._renderMetaBlock(e,t),this._dom.find(".lh-footer__version_issue",t).textContent=l.i18n.strings.footerIssue,this._dom.find(".lh-footer__version",t).textContent=e.lighthouseVersion,t}_renderMetaBlock(e,t){const n=l.getEmulationDescriptions(e.configSettings||{}),r=e.userAgent.match(/(\w*Chrome\/[\d.]+)/),o=Array.isArray(r)?r[1].replace("/"," ").replace("Chrome","Chromium"):"Chromium",i=e.configSettings.channel,a=e.environment.benchmarkIndex.toFixed(0),s=e.environment.credits?.["axe-core"],c=[["date",`Captured at ${l.i18n.formatDateTime(e.fetchTime)}`],["devices",`${n.deviceEmulation} with Lighthouse ${e.lighthouseVersion}`,`${l.i18n.strings.runtimeSettingsBenchmark}: ${a}\n${l.i18n.strings.runtimeSettingsCPUThrottling}: ${n.cpuThrottling}`+(s?`\n${l.i18n.strings.runtimeSettingsAxeVersion}: ${s}`:"")],["samples-one",l.i18n.strings.runtimeSingleLoad,l.i18n.strings.runtimeSingleLoadTooltip],["stopwatch",l.i18n.strings.runtimeAnalysisWindow],["networkspeed",`${n.summary}`,`${l.i18n.strings.runtimeSettingsNetworkThrottling}: ${n.networkThrottling}`],["chrome",`Using ${o}`+(i?` with ${i}`:""),`${l.i18n.strings.runtimeSettingsUANetwork}: "${e.environment.networkUserAgent}"`]],d=this._dom.find(".lh-meta__items",t);for(const[e,t,n]of c){const r=this._dom.createChildOf(d,"li","lh-meta__item");if(r.textContent=t,n){r.classList.add("lh-tooltip-boundary");this._dom.createChildOf(r,"div","lh-tooltip").textContent=n}r.classList.add("lh-report-icon",`lh-report-icon--${e}`)}}_renderReportWarnings(e){if(!e.runWarnings||0===e.runWarnings.length)return this._dom.createElement("div");const t=this._dom.createComponent("warningsToplevel");this._dom.find(".lh-warnings__msg",t).textContent=l.i18n.strings.toplevelWarningsMessage;const n=this._dom.find("ul",t);for(const t of e.runWarnings){n.appendChild(this._dom.createElement("li")).appendChild(this._dom.convertMarkdownLinkSnippets(t))}return t}_renderScoreGauges(e,t,n){const r=[],o=[],i=[];for(const a of Object.values(e.categories)){const s=n[a.id]||t,c=s.renderCategoryScore(a,e.categoryGroups||{},{gatherMode:e.gatherMode}),d=this._dom.find("a.lh-gauge__wrapper, a.lh-fraction__wrapper",c);d&&(this._dom.safelySetHref(d,`#${a.id}`),d.addEventListener("click",(e=>{if(!d.matches('[href^="#"]'))return;const t=d.getAttribute("href"),n=this._dom.rootEl;if(!t||!n)return;const r=this._dom.find(t,n);e.preventDefault(),r.scrollIntoView()})),this._opts.onPageAnchorRendered?.(d)),l.isPluginCategory(a.id)?i.push(c):s.renderCategoryScore===t.renderCategoryScore?r.push(c):o.push(c)}return[...r,...o,...i]}_renderReport(e){const t=new L(e.configSettings.locale,{...l.UIStrings,...e.i18n.rendererFormattedStrings});l.i18n=t,l.reportJson=e;const n=e.audits["full-page-screenshot"]&&e.audits["full-page-screenshot"].details&&"full-page-screenshot"===e.audits["full-page-screenshot"].details.type?e.audits["full-page-screenshot"].details:void 0,r=new z(this._dom,{fullPageScreenshot:n}),o=new c(this._dom,r),i={performance:new M(this._dom,r),pwa:new F(this._dom,r)},a=this._dom.createElement("div");a.appendChild(this._renderReportHeader());const s=this._dom.createElement("div","lh-container"),d=this._dom.createElement("div","lh-report");let h;d.appendChild(this._renderReportWarnings(e));1===Object.keys(e.categories).length?a.classList.add("lh-header--solo-category"):h=this._dom.createElement("div","lh-scores-header");const p=this._dom.createElement("div");if(p.classList.add("lh-scorescale-wrap"),p.append(this._dom.createComponent("scorescale")),h){const t=this._dom.find(".lh-scores-container",a);h.append(...this._renderScoreGauges(e,o,i)),t.appendChild(h),t.appendChild(p);const n=this._dom.createElement("div","lh-sticky-header");n.append(...this._renderScoreGauges(e,o,i)),s.appendChild(n)}const u=d.appendChild(this._dom.createElement("div","lh-categories")),g={gatherMode:e.gatherMode};for(const t of Object.values(e.categories)){const n=i[t.id]||o;n.dom.createChildOf(u,"div","lh-category-wrapper").appendChild(n.render(t,e.categoryGroups,g))}o.injectFinalScreenshot(u,e.audits,p);const m=this._dom.createFragment();return m.append(this._dom.createComponent("styles")),this._opts.omitTopbar||m.appendChild(this._renderReportTopbar(e)),m.appendChild(s),s.appendChild(a),s.appendChild(d),d.appendChild(this._renderReportFooter(e)),n&&A.installFullPageScreenshot(this._dom.rootEl,n.screenshot),m}}
148
+ */class N{constructor(e){this._dom=e,this._opts={}}renderReport(e,t,n){if(!this._dom.rootEl&&t){console.warn("Please adopt the new report API in renderer/api.js.");const e=t.closest(".lh-root");e?this._dom.rootEl=e:(t.classList.add("lh-root","lh-vars"),this._dom.rootEl=t)}else this._dom.rootEl&&t&&(this._dom.rootEl=t);n&&(this._opts=n),this._dom.setLighthouseChannel(e.configSettings.channel||"unknown");const r=l.prepareReportResult(e);return this._dom.rootEl.textContent="",this._dom.rootEl.appendChild(this._renderReport(r)),this._dom.rootEl}_renderReportTopbar(e){const t=this._dom.createComponent("topbar"),n=this._dom.find("a.lh-topbar__url",t);return n.textContent=e.finalUrl,n.title=e.finalUrl,this._dom.safelySetHref(n,e.finalUrl),t}_renderReportHeader(){const e=this._dom.createComponent("heading"),t=this._dom.createComponent("scoresWrapper");return this._dom.find(".lh-scores-wrapper-placeholder",e).replaceWith(t),e}_renderReportFooter(e){const t=this._dom.createComponent("footer");return this._renderMetaBlock(e,t),this._dom.find(".lh-footer__version_issue",t).textContent=l.i18n.strings.footerIssue,this._dom.find(".lh-footer__version",t).textContent=e.lighthouseVersion,t}_renderMetaBlock(e,t){const n=l.getEmulationDescriptions(e.configSettings||{}),r=e.userAgent.match(/(\w*Chrome\/[\d.]+)/),o=Array.isArray(r)?r[1].replace("/"," ").replace("Chrome","Chromium"):"Chromium",i=e.configSettings.channel,a=e.environment.benchmarkIndex.toFixed(0),s=e.environment.credits?.["axe-core"],c=[["date",`Captured at ${l.i18n.formatDateTime(e.fetchTime)}`],["devices",`${n.deviceEmulation} with Lighthouse ${e.lighthouseVersion}`,`${l.i18n.strings.runtimeSettingsBenchmark}: ${a}\n${l.i18n.strings.runtimeSettingsCPUThrottling}: ${n.cpuThrottling}`+(s?`\n${l.i18n.strings.runtimeSettingsAxeVersion}: ${s}`:"")],["samples-one",l.i18n.strings.runtimeSingleLoad,l.i18n.strings.runtimeSingleLoadTooltip],["stopwatch",l.i18n.strings.runtimeAnalysisWindow],["networkspeed",`${n.summary}`,`${l.i18n.strings.runtimeSettingsNetworkThrottling}: ${n.networkThrottling}`],["chrome",`Using ${o}`+(i?` with ${i}`:""),`${l.i18n.strings.runtimeSettingsUANetwork}: "${e.environment.networkUserAgent}"`]],d=this._dom.find(".lh-meta__items",t);for(const[e,t,n]of c){const r=this._dom.createChildOf(d,"li","lh-meta__item");if(r.textContent=t,n){r.classList.add("lh-tooltip-boundary");this._dom.createChildOf(r,"div","lh-tooltip").textContent=n}r.classList.add("lh-report-icon",`lh-report-icon--${e}`)}}_renderReportWarnings(e){if(!e.runWarnings||0===e.runWarnings.length)return this._dom.createElement("div");const t=this._dom.createComponent("warningsToplevel");this._dom.find(".lh-warnings__msg",t).textContent=l.i18n.strings.toplevelWarningsMessage;const n=this._dom.find("ul",t);for(const t of e.runWarnings){n.appendChild(this._dom.createElement("li")).appendChild(this._dom.convertMarkdownLinkSnippets(t))}return t}_renderScoreGauges(e,t,n){const r=[],o=[],i=[];for(const a of Object.values(e.categories)){const s=n[a.id]||t,c=s.renderCategoryScore(a,e.categoryGroups||{},{gatherMode:e.gatherMode}),d=this._dom.find("a.lh-gauge__wrapper, a.lh-fraction__wrapper",c);d&&(this._dom.safelySetHref(d,`#${a.id}`),d.addEventListener("click",(e=>{if(!d.matches('[href^="#"]'))return;const t=d.getAttribute("href"),n=this._dom.rootEl;if(!t||!n)return;const r=this._dom.find(t,n);e.preventDefault(),r.scrollIntoView()})),this._opts.onPageAnchorRendered?.(d)),l.isPluginCategory(a.id)?i.push(c):s.renderCategoryScore===t.renderCategoryScore?r.push(c):o.push(c)}return[...r,...o,...i]}_renderReport(e){const t=new L(e.configSettings.locale,{...l.UIStrings,...e.i18n.rendererFormattedStrings});l.i18n=t,l.reportJson=e;const n=e.audits["full-page-screenshot"]&&e.audits["full-page-screenshot"].details&&"full-page-screenshot"===e.audits["full-page-screenshot"].details.type?e.audits["full-page-screenshot"].details:void 0,r=new z(this._dom,{fullPageScreenshot:n}),o=new c(this._dom,r),i={performance:new M(this._dom,r),pwa:new F(this._dom,r)},a=this._dom.createElement("div");a.appendChild(this._renderReportHeader());const s=this._dom.createElement("div","lh-container"),d=this._dom.createElement("div","lh-report");let h;d.appendChild(this._renderReportWarnings(e));1===Object.keys(e.categories).length?a.classList.add("lh-header--solo-category"):h=this._dom.createElement("div","lh-scores-header");const p=this._dom.createElement("div");if(p.classList.add("lh-scorescale-wrap"),p.append(this._dom.createComponent("scorescale")),h){const t=this._dom.find(".lh-scores-container",a);h.append(...this._renderScoreGauges(e,o,i)),t.appendChild(h),t.appendChild(p);const n=this._dom.createElement("div","lh-sticky-header");n.append(...this._renderScoreGauges(e,o,i)),s.appendChild(n)}const u=d.appendChild(this._dom.createElement("div","lh-categories")),g={gatherMode:e.gatherMode};for(const t of Object.values(e.categories)){const n=i[t.id]||o;n.dom.createChildOf(u,"div","lh-category-wrapper").appendChild(n.render(t,e.categoryGroups,g))}o.injectFinalScreenshot(u,e.audits,p);const m=this._dom.createFragment();return this._opts.omitGlobalStyles||m.append(this._dom.createComponent("styles")),this._opts.omitTopbar||m.appendChild(this._renderReportTopbar(e)),m.appendChild(s),s.appendChild(a),s.appendChild(d),d.appendChild(this._renderReportFooter(e)),n&&A.installFullPageScreenshot(this._dom.rootEl,n.screenshot),m}}
149
149
  /**
150
150
  * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
151
151
  * 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
@@ -187,7 +187,7 @@ class O{constructor(e){this._dom=e,this._toggleEl,this._menuEl,this.onDocumentKe
187
187
  * @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
188
188
  * 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
189
189
  * 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.
190
- */function V(e,t){const n=t?new Date(t):new Date,r=n.toLocaleTimeString("en-US",{hour12:!1}),o=n.toLocaleDateString("en-US",{year:"numeric",month:"2-digit",day:"2-digit"}).split("/");o.unshift(o.pop());return`${e}_${o.join("-")}_${r}`.replace(/[/?<>\\:*|"]/g,"-")}var G={getLhrFilenamePrefix:function(e){return V(new URL(e.finalUrl).hostname,e.fetchTime)},getFilenamePrefix:V};
190
+ */function V(e,t){const n=t?new Date(t):new Date,r=n.toLocaleTimeString("en-US",{hour12:!1}),o=n.toLocaleDateString("en-US",{year:"numeric",month:"2-digit",day:"2-digit"}).split("/");o.unshift(o.pop());return`${e}_${o.join("-")}_${r}`.replace(/[/?<>\\:*|"]/g,"-")}var G={getLhrFilenamePrefix:function(e){return V(new URL(e.finalUrl).hostname,e.fetchTime)},getFilenamePrefix:V,getFlowResultFilenamePrefix:function(e){const t=e.steps[0].lhr;return V(e.name.replace(/\s/g,"-"),t.fetchTime)}};
191
191
  /**
192
192
  * @license
193
193
  * Copyright 2017 The Lighthouse Authors. All Rights Reserved.
@@ -0,0 +1,18 @@
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 {render, h} from 'preact';
8
+
9
+ import {App} from './src/app';
10
+
11
+ export function renderFlowReport(
12
+ flowResult: LH.FlowResult,
13
+ root: HTMLElement,
14
+ options?: LH.FlowReportOptions
15
+ ) {
16
+ root.classList.add('flow-vars', 'lh-vars', 'lh-root');
17
+ render(h(App, {flowResult, options}), root);
18
+ }
@@ -9,15 +9,14 @@
9
9
  * The renderer code is bundled and injected into the report HTML along with the JSON report.
10
10
  */
11
11
 
12
- import {render} from 'preact';
13
-
14
- import {App} from './src/app';
12
+ import {renderFlowReport} from '../api';
15
13
 
16
14
  function __initLighthouseFlowReport__() {
17
15
  const container = document.body.querySelector('main');
18
16
  if (!container) throw Error('Container element not found');
19
- container.classList.add('flow-vars', 'lh-root', 'lh-vars');
20
- render(<App flowResult={window.__LIGHTHOUSE_FLOW_JSON__} />, container);
17
+ renderFlowReport(window.__LIGHTHOUSE_FLOW_JSON__, container, {
18
+ getReportHtml: () => document.documentElement.outerHTML,
19
+ });
21
20
  }
22
21
 
23
22
  window.__initLighthouseFlowReport__ = __initLighthouseFlowReport__;
@@ -5,15 +5,16 @@
5
5
  */
6
6
 
7
7
  import {FunctionComponent} from 'preact';
8
- import {useLayoutEffect, useRef, useState} from 'preact/hooks';
8
+ import {useLayoutEffect, useMemo, useRef, useState} from 'preact/hooks';
9
9
 
10
10
  import {Sidebar} from './sidebar/sidebar';
11
11
  import {Summary} from './summary/summary';
12
- import {classNames, FlowResultContext, useHashState} from './util';
12
+ import {classNames, FlowResultContext, OptionsContext, useHashState} from './util';
13
13
  import {Report} from './wrappers/report';
14
14
  import {Topbar} from './topbar';
15
15
  import {Header} from './header';
16
16
  import {I18nProvider} from './i18n/i18n';
17
+ import {Styles} from './wrappers/styles';
17
18
 
18
19
  function getAnchorElement(hashState: LH.FlowResult.HashState|null) {
19
20
  if (!hashState || !hashState.anchor) return null;
@@ -47,17 +48,24 @@ const Content: FunctionComponent = () => {
47
48
  );
48
49
  };
49
50
 
50
- export const App: FunctionComponent<{flowResult: LH.FlowResult}> = ({flowResult}) => {
51
+ export const App: FunctionComponent<{
52
+ flowResult: LH.FlowResult,
53
+ options?: LH.FlowReportOptions
54
+ }> = ({flowResult, options}) => {
51
55
  const [collapsed, setCollapsed] = useState(false);
56
+ const optionsValue = useMemo(() => options || {}, [options]);
52
57
  return (
53
- <FlowResultContext.Provider value={flowResult}>
54
- <I18nProvider>
55
- <div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
56
- <Topbar onMenuClick={() => setCollapsed(c => !c)} />
57
- <Sidebar/>
58
- <Content/>
59
- </div>
60
- </I18nProvider>
61
- </FlowResultContext.Provider>
58
+ <OptionsContext.Provider value={optionsValue}>
59
+ <FlowResultContext.Provider value={flowResult}>
60
+ <I18nProvider>
61
+ <Styles/>
62
+ <div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
63
+ <Topbar onMenuClick={() => setCollapsed(c => !c)} />
64
+ <Sidebar/>
65
+ <Content/>
66
+ </div>
67
+ </I18nProvider>
68
+ </FlowResultContext.Provider>
69
+ </OptionsContext.Provider>
62
70
  );
63
71
  };
@@ -48,8 +48,6 @@ export const SidebarFlow: FunctionComponent = () => {
48
48
  {
49
49
  flowResult.steps.map((step, index) => {
50
50
  const {lhr, name} = step;
51
- const url = new URL(location.href);
52
- url.hash = `#index=${index}`;
53
51
  return <>
54
52
  {
55
53
  lhr.gatherMode === 'navigation' && index !== 0 ?
@@ -59,7 +57,7 @@ export const SidebarFlow: FunctionComponent = () => {
59
57
  <SidebarFlowStep
60
58
  key={lhr.fetchTime}
61
59
  mode={lhr.gatherMode}
62
- href={url.href}
60
+ href={`#index=${index}`}
63
61
  label={name}
64
62
  isCurrent={index === (hashState && hashState.index)}
65
63
  />
@@ -17,11 +17,9 @@ const SidebarSummary: FunctionComponent = () => {
17
17
  const hashState = useHashState();
18
18
  const strings = useLocalizedStrings();
19
19
 
20
- const url = new URL(location.href);
21
- url.hash = '#';
22
20
  return (
23
21
  <a
24
- href={url.href}
22
+ href="#"
25
23
  className={classNames('SidebarSummary', {'Sidebar--current': hashState === null})}
26
24
  data-testid="SidebarSummary"
27
25
  >
@@ -8,20 +8,15 @@ import {FunctionComponent, JSX} from 'preact';
8
8
  import {useState} from 'preact/hooks';
9
9
 
10
10
  import {HelpDialog} from './help-dialog';
11
- import {getFilenamePrefix} from '../../report/generator/file-namer';
11
+ import {getFlowResultFilenamePrefix} from '../../report/generator/file-namer';
12
12
  import {useLocalizedStrings} from './i18n/i18n';
13
13
  import {HamburgerIcon, InfoIcon} from './icons';
14
- import {useFlowResult} from './util';
14
+ import {useFlowResult, useOptions} from './util';
15
15
  import {saveFile} from '../../report/renderer/api';
16
16
 
17
- function saveHtml(flowResult: LH.FlowResult) {
18
- const htmlStr = document.documentElement.outerHTML;
17
+ function saveHtml(flowResult: LH.FlowResult, htmlStr: string) {
19
18
  const blob = new Blob([htmlStr], {type: 'text/html'});
20
-
21
- const lhr = flowResult.steps[0].lhr;
22
- const name = flowResult.name.replace(/\s/g, '-');
23
- const filename = getFilenamePrefix(name, lhr.fetchTime);
24
-
19
+ const filename = getFlowResultFilenamePrefix(flowResult);
25
20
  saveFile(blob, filename);
26
21
  }
27
22
 
@@ -78,6 +73,7 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
78
73
  const flowResult = useFlowResult();
79
74
  const strings = useLocalizedStrings();
80
75
  const [showHelpDialog, setShowHelpDialog] = useState(false);
76
+ const {getReportHtml, saveAsGist} = useOptions();
81
77
 
82
78
  return (
83
79
  <div className="Topbar">
@@ -88,10 +84,23 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
88
84
  <Logo/>
89
85
  </div>
90
86
  <div className="Topbar__title">{strings.title}</div>
91
- <TopbarButton
92
- onClick={() => saveHtml(flowResult)}
93
- label="Button that saves the report as HTML"
94
- >{strings.save}</TopbarButton>
87
+ {
88
+ getReportHtml &&
89
+ <TopbarButton
90
+ onClick={() => {
91
+ const htmlStr = getReportHtml(flowResult);
92
+ saveHtml(flowResult, htmlStr);
93
+ }}
94
+ label="Button that saves the report as HTML"
95
+ >{strings.save}</TopbarButton>
96
+ }
97
+ {
98
+ saveAsGist &&
99
+ <TopbarButton
100
+ onClick={() => saveAsGist(flowResult)}
101
+ label="Button that saves the report to a gist"
102
+ >{strings.dropdownSaveGist}</TopbarButton>
103
+ }
95
104
  <div style={{flexGrow: 1}} />
96
105
  <TopbarButton
97
106
  onClick={() => setShowHelpDialog(previous => !previous)}
@@ -10,6 +10,7 @@ import {useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from
10
10
  import type {UIStringsType} from './i18n/ui-strings';
11
11
 
12
12
  const FlowResultContext = createContext<LH.FlowResult|undefined>(undefined);
13
+ const OptionsContext = createContext<LH.FlowReportOptions>({});
13
14
 
14
15
  function getHashParam(param: string): string|null {
15
16
  const params = new URLSearchParams(location.hash.replace('#', '?'));
@@ -147,8 +148,13 @@ function useExternalRenderer<T extends Element>(
147
148
  return ref;
148
149
  }
149
150
 
151
+ function useOptions() {
152
+ return useContext(OptionsContext);
153
+ }
154
+
150
155
  export {
151
156
  FlowResultContext,
157
+ OptionsContext,
152
158
  classNames,
153
159
  getScreenDimensions,
154
160
  getFullPageScreenshot,
@@ -158,4 +164,5 @@ export {
158
164
  useHashParams,
159
165
  useHashState,
160
166
  useExternalRenderer,
167
+ useOptions,
161
168
  };
@@ -35,6 +35,7 @@ export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
35
35
  return renderReport(hashState.currentLhr, {
36
36
  disableAutoDarkModeAndFireworks: true,
37
37
  omitTopbar: true,
38
+ omitGlobalStyles: true,
38
39
  onPageAnchorRendered: link => convertAnchor(link, hashState.index),
39
40
  });
40
41
  }, [hashState]);
@@ -0,0 +1,16 @@
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
+
9
+ import {createStylesElement} from '../../../report/renderer/api';
10
+ import {useExternalRenderer} from '../util';
11
+
12
+ export const Styles: FunctionComponent = () => {
13
+ const ref = useExternalRenderer<HTMLDivElement>(createStylesElement);
14
+
15
+ return <div ref={ref}/>;
16
+ };
@@ -8,7 +8,7 @@ import {jest} from '@jest/globals';
8
8
  import {FunctionComponent} from 'preact';
9
9
  import {act, render} from '@testing-library/preact';
10
10
 
11
- import {FlowResultContext} from '../src/util';
11
+ import {FlowResultContext, OptionsContext} from '../src/util';
12
12
  import {I18nProvider} from '../src/i18n/i18n';
13
13
 
14
14
  const mockSaveFile = jest.fn();
@@ -31,19 +31,24 @@ const flowResult = {
31
31
  } as any;
32
32
 
33
33
  let wrapper: FunctionComponent;
34
+ let options: LH.FlowReportOptions;
34
35
 
35
36
  beforeEach(() => {
36
37
  mockSaveFile.mockReset();
38
+ options = {};
37
39
  wrapper = ({children}) => (
38
- <FlowResultContext.Provider value={flowResult}>
39
- <I18nProvider>
40
- {children}
41
- </I18nProvider>
42
- </FlowResultContext.Provider>
40
+ <OptionsContext.Provider value={options}>
41
+ <FlowResultContext.Provider value={flowResult}>
42
+ <I18nProvider>
43
+ {children}
44
+ </I18nProvider>
45
+ </FlowResultContext.Provider>
46
+ </OptionsContext.Provider>
43
47
  );
44
48
  });
45
49
 
46
50
  it('save button opens save dialog for HTML file', async () => {
51
+ options = {getReportHtml: () => '<html></html>'};
47
52
  const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});
48
53
 
49
54
  const saveButton = root.getByText('Save');
@@ -55,6 +60,17 @@ it('save button opens save dialog for HTML file', async () => {
55
60
  );
56
61
  });
57
62
 
63
+ it('provides save as gist option if defined', async () => {
64
+ const saveAsGist = jest.fn();
65
+ options = {saveAsGist};
66
+ const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});
67
+
68
+ const saveButton = root.getByText('Save as Gist');
69
+ saveButton.click();
70
+
71
+ expect(saveAsGist).toHaveBeenCalledWith(flowResult);
72
+ });
73
+
58
74
  it('toggles help dialog', async () => {
59
75
  const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});
60
76
 
@@ -20,6 +20,10 @@ declare global {
20
20
  // Expose global types in LH namespace.
21
21
  module LH {
22
22
  export type ConfigSettings = Settings.ConfigSettings;
23
+ export interface FlowReportOptions {
24
+ getReportHtml?: (flowResult: FlowResult_) => string;
25
+ saveAsGist?: (flowResult: FlowResult_) => void;
26
+ }
23
27
 
24
28
  export import FlowResult = FlowResult_;
25
29
  }
@@ -23,14 +23,6 @@ const stackPacksToInclude = [
23
23
  packId: 'drupal',
24
24
  requiredStacks: ['js:drupal'],
25
25
  },
26
- {
27
- packId: 'react',
28
- requiredStacks: ['js:react'],
29
- },
30
- {
31
- packId: 'angular',
32
- requiredStacks: ['js:@angular/core'],
33
- },
34
26
  {
35
27
  packId: 'amp',
36
28
  requiredStacks: ['js:amp'],
@@ -47,6 +39,18 @@ const stackPacksToInclude = [
47
39
  packId: 'joomla',
48
40
  requiredStacks: ['js:joomla'],
49
41
  },
42
+ {
43
+ packId: 'next.js',
44
+ requiredStacks: ['js:next'],
45
+ },
46
+ {
47
+ packId: 'angular',
48
+ requiredStacks: ['js:@angular/core'],
49
+ },
50
+ {
51
+ packId: 'react',
52
+ requiredStacks: ['js:react'],
53
+ },
50
54
  ];
51
55
 
52
56
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.0.0-dev.20211123",
3
+ "version": "9.1.0-dev.20211126",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -196,7 +196,7 @@
196
196
  "jpeg-js": "^0.4.1",
197
197
  "js-library-detector": "^6.4.0",
198
198
  "lighthouse-logger": "^1.3.0",
199
- "lighthouse-stack-packs": "^1.5.0",
199
+ "lighthouse-stack-packs": "^1.6.0",
200
200
  "lodash.clonedeep": "^4.5.0",
201
201
  "lodash.get": "^4.4.2",
202
202
  "lodash.isequal": "^4.5.0",
@@ -43,4 +43,15 @@ function getLhrFilenamePrefix(lhr) {
43
43
  return getFilenamePrefix(hostname, lhr.fetchTime);
44
44
  }
45
45
 
46
- module.exports = {getLhrFilenamePrefix, getFilenamePrefix};
46
+ /**
47
+ * Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS.
48
+ * @param {{name: string, steps: Array<{lhr: {fetchTime: string}}>}} flowResult
49
+ * @return {string}
50
+ */
51
+ function getFlowResultFilenamePrefix(flowResult) {
52
+ const lhr = flowResult.steps[0].lhr;
53
+ const name = flowResult.name.replace(/\s/g, '-');
54
+ return getFilenamePrefix(name, lhr.fetchTime);
55
+ }
56
+
57
+ module.exports = {getLhrFilenamePrefix, getFilenamePrefix, getFlowResultFilenamePrefix};
@@ -63,4 +63,18 @@ function convertMarkdownCodeSnippets(markdownText) {
63
63
  return dom.convertMarkdownCodeSnippets(markdownText);
64
64
  }
65
65
 
66
- export {renderReport, renderCategoryScore, saveFile, convertMarkdownCodeSnippets};
66
+ /**
67
+ * @return {DocumentFragment}
68
+ */
69
+ function createStylesElement() {
70
+ const dom = new DOM(document, document.documentElement);
71
+ return dom.createComponent('styles');
72
+ }
73
+
74
+ export {
75
+ renderReport,
76
+ renderCategoryScore,
77
+ saveFile,
78
+ convertMarkdownCodeSnippets,
79
+ createStylesElement,
80
+ };
@@ -29,6 +29,7 @@ import {PerformanceCategoryRenderer} from './performance-category-renderer.js';
29
29
  import {PwaCategoryRenderer} from './pwa-category-renderer.js';
30
30
  import {Util} from './util.js';
31
31
 
32
+
32
33
  export class ReportRenderer {
33
34
  /**
34
35
  * @param {DOM} dom
@@ -322,7 +323,9 @@ export class ReportRenderer {
322
323
  categoryRenderer.injectFinalScreenshot(categories, report.audits, scoreScale);
323
324
 
324
325
  const reportFragment = this._dom.createFragment();
325
- reportFragment.append(this._dom.createComponent('styles'));
326
+ if (!this._opts.omitGlobalStyles) {
327
+ reportFragment.append(this._dom.createComponent('styles'));
328
+ }
326
329
 
327
330
  if (!this._opts.omitTopbar) {
328
331
  reportFragment.appendChild(this._renderReportTopbar(report));
@@ -17,6 +17,8 @@ declare module Renderer {
17
17
 
18
18
  /** Disable the topbar UI component */
19
19
  omitTopbar?: boolean;
20
+ /** Prevent injection of report styles. Set to true if these styles are already included by the environment. */
21
+ omitGlobalStyles?: boolean;
20
22
  /**
21
23
  * Convert report anchor links to a different format.
22
24
  * Flow report uses this to convert `#seo` to `#index=0&anchor=seo`.
@@ -3,7 +3,7 @@
3
3
  "message": "Всички отчети"
4
4
  },
5
5
  "flow-report/src/i18n/ui-strings.js | categories": {
6
- "message": "Categories"
6
+ "message": "Категории"
7
7
  },
8
8
  "flow-report/src/i18n/ui-strings.js | categoryAccessibility": {
9
9
  "message": "Достъпност"
@@ -24,40 +24,40 @@
24
24
  "message": "Настолни компютри"
25
25
  },
26
26
  "flow-report/src/i18n/ui-strings.js | helpDialogTitle": {
27
- "message": "Understanding the Lighthouse Flow Report"
27
+ "message": "Тълкуване на отчета на Lighthouse за навигацията"
28
28
  },
29
29
  "flow-report/src/i18n/ui-strings.js | helpLabel": {
30
- "message": "Understanding Flows"
30
+ "message": "Информация за навигацията"
31
31
  },
32
32
  "flow-report/src/i18n/ui-strings.js | helpUseCaseInstructionNavigation": {
33
- "message": "Use Navigation reports to..."
33
+ "message": "Използване на отчетите за навигирането за..."
34
34
  },
35
35
  "flow-report/src/i18n/ui-strings.js | helpUseCaseInstructionSnapshot": {
36
- "message": "Use Snapshot reports to..."
36
+ "message": "Използвайте отчетите за моментната снимка за..."
37
37
  },
38
38
  "flow-report/src/i18n/ui-strings.js | helpUseCaseInstructionTimespan": {
39
- "message": "Use Timespan reports to..."
39
+ "message": "Използване на отчетите за периода от време за..."
40
40
  },
41
41
  "flow-report/src/i18n/ui-strings.js | helpUseCaseNavigation1": {
42
- "message": "Obtain a Lighthouse Performance score."
42
+ "message": "Получаване на резултат за ефективността от Lighthouse."
43
43
  },
44
44
  "flow-report/src/i18n/ui-strings.js | helpUseCaseNavigation2": {
45
- "message": "Measure page load Performance metrics such as Largest Contentful Paint and Speed Index."
45
+ "message": "Измерване на показатели за ефективността при зареждане на страниците, като например изобразяване на най-голямото съдържание (LCP) и индекс на скоростта."
46
46
  },
47
47
  "flow-report/src/i18n/ui-strings.js | helpUseCaseNavigation3": {
48
- "message": "Assess Progressive Web App capabilities."
48
+ "message": "Тестване на възможностите на прогресивни уеб приложения."
49
49
  },
50
50
  "flow-report/src/i18n/ui-strings.js | helpUseCaseSnapshot1": {
51
- "message": "Find accessibility issues in single page applications or complex forms."
51
+ "message": "Намиране на проблеми с достъпността в приложения от една страница и сложни формуляри."
52
52
  },
53
53
  "flow-report/src/i18n/ui-strings.js | helpUseCaseSnapshot2": {
54
- "message": "Evaluate best practices of menus and UI elements hidden behind interaction."
54
+ "message": "Анализ на най-добрите практики, свързани с взаимодействията с менюта и елементи на ПИ."
55
55
  },
56
56
  "flow-report/src/i18n/ui-strings.js | helpUseCaseTimespan1": {
57
- "message": "Measure layout shifts and JavaScript execution time on a series of interactions."
57
+ "message": "Измерване на структурните промени и времето за изпълнение на JavaScript за поредица от взаимодействия."
58
58
  },
59
59
  "flow-report/src/i18n/ui-strings.js | helpUseCaseTimespan2": {
60
- "message": "Discover performance opportunities to improve the experience for long-lived pages and single-page applications."
60
+ "message": "Откриване на възможности за подобряване на ефективността на продължително отворените страници и приложенията от една страница."
61
61
  },
62
62
  "flow-report/src/i18n/ui-strings.js | highestImpact": {
63
63
  "message": "С най-голямо въздействие"
@@ -72,7 +72,7 @@
72
72
  "message": "Зареждане на страницата"
73
73
  },
74
74
  "flow-report/src/i18n/ui-strings.js | navigationLongDescription": {
75
- "message": "Navigation reports analyze a single page load, exactly like the original Lighthouse reports."
75
+ "message": "Отчетите за навигацията анализират зареждането на отделни страници, точно както първоначалните отчети на Lighthouse."
76
76
  },
77
77
  "flow-report/src/i18n/ui-strings.js | navigationReport": {
78
78
  "message": "Отчет за навигирането"
@@ -99,13 +99,13 @@
99
99
  "message": "Добра"
100
100
  },
101
101
  "flow-report/src/i18n/ui-strings.js | save": {
102
- "message": "Save"
102
+ "message": "Запазване"
103
103
  },
104
104
  "flow-report/src/i18n/ui-strings.js | snapshotDescription": {
105
105
  "message": "Моментно състояние на страницата"
106
106
  },
107
107
  "flow-report/src/i18n/ui-strings.js | snapshotLongDescription": {
108
- "message": "Snapshot reports analyze the page in a particular state, typically after user interactions."
108
+ "message": "Отчетите за моментната снимка анализират страницата в определено състояние, обикновено след потребителски взаимодействия."
109
109
  },
110
110
  "flow-report/src/i18n/ui-strings.js | snapshotReport": {
111
111
  "message": "Отчет за моментното състояние"
@@ -120,7 +120,7 @@
120
120
  "message": "Потребителски взаимодействия"
121
121
  },
122
122
  "flow-report/src/i18n/ui-strings.js | timespanLongDescription": {
123
- "message": "Timespan reports analyze an arbitrary period of time, typically containing user interactions."
123
+ "message": "Отчетите за периода от време анализират произволен времеви интервал, който обикновено съдържа потребителски взаимодействия."
124
124
  },
125
125
  "flow-report/src/i18n/ui-strings.js | timespanReport": {
126
126
  "message": "Отчет за периода от време"