lighthouse 9.0.0-dev.20211121 → 9.1.0

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 (101) hide show
  1. package/changelog.md +70 -0
  2. package/dist/report/bundle.esm.js +17 -3
  3. package/dist/report/flow.js +34 -23
  4. package/dist/report/standalone.js +3 -3
  5. package/flow-report/api.ts +18 -0
  6. package/flow-report/assets/standalone-flow-template.html +0 -1
  7. package/flow-report/{standalone-flow.tsx → clients/standalone.ts} +4 -5
  8. package/flow-report/src/app.tsx +20 -12
  9. package/flow-report/src/sidebar/flow.tsx +1 -3
  10. package/flow-report/src/sidebar/sidebar.tsx +1 -3
  11. package/flow-report/src/topbar.tsx +22 -13
  12. package/flow-report/src/util.ts +7 -0
  13. package/flow-report/src/wrappers/report.tsx +1 -0
  14. package/flow-report/src/wrappers/styles.tsx +16 -0
  15. package/flow-report/test/topbar-test.tsx +22 -6
  16. package/flow-report/types/flow-report.d.ts +4 -0
  17. package/lighthouse-cli/cli-flags.js +4 -4
  18. package/lighthouse-core/audits/audit.js +39 -7
  19. package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +1 -26
  20. package/lighthouse-core/audits/deprecations.js +10 -14
  21. package/lighthouse-core/audits/dobetterweb/geolocation-on-start.js +5 -4
  22. package/lighthouse-core/audits/dobetterweb/js-libraries.js +1 -1
  23. package/lighthouse-core/audits/dobetterweb/no-document-write.js +6 -4
  24. package/lighthouse-core/audits/dobetterweb/notification-on-start.js +6 -4
  25. package/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js +6 -4
  26. package/lighthouse-core/audits/errors-in-console.js +7 -4
  27. package/lighthouse-core/audits/no-unload-listeners.js +9 -10
  28. package/lighthouse-core/audits/seo/canonical.js +0 -14
  29. package/lighthouse-core/audits/seo/font-size.js +4 -2
  30. package/lighthouse-core/audits/seo/is-crawlable.js +2 -2
  31. package/lighthouse-core/audits/third-party-summary.js +2 -2
  32. package/lighthouse-core/audits/valid-source-maps.js +1 -1
  33. package/lighthouse-core/audits/violation-audit.js +10 -3
  34. package/lighthouse-core/computed/js-bundles.js +1 -3
  35. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +3 -3
  36. package/lighthouse-core/gather/driver/execution-context.js +1 -1
  37. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +7 -7
  38. package/lighthouse-core/gather/gatherers/stacks.js +0 -3
  39. package/lighthouse-core/lib/stack-packs.js +12 -8
  40. package/lighthouse-core/runner.js +7 -7
  41. package/package.json +4 -6
  42. package/report/generator/file-namer.js +12 -1
  43. package/report/generator/flow-report-assets.js +2 -1
  44. package/report/generator/report-assets.js +2 -5
  45. package/report/generator/report-generator.js +0 -1
  46. package/report/renderer/api.js +15 -1
  47. package/report/renderer/report-renderer.js +4 -1
  48. package/report/renderer/report-ui-features.js +1 -1
  49. package/report/renderer/swap-locale-feature.js +0 -5
  50. package/report/test/clients/bundle-test.js +2 -2
  51. package/report/test/renderer/report-ui-features-test.js +0 -1
  52. package/report/types/report-renderer.d.ts +2 -0
  53. package/shared/localization/locales/ar-XB.json +0 -3
  54. package/shared/localization/locales/ar.json +0 -3
  55. package/shared/localization/locales/bg.json +17 -20
  56. package/shared/localization/locales/ca.json +0 -3
  57. package/shared/localization/locales/cs.json +0 -3
  58. package/shared/localization/locales/da.json +16 -19
  59. package/shared/localization/locales/de.json +0 -3
  60. package/shared/localization/locales/el.json +0 -3
  61. package/shared/localization/locales/en-GB.json +19 -22
  62. package/shared/localization/locales/en-US.json +30 -3
  63. package/shared/localization/locales/en-XA.json +0 -3
  64. package/shared/localization/locales/en-XL.json +30 -3
  65. package/shared/localization/locales/es-419.json +17 -20
  66. package/shared/localization/locales/es.json +0 -3
  67. package/shared/localization/locales/fi.json +16 -19
  68. package/shared/localization/locales/fil.json +33 -36
  69. package/shared/localization/locales/fr.json +16 -19
  70. package/shared/localization/locales/he.json +16 -19
  71. package/shared/localization/locales/hi.json +0 -3
  72. package/shared/localization/locales/hr.json +16 -19
  73. package/shared/localization/locales/hu.json +16 -19
  74. package/shared/localization/locales/id.json +16 -19
  75. package/shared/localization/locales/it.json +37 -40
  76. package/shared/localization/locales/ja.json +16 -19
  77. package/shared/localization/locales/ko.json +16 -19
  78. package/shared/localization/locales/lt.json +17 -20
  79. package/shared/localization/locales/lv.json +0 -3
  80. package/shared/localization/locales/nl.json +16 -19
  81. package/shared/localization/locales/no.json +17 -20
  82. package/shared/localization/locales/pl.json +16 -19
  83. package/shared/localization/locales/pt-PT.json +37 -40
  84. package/shared/localization/locales/pt.json +16 -19
  85. package/shared/localization/locales/ro.json +0 -3
  86. package/shared/localization/locales/ru.json +16 -19
  87. package/shared/localization/locales/sk.json +0 -3
  88. package/shared/localization/locales/sl.json +0 -3
  89. package/shared/localization/locales/sr-Latn.json +16 -19
  90. package/shared/localization/locales/sr.json +16 -19
  91. package/shared/localization/locales/sv.json +17 -20
  92. package/shared/localization/locales/ta.json +0 -3
  93. package/shared/localization/locales/te.json +0 -3
  94. package/shared/localization/locales/th.json +0 -3
  95. package/shared/localization/locales/tr.json +0 -3
  96. package/shared/localization/locales/uk.json +17 -20
  97. package/shared/localization/locales/vi.json +0 -3
  98. package/shared/localization/locales/zh-HK.json +0 -3
  99. package/shared/localization/locales/zh-TW.json +0 -3
  100. package/shared/localization/locales/zh.json +0 -3
  101. package/types/externs.d.ts +5 -24
@@ -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.
@@ -203,7 +203,7 @@ class O{constructor(e){this._dom=e,this._toggleEl,this._menuEl,this.onDocumentKe
203
203
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
204
204
  * See the License for the specific language governing permissions and
205
205
  * limitations under the License.
206
- */class j{constructor(e,t={}){this.json,this._dom=e,this._opts=t,this._topbar=t.omitTopbar?null:new I(this,e),this.onMediaQueryChange=this.onMediaQueryChange.bind(this)}initFeatures(e){this.json=e,this._topbar&&(this._topbar.enable(e),this._topbar.resetUIState()),this._setupMediaQueryListeners(),this._setupThirdPartyFilter(),this._setupElementScreenshotOverlay(this._dom.rootEl);let t=!1;!(this._dom.isDevTools()||this._opts.disableAutoDarkModeAndFireworks)&&window.matchMedia("(prefers-color-scheme: dark)").matches&&(t=!0);["performance","accessibility","best-practices","seo"].every((t=>{const n=e.categories[t];return n&&1===n.score}))&&(t=!0,this._enableFireworks()),t&&T(this._dom,!0);if(e.categories.performance&&e.categories.performance.auditRefs.some((t=>Boolean("metrics"===t.group&&e.audits[t.id].errorMessage)))){this._dom.find("input.lh-metrics-toggle__input",this._dom.rootEl).checked=!0}this.json.audits["script-treemap-data"]&&this.json.audits["script-treemap-data"].details&&this.addButton({text:l.i18n.strings.viewTreemapLabel,icon:"treemap",onClick:()=>function(e){if(!e.audits["script-treemap-data"].details)throw new Error("no script treemap data found");U({lhr:{requestedUrl:e.requestedUrl,finalUrl:e.finalUrl,audits:{"script-treemap-data":e.audits["script-treemap-data"]},configSettings:{locale:e.configSettings.locale}}},P()+"/treemap/","treemap-"+$(e))}(this.json)});for(const e of this._dom.findAll("[data-i18n]",this._dom.rootEl)){const t=e.getAttribute("data-i18n");e.textContent=l.i18n.strings[t]}}addButton(e){const t=this._dom.rootEl.querySelector(".lh-audit-group--metrics");if(!t)return;let n=t.querySelector(".lh-buttons");n||(n=this._dom.createChildOf(t,"div","lh-buttons"));const r=["lh-button"];e.icon&&(r.push("lh-report-icon"),r.push(`lh-report-icon--${e.icon}`));const o=this._dom.createChildOf(n,"button",r.join(" "));return o.textContent=e.text,o.addEventListener("click",e.onClick),o}getReportHtml(){return this._topbar&&this._topbar.resetUIState(),`<!doctype html><body>${this._dom.rootEl.outerHTML}`}saveAsGist(){throw new Error("Cannot save as gist from base report")}_enableFireworks(){const e=this._dom.find(".lh-scores-container",this._dom.rootEl);e.classList.add("lh-score100"),e.addEventListener("click",(t=>{e.classList.toggle("lh-fireworks-paused")}))}_setupMediaQueryListeners(){const e=self.matchMedia("(max-width: 500px)");e.addListener(this.onMediaQueryChange),this.onMediaQueryChange(e)}_resetUIState(){this._topbar&&this._topbar.resetUIState()}onMediaQueryChange(e){this._dom.rootEl.classList.toggle("lh-narrow",e.matches)}_setupThirdPartyFilter(){const e=["uses-rel-preconnect","third-party-facades"],t=["legacy-javascript"];Array.from(this._dom.rootEl.querySelectorAll("table.lh-table")).filter((e=>e.querySelector("td.lh-table-column--url, td.lh-table-column--source-location"))).filter((t=>{const n=t.closest(".lh-audit");if(!n)throw new Error(".lh-table not within audit");return!e.includes(n.id)})).forEach((e=>{const n=function(e){return Array.from(e.tBodies[0].rows)}(e),r=this._getThirdPartyRows(n,this.json.finalUrl),o=this._dom.createComponent("3pFilter"),i=this._dom.find("input",o);i.addEventListener("change",(e=>{const t=e.target instanceof HTMLInputElement&&!e.target.checked;let o=!0,i=n[0];for(;i;){const e=t&&r.includes(i);do{i.classList.toggle("lh-row--hidden",e),i.classList.toggle("lh-row--even",!e&&o),i.classList.toggle("lh-row--odd",!e&&!o),i=i.nextElementSibling}while(i&&i.classList.contains("lh-sub-item-row"));e||(o=!o)}})),this._dom.find(".lh-3p-filter-count",o).textContent=`${r.length}`,this._dom.find(".lh-3p-ui-string",o).textContent=l.i18n.strings.thirdPartyResourcesLabel;const a=r.length===n.length,s=!r.length;if((a||s)&&(this._dom.find("div.lh-3p-filter",o).hidden=!0),!e.parentNode)return;e.parentNode.insertBefore(o,e);const c=e.closest(".lh-audit");if(!c)throw new Error(".lh-table not within audit");t.includes(c.id)&&!a&&i.click()}))}_setupElementScreenshotOverlay(e){const t=this.json.audits["full-page-screenshot"]&&this.json.audits["full-page-screenshot"].details&&"full-page-screenshot"===this.json.audits["full-page-screenshot"].details.type&&this.json.audits["full-page-screenshot"].details;t&&A.installOverlayFeature({dom:this._dom,rootEl:e,overlayContainerEl:e,fullPageScreenshot:t})}_getThirdPartyRows(e,t){const n=[],r=l.getRootDomain(t);for(const t of e){if(t.classList.contains("lh-sub-item-row"))continue;const e=t.querySelector("div.lh-text__url");if(!e)continue;const o=e.dataset.url;if(!o)continue;l.getRootDomain(o)!==r&&n.push(t)}return n}_saveFile(e){const t=G.getLhrFilenamePrefix(this.json);this._dom.saveFile(e,t)}}
206
+ */class j{constructor(e,t={}){this.json,this._dom=e,this._opts=t,this._topbar=t.omitTopbar?null:new I(this,e),this.onMediaQueryChange=this.onMediaQueryChange.bind(this)}initFeatures(e){this.json=e,this._topbar&&(this._topbar.enable(e),this._topbar.resetUIState()),this._setupMediaQueryListeners(),this._setupThirdPartyFilter(),this._setupElementScreenshotOverlay(this._dom.rootEl);let t=!1;!(this._dom.isDevTools()||this._opts.disableAutoDarkModeAndFireworks)&&window.matchMedia("(prefers-color-scheme: dark)").matches&&(t=!0);["performance","accessibility","best-practices","seo"].every((t=>{const n=e.categories[t];return n&&1===n.score}))&&(t=!0,this._enableFireworks()),t&&T(this._dom,!0);if(e.categories.performance&&e.categories.performance.auditRefs.some((t=>Boolean("metrics"===t.group&&e.audits[t.id].errorMessage)))){this._dom.find("input.lh-metrics-toggle__input",this._dom.rootEl).checked=!0}this.json.audits["script-treemap-data"]&&this.json.audits["script-treemap-data"].details&&this.addButton({text:l.i18n.strings.viewTreemapLabel,icon:"treemap",onClick:()=>function(e){if(!e.audits["script-treemap-data"].details)throw new Error("no script treemap data found");U({lhr:{requestedUrl:e.requestedUrl,finalUrl:e.finalUrl,audits:{"script-treemap-data":e.audits["script-treemap-data"]},configSettings:{locale:e.configSettings.locale}}},P()+"/treemap/","treemap-"+$(e))}(this.json)});for(const e of this._dom.findAll("[data-i18n]",this._dom.rootEl)){const t=e.getAttribute("data-i18n");e.textContent=l.i18n.strings[t]}}addButton(e){const t=this._dom.rootEl.querySelector(".lh-audit-group--metrics");if(!t)return;let n=t.querySelector(".lh-buttons");n||(n=this._dom.createChildOf(t,"div","lh-buttons"));const r=["lh-button"];e.icon&&(r.push("lh-report-icon"),r.push(`lh-report-icon--${e.icon}`));const o=this._dom.createChildOf(n,"button",r.join(" "));return o.textContent=e.text,o.addEventListener("click",e.onClick),o}getReportHtml(){return this._topbar&&this._topbar.resetUIState(),`<!doctype html><body style="margin: 0">${this._dom.rootEl.outerHTML}`}saveAsGist(){throw new Error("Cannot save as gist from base report")}_enableFireworks(){const e=this._dom.find(".lh-scores-container",this._dom.rootEl);e.classList.add("lh-score100"),e.addEventListener("click",(t=>{e.classList.toggle("lh-fireworks-paused")}))}_setupMediaQueryListeners(){const e=self.matchMedia("(max-width: 500px)");e.addListener(this.onMediaQueryChange),this.onMediaQueryChange(e)}_resetUIState(){this._topbar&&this._topbar.resetUIState()}onMediaQueryChange(e){this._dom.rootEl.classList.toggle("lh-narrow",e.matches)}_setupThirdPartyFilter(){const e=["uses-rel-preconnect","third-party-facades"],t=["legacy-javascript"];Array.from(this._dom.rootEl.querySelectorAll("table.lh-table")).filter((e=>e.querySelector("td.lh-table-column--url, td.lh-table-column--source-location"))).filter((t=>{const n=t.closest(".lh-audit");if(!n)throw new Error(".lh-table not within audit");return!e.includes(n.id)})).forEach((e=>{const n=function(e){return Array.from(e.tBodies[0].rows)}(e),r=this._getThirdPartyRows(n,this.json.finalUrl),o=this._dom.createComponent("3pFilter"),i=this._dom.find("input",o);i.addEventListener("change",(e=>{const t=e.target instanceof HTMLInputElement&&!e.target.checked;let o=!0,i=n[0];for(;i;){const e=t&&r.includes(i);do{i.classList.toggle("lh-row--hidden",e),i.classList.toggle("lh-row--even",!e&&o),i.classList.toggle("lh-row--odd",!e&&!o),i=i.nextElementSibling}while(i&&i.classList.contains("lh-sub-item-row"));e||(o=!o)}})),this._dom.find(".lh-3p-filter-count",o).textContent=`${r.length}`,this._dom.find(".lh-3p-ui-string",o).textContent=l.i18n.strings.thirdPartyResourcesLabel;const a=r.length===n.length,s=!r.length;if((a||s)&&(this._dom.find("div.lh-3p-filter",o).hidden=!0),!e.parentNode)return;e.parentNode.insertBefore(o,e);const c=e.closest(".lh-audit");if(!c)throw new Error(".lh-table not within audit");t.includes(c.id)&&!a&&i.click()}))}_setupElementScreenshotOverlay(e){const t=this.json.audits["full-page-screenshot"]&&this.json.audits["full-page-screenshot"].details&&"full-page-screenshot"===this.json.audits["full-page-screenshot"].details.type&&this.json.audits["full-page-screenshot"].details;t&&A.installOverlayFeature({dom:this._dom,rootEl:e,overlayContainerEl:e,fullPageScreenshot:t})}_getThirdPartyRows(e,t){const n=[],r=l.getRootDomain(t);for(const t of e){if(t.classList.contains("lh-sub-item-row"))continue;const e=t.querySelector("div.lh-text__url");if(!e)continue;const o=e.dataset.url;if(!o)continue;l.getRootDomain(o)!==r&&n.push(t)}return n}_saveFile(e){const t=G.getLhrFilenamePrefix(this.json);this._dom.saveFile(e,t)}}
207
207
  /**
208
208
  * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
209
209
  * 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
@@ -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
+ }
@@ -21,7 +21,6 @@ limitations under the License.
21
21
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
22
22
  <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEhklEQVR4AWJxL/BhIAesev1U5tcflpncgNrKIsqNIwzC9feMpDUzs70kOczMzMzJJcxwCTMzncPMnOwtzBwzMzPb0vRfeZPp0VhPS5I39V5fdiXV1/VD+9QC7OVn9BsyH1XIoEI1PfmJvLFowVV564+34DFUHudbmfDh4kVXh//7XwE+WjS/YfXZe3yr4j2rqj1AIhSB7hZ8ZtPZu/zw8cK523U4wE1/rvPfWrz4zs0m9ZdC9yUJAlASdBAgocRegfF/f3/h/PuaFsxMdwjAR0vm1+06eMMfIrhLqTWqdH4EumU2SPfMhigJAlRQbZrgrRsl9U+Y2DYDFCz3ILC9kiAiqSrMwbWT0nceEnR+9Kggc2zjOJCASDENkg0a5HfZZgDP81CM3CrQs2Z1+o7DJ6ePr8sK0AOCHv5Jjdt3evyYSaZ351VIStIxPRAUtrBYbxC6w+BZ0ivVSBKkIhJhemSyZpfB00EiPO2VjzYkxhcqXQqCWCShGplvi3y0QxqbuBurMjyJeWnkHZuAEgIQGsUBqwrfjZ+IlBgKyRJzVVYF8O6qFWdh86YzQzMrZigYmxAyfvHgLZQ/LC1CbeniW2Hkqr/PH16SgvGuf2/uzNMBwJA/njxizGPtSyAf7EziJCMGRDRdhoAC4PL1A/SrKQMAAQkEfpJAcRQdrBJ7gNwjSpJsdwK+CANBkqa1LgQB4IicV9nYUct7gaxuDJUErQIiEAiMxLVOFlKzIktPpT0ggpdpC/8YAHnxbgkUY4tAAFSR7AAXNyAAWHJrA/kHGjzg5nleuwFO7Nd/IoDw4Pm58+4jNLmYG0wRA5bErc2Mr3Y+dXTDW1VvwqbJkzMCHQ4S1GTCBOIgUHJrGdEwqzR+jAp/o2qAZelUDoQnruEEdDclJI6576AlNVfc+22XN/+Y1vnJD0Yind6UpEEvn/Hqq15EYjCW7jZCJEpnNvDgkyelDjs106kuux2AAXCSobULOWP8mLhYlpoDMK4qAFXJGk+grtH8YXVz5KJblqaG1+VUdTc0I290bmUQAriGITRbdQnom0aoFj8kx1+wMD2ifncAXUQE4SkDqN1hE0jEophs1SUwZAOhUAiMCLwRtamtTZtbbmZErSAUHbSysaoEmnrsakiMiUAURi283gN6wans9oX8rOCrj7/JP35DFD+iQ7Au/K2KE1jzx6ujjUnXFH9KjEq6ZlhsTBICrNLJf47Pv/pkHzvup1w4dmUbEei0+bcXRqJuh5kVARQ8byyYxOwNGr7A87xh1tp8sGT+uMInrwi++Xj7TQz2d27NvwEkrOflAFQGIDA5khASBCGdO2/Z/MnLPwYfv5TFhjW7QhVKAB6afwe2LpFlFsCnlQEosgQgDsdOG1/LKeNqJS4JCSPJ/i+TakwEARor7gER1Iva5JmPOJK0RUqmoPnnlzFCtmIAhAAQEIQRgDaiYPIauNXcnDlRIrWNFY3hm7PG9YRqr7IV7HrCgAC17befjEvRq2nGhAHtBqDpOuI/I1diUUAMYIxEdyejBJqLnNoszGZtfiX/CztGv2mq+sdaAAAAAElFTkSuQmCC">
23
23
  <title>Lighthouse Flow Report</title>
24
- <style>/*%%LIGHTHOUSE_CSS%%*/</style>
25
24
  <style>/*%%LIGHTHOUSE_FLOW_CSS%%*/</style>
26
25
  <style>body {margin: 0}</style>
27
26
  </head>
@@ -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
  }
@@ -225,7 +225,7 @@ function getFlags(manualArgv, options = {}) {
225
225
  .options({
226
226
  'output': {
227
227
  type: 'array',
228
- default: /** @type {['html']} */ (['html']),
228
+ default: /** @type {const} */ (['html']),
229
229
  coerce: coerceOutput,
230
230
  describe: 'Reporter for the results, supports multiple values. choices: "json", "html", "csv"',
231
231
  },
@@ -309,9 +309,9 @@ function getFlags(manualArgv, options = {}) {
309
309
  })
310
310
 
311
311
  // Choices added outside of `options()` and cast so tsc picks them up.
312
- .choices('form-factor', /** @type {['mobile', 'desktop']} */ (['mobile', 'desktop']))
313
- .choices('throttling-method', /** @type {['devtools', 'provided', 'simulate']} */ (['devtools', 'provided', 'simulate']))
314
- .choices('preset', /** @type {['perf', 'experimental', 'desktop']} */ (['perf', 'experimental', 'desktop']))
312
+ .choices('form-factor', /** @type {const} */ (['mobile', 'desktop']))
313
+ .choices('throttling-method', /** @type {const} */ (['devtools', 'provided', 'simulate']))
314
+ .choices('preset', /** @type {const} */ (['perf', 'experimental', 'desktop']))
315
315
 
316
316
  .check(argv => {
317
317
  // Lighthouse doesn't need a URL if...
@@ -233,21 +233,53 @@ class Audit {
233
233
  }
234
234
 
235
235
  /**
236
- * @param {LH.Artifacts.ConsoleMessage} entry
237
- * @return {LH.Audit.Details.SourceLocationValue | undefined}
236
+ * @param {LH.Artifacts.Bundle} bundle
237
+ * @param {number} generatedLine
238
+ * @param {number} generatedColumn
239
+ * @return {LH.Audit.Details.SourceLocationValue['original']}
238
240
  */
239
- static makeSourceLocationFromConsoleMessage(entry) {
240
- if (!entry.url) return;
241
+ static _findOriginalLocation(bundle, generatedLine, generatedColumn) {
242
+ const entry = bundle?.map.findEntry(generatedLine, generatedColumn);
243
+ if (!entry) return;
241
244
 
245
+ return {
246
+ file: entry.sourceURL || '',
247
+ line: entry.sourceLineNumber || 0,
248
+ column: entry.sourceColumnNumber || 0,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * @param {string} url
254
+ * @param {number} line 0-indexed
255
+ * @param {number} column 0-indexed
256
+ * @param {LH.Artifacts.Bundle=} bundle
257
+ * @return {LH.Audit.Details.SourceLocationValue}
258
+ */
259
+ static makeSourceLocation(url, line, column, bundle) {
242
260
  return {
243
261
  type: 'source-location',
244
- url: entry.url,
262
+ url,
245
263
  urlProvider: 'network',
246
- line: entry.lineNumber || 0,
247
- column: entry.columnNumber || 0,
264
+ line,
265
+ column,
266
+ original: bundle && this._findOriginalLocation(bundle, line, column),
248
267
  };
249
268
  }
250
269
 
270
+ /**
271
+ * @param {LH.Artifacts.ConsoleMessage} entry
272
+ * @param {LH.Artifacts.Bundle=} bundle
273
+ * @return {LH.Audit.Details.SourceLocationValue | undefined}
274
+ */
275
+ static makeSourceLocationFromConsoleMessage(entry, bundle) {
276
+ if (!entry.url) return;
277
+
278
+ const line = entry.lineNumber || 0;
279
+ const column = entry.columnNumber || 0;
280
+ return this.makeSourceLocation(entry.url, line, column, bundle);
281
+ }
282
+
251
283
  /**
252
284
  * @param {number|null} score
253
285
  * @param {LH.Audit.ScoreDisplayMode} scoreDisplayMode
@@ -397,23 +397,6 @@ class LegacyJavascript extends ByteEfficiencyAudit {
397
397
  return transferRatio;
398
398
  }
399
399
 
400
- /**
401
- * @param {LH.Artifacts.Bundle} bundle
402
- * @param {number} generatedLine
403
- * @param {number} generatedColumn
404
- * @return {LH.Audit.Details.SourceLocationValue['original']}
405
- */
406
- static _findOriginalLocation(bundle, generatedLine, generatedColumn) {
407
- const entry = bundle && bundle.map.findEntry(generatedLine, generatedColumn);
408
- if (!entry) return;
409
-
410
- return {
411
- file: entry.sourceURL || '',
412
- line: entry.sourceLineNumber || 0,
413
- column: entry.sourceColumnNumber || 0,
414
- };
415
- }
416
-
417
400
  /**
418
401
  * @param {LH.Artifacts} artifacts
419
402
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@@ -456,18 +439,10 @@ class LegacyJavascript extends ByteEfficiencyAudit {
456
439
  const bundle = bundles.find(bundle => bundle.script.src === url);
457
440
  for (const match of matches) {
458
441
  const {name, line, column} = match;
459
-
460
442
  /** @type {SubItem} */
461
443
  const subItem = {
462
444
  signal: name,
463
- location: {
464
- type: 'source-location',
465
- url,
466
- line,
467
- column,
468
- original: bundle && this._findOriginalLocation(bundle, line, column),
469
- urlProvider: 'network',
470
- },
445
+ location: ByteEfficiencyAudit.makeSourceLocation(url, line, column, bundle),
471
446
  };
472
447
  item.subItems.items.push(subItem);
473
448
  }
@@ -14,6 +14,7 @@
14
14
  // TODO: when M97 is sufficiently old, drop support for console messages
15
15
 
16
16
  const Audit = require('./audit.js');
17
+ const JsBundles = require('../computed/js-bundles.js');
17
18
  const i18n = require('../lib/i18n/i18n.js');
18
19
 
19
20
  const UIStrings = {
@@ -47,34 +48,29 @@ class Deprecations extends Audit {
47
48
  title: str_(UIStrings.title),
48
49
  failureTitle: str_(UIStrings.failureTitle),
49
50
  description: str_(UIStrings.description),
50
- requiredArtifacts: ['ConsoleMessages', 'InspectorIssues'],
51
+ requiredArtifacts: ['ConsoleMessages', 'InspectorIssues', 'SourceMaps', 'ScriptElements'],
51
52
  };
52
53
  }
53
54
 
54
55
  /**
55
56
  * @param {LH.Artifacts} artifacts
56
- * @return {LH.Audit.Product}
57
+ * @param {LH.Audit.Context} context
58
+ * @return {Promise<LH.Audit.Product>}
57
59
  */
58
- static audit(artifacts) {
60
+ static async audit(artifacts, context) {
59
61
  const entries = artifacts.ConsoleMessages;
62
+ const bundles = await JsBundles.request(artifacts, context);
60
63
 
61
64
  let deprecations;
62
-
63
65
  if (artifacts.InspectorIssues.deprecations.length) {
64
66
  deprecations = artifacts.InspectorIssues.deprecations
65
67
  .map(deprecation => {
68
+ const {url, lineNumber, columnNumber} = deprecation.sourceCodeLocation;
69
+ const bundle = bundles.find(bundle => bundle.script.src === url);
66
70
  return {
67
71
  value: deprecation.message || '',
68
- /** @type {LH.Audit.Details.SourceLocationValue} */
69
- source: {
70
- type: 'source-location',
71
- url: deprecation.sourceCodeLocation.url,
72
- urlProvider: 'network',
73
- line: deprecation.sourceCodeLocation.lineNumber,
74
- // Protocol.Audits.SourceCodeLocation.columnNumber is 1-indexed,
75
- // but we use 0-indexed.
76
- column: deprecation.sourceCodeLocation.columnNumber - 1,
77
- },
72
+ // Protocol.Audits.SourceCodeLocation.columnNumber is 1-indexed, but we use 0-indexed.
73
+ source: Audit.makeSourceLocation(url, lineNumber, columnNumber - 1, bundle),
78
74
  };
79
75
  });
80
76
  } else {
@@ -38,17 +38,18 @@ class GeolocationOnStart extends ViolationAudit {
38
38
  failureTitle: str_(UIStrings.failureTitle),
39
39
  description: str_(UIStrings.description),
40
40
  supportedModes: ['navigation'],
41
- requiredArtifacts: ['ConsoleMessages'],
41
+ requiredArtifacts: ['ConsoleMessages', 'SourceMaps', 'ScriptElements'],
42
42
  };
43
43
  }
44
44
 
45
45
  /**
46
46
  * @param {LH.Artifacts} artifacts
47
- * @return {LH.Audit.Product}
47
+ * @param {LH.Audit.Context} context
48
+ * @return {Promise<LH.Audit.Product>}
48
49
  */
49
- static audit(artifacts) {
50
+ static async audit(artifacts, context) {
50
51
  // 'Only request geolocation information in response to a user gesture.'
51
- const results = ViolationAudit.getViolationResults(artifacts, /geolocation/);
52
+ const results = await ViolationAudit.getViolationResults(artifacts, context, /geolocation/);
52
53
 
53
54
  /** @type {LH.Audit.Details.Table['headings']} */
54
55
  const headings = [
@@ -61,7 +61,7 @@ class JsLibrariesAudit extends Audit {
61
61
  const details = Audit.makeTableDetails(headings, libDetails, {});
62
62
 
63
63
  const debugData = {
64
- type: /** @type {'debugdata'} */ ('debugdata'),
64
+ type: /** @type {const} */ ('debugdata'),
65
65
  stacks: artifacts.Stacks.map(stack => {
66
66
  return {
67
67
  id: stack.id,
@@ -54,16 +54,18 @@ class NoDocWriteAudit extends ViolationAudit {
54
54
  title: str_(UIStrings.title),
55
55
  failureTitle: str_(UIStrings.failureTitle),
56
56
  description: str_(UIStrings.description),
57
- requiredArtifacts: ['ConsoleMessages'],
57
+ requiredArtifacts: ['ConsoleMessages', 'SourceMaps', 'ScriptElements'],
58
58
  };
59
59
  }
60
60
 
61
61
  /**
62
62
  * @param {LH.Artifacts} artifacts
63
- * @return {LH.Audit.Product}
63
+ * @param {LH.Audit.Context} context
64
+ * @return {Promise<LH.Audit.Product>}
64
65
  */
65
- static audit(artifacts) {
66
- const results = ViolationAudit.getViolationResults(artifacts, /document\.write/);
66
+ static async audit(artifacts, context) {
67
+ const results =
68
+ await ViolationAudit.getViolationResults(artifacts, context, /document\.write/);
67
69
 
68
70
  /** @type {LH.Audit.Details.Table['headings']} */
69
71
  const headings = [