Patrick Hulce | a087f62 | 2018-05-18 00:37:53 +0000 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Tim van der Lippe | 7696157 | 2021-04-06 11:48:07 +0100 | [diff] [blame] | 5 | import * as Common from '../../core/common/common.js'; |
Tim van der Lippe | e024731 | 2021-04-01 15:25:30 +0100 | [diff] [blame] | 6 | import * as Host from '../../core/host/host.js'; |
Tim van der Lippe | aa1ed7a | 2021-03-31 15:38:27 +0100 | [diff] [blame] | 7 | import * as Platform from '../../core/platform/platform.js'; |
Tim van der Lippe | e00b92f | 2021-03-31 17:52:17 +0100 | [diff] [blame] | 8 | import * as SDK from '../../core/sdk/sdk.js'; |
Tim van der Lippe | 99aeaf3 | 2021-04-09 11:33:34 +0100 | [diff] [blame] | 9 | import * as Workspace from '../../models/workspace/workspace.js'; |
Connor Clark | 4a7d834 | 2021-07-19 13:38:37 -0700 | [diff] [blame] | 10 | import * as LighthouseReport from '../../third_party/lighthouse/report/report.js'; |
Tim van der Lippe | 339ad26 | 2021-04-21 13:23:36 +0100 | [diff] [blame] | 11 | import * as Components from '../../ui/legacy/components/utils/utils.js'; |
Tim van der Lippe | aa61faf | 2021-04-07 16:32:07 +0100 | [diff] [blame] | 12 | import * as UI from '../../ui/legacy/legacy.js'; |
Tim van der Lippe | fddcf40 | 2021-04-19 14:00:29 +0100 | [diff] [blame] | 13 | import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; |
Tim van der Lippe | 01e1c46 | 2021-04-19 16:04:03 +0100 | [diff] [blame] | 14 | import * as Timeline from '../timeline/timeline.js'; |
Jack Franklin | 3a80260 | 2022-07-13 08:39:42 +0000 | [diff] [blame] | 15 | import { |
| 16 | type RunnerResultArtifacts, |
| 17 | type NodeDetailsJSON, |
| 18 | type SourceLocationDetailsJSON, |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 19 | type ReportJSON, |
Jack Franklin | 3a80260 | 2022-07-13 08:39:42 +0000 | [diff] [blame] | 20 | } from './LighthouseReporterTypes.js'; |
Tim van der Lippe | 1e10f85 | 2020-10-30 14:35:01 +0000 | [diff] [blame] | 21 | |
Paul Lewis | cf2ef22 | 2019-11-22 14:55:35 +0000 | [diff] [blame] | 22 | const MaxLengthForLinks = 40; |
| 23 | |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 24 | interface RenderReportOpts { |
| 25 | beforePrint?: () => void; |
| 26 | afterPrint?: () => void; |
| 27 | } |
| 28 | |
| 29 | export class LighthouseReportRenderer { |
| 30 | static renderLighthouseReport(lhr: ReportJSON, artifacts?: RunnerResultArtifacts, opts?: RenderReportOpts): |
| 31 | HTMLElement { |
| 32 | let onViewTrace: (() => Promise<void>)|undefined = undefined; |
| 33 | if (artifacts) { |
| 34 | onViewTrace = async(): Promise<void> => { |
| 35 | const defaultPassTrace = artifacts.traces.defaultPass; |
| 36 | Host.userMetrics.actionTaken(Host.UserMetrics.Action.LighthouseViewTrace); |
| 37 | await UI.InspectorView.InspectorView.instance().showPanel('timeline'); |
| 38 | Timeline.TimelinePanel.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents); |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | async function onSaveFileOverride(blob: Blob): Promise<void> { |
| 43 | const domain = new Common.ParsedURL.ParsedURL(lhr.finalUrl || lhr.finalDisplayedUrl).domain(); |
| 44 | const sanitizedDomain = domain.replace(/[^a-z0-9.-]+/gi, '_'); |
| 45 | const timestamp = Platform.DateUtilities.toISO8601Compact(new Date(lhr.fetchTime)); |
| 46 | const ext = blob.type.match('json') ? '.json' : '.html'; |
| 47 | const basename = `${sanitizedDomain}-${timestamp}${ext}` as Platform.DevToolsPath.RawPathString; |
| 48 | const text = await blob.text(); |
| 49 | void Workspace.FileManager.FileManager.instance().save(basename, text, true /* forceSaveAs */); |
| 50 | } |
| 51 | |
| 52 | async function onPrintOverride(rootEl: HTMLElement): Promise<void> { |
| 53 | const clonedReport = rootEl.cloneNode(true); |
| 54 | const printWindow = window.open('', '_blank', 'channelmode=1,status=1,resizable=1'); |
| 55 | if (!printWindow) { |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | printWindow.document.body.replaceWith(clonedReport); |
| 60 | // Linkified nodes are shadow elements, which aren't exposed via `cloneNode`. |
| 61 | await LighthouseReportRenderer.linkifyNodeDetails(clonedReport as HTMLElement); |
| 62 | |
| 63 | opts?.beforePrint?.(); |
| 64 | printWindow.focus(); |
| 65 | printWindow.print(); |
| 66 | printWindow.close(); |
| 67 | opts?.afterPrint?.(); |
| 68 | } |
| 69 | |
| 70 | function getStandaloneReportHTML(): string { |
| 71 | // @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628 |
Adam Raine | de6c4e5 | 2023-02-09 14:10:42 -0800 | [diff] [blame^] | 72 | return Lighthouse.ReportGenerator.ReportGenerator.generateReportHtml(lhr); |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 73 | } |
| 74 | |
| 75 | const reportEl = LighthouseReport.renderReport(lhr, { |
| 76 | // Disable dark mode so we can manually adjust it. |
| 77 | disableDarkMode: true, |
| 78 | onViewTrace, |
| 79 | onSaveFileOverride, |
| 80 | onPrintOverride, |
| 81 | getStandaloneReportHTML, |
| 82 | }); |
| 83 | reportEl.classList.add('lh-devtools'); |
| 84 | |
| 85 | const updateDarkModeIfNecessary = (): void => { |
| 86 | reportEl.classList.toggle('lh-dark', ThemeSupport.ThemeSupport.instance().themeName() === 'dark'); |
| 87 | }; |
| 88 | ThemeSupport.ThemeSupport.instance().addEventListener( |
| 89 | ThemeSupport.ThemeChangeEvent.eventName, updateDarkModeIfNecessary); |
| 90 | updateDarkModeIfNecessary(); |
| 91 | |
| 92 | // @ts-ignore Expose LHR on DOM for e2e tests |
| 93 | reportEl._lighthouseResultForTesting = lhr; |
| 94 | // @ts-ignore Expose Artifacts on DOM for e2e tests |
| 95 | reportEl._lighthouseArtifactsForTesting = artifacts; |
| 96 | |
| 97 | // Linkifying requires the target be loaded. Do not block the report |
| 98 | // from rendering, as this is just an embellishment and the main target |
| 99 | // could take awhile to load. |
| 100 | void LighthouseReportRenderer.waitForMainTargetLoad().then(() => { |
| 101 | void LighthouseReportRenderer.linkifyNodeDetails(reportEl); |
| 102 | void LighthouseReportRenderer.linkifySourceLocationDetails(reportEl); |
| 103 | }); |
| 104 | |
| 105 | return reportEl; |
Tim van der Lippe | 4df32c9 | 2020-11-06 12:35:05 +0000 | [diff] [blame] | 106 | } |
Jan Scheffler | c5a400f | 2021-01-22 17:41:47 +0100 | [diff] [blame] | 107 | |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 108 | static async waitForMainTargetLoad(): Promise<void> { |
| 109 | const mainTarget = SDK.TargetManager.TargetManager.instance().mainFrameTarget(); |
| 110 | if (!mainTarget) { |
Paul Irish | 8f1e33d | 2018-05-31 02:29:50 +0000 | [diff] [blame] | 111 | return; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 112 | } |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 113 | const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel); |
| 114 | if (!resourceTreeModel) { |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 115 | return; |
| 116 | } |
Adam Raine | 2a9afbc | 2022-11-30 10:28:58 -0800 | [diff] [blame] | 117 | await resourceTreeModel.once(SDK.ResourceTreeModel.Events.Load); |
Patrick Hulce | a087f62 | 2018-05-18 00:37:53 +0000 | [diff] [blame] | 118 | } |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 119 | |
Jan Scheffler | c5a400f | 2021-01-22 17:41:47 +0100 | [diff] [blame] | 120 | static async linkifyNodeDetails(el: Element): Promise<void> { |
Danil Somsikov | e1ee1b2 | 2022-11-07 18:44:57 +0100 | [diff] [blame] | 121 | const mainTarget = SDK.TargetManager.TargetManager.instance().mainFrameTarget(); |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 122 | if (!mainTarget) { |
| 123 | return; |
| 124 | } |
Paul Lewis | daac106 | 2020-03-05 14:37:10 +0000 | [diff] [blame] | 125 | const domModel = mainTarget.model(SDK.DOMModel.DOMModel); |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 126 | if (!domModel) { |
| 127 | return; |
| 128 | } |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 129 | |
| 130 | for (const origElement of el.getElementsByClassName('lh-node')) { |
Jan Scheffler | c5a400f | 2021-01-22 17:41:47 +0100 | [diff] [blame] | 131 | const origHTMLElement = origElement as HTMLElement; |
Connor Clark | 4a7d834 | 2021-07-19 13:38:37 -0700 | [diff] [blame] | 132 | const detailsItem = origHTMLElement.dataset as unknown as NodeDetailsJSON; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 133 | if (!detailsItem.path) { |
cjamcl@google.com | 5c46b7e | 2018-12-04 14:44:47 +0000 | [diff] [blame] | 134 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 135 | } |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 136 | |
| 137 | const nodeId = await domModel.pushNodeByPathToFrontend(detailsItem.path); |
| 138 | |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 139 | if (!nodeId) { |
cjamcl@google.com | 5c46b7e | 2018-12-04 14:44:47 +0000 | [diff] [blame] | 140 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 141 | } |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 142 | const node = domModel.nodeForId(nodeId); |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 143 | if (!node) { |
cjamcl@google.com | 5c46b7e | 2018-12-04 14:44:47 +0000 | [diff] [blame] | 144 | continue; |
Tim van der Lippe | 1d6e57a | 2019-09-30 11:55:34 +0000 | [diff] [blame] | 145 | } |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 146 | |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 147 | const element = await Common.Linkifier.Linkifier.linkify( |
| 148 | node, {tooltip: detailsItem.snippet, preventKeyboardFocus: undefined}); |
Tim van der Lippe | 70842f3 | 2020-11-23 16:56:57 +0000 | [diff] [blame] | 149 | UI.Tooltip.Tooltip.install(origHTMLElement, ''); |
Connor Clark | 49872c0 | 2020-12-16 13:39:28 -0600 | [diff] [blame] | 150 | |
| 151 | const screenshotElement = origHTMLElement.querySelector('.lh-element-screenshot'); |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 152 | origHTMLElement.textContent = ''; |
Connor Clark | 49872c0 | 2020-12-16 13:39:28 -0600 | [diff] [blame] | 153 | if (screenshotElement) { |
| 154 | origHTMLElement.append(screenshotElement); |
| 155 | } |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 156 | origHTMLElement.appendChild(element); |
cjamcl@google.com | da0e4c6 | 2018-11-27 23:52:10 +0000 | [diff] [blame] | 157 | } |
| 158 | } |
cjamcl@google.com | f2f8c09 | 2019-05-30 22:01:56 +0000 | [diff] [blame] | 159 | |
Jan Scheffler | c5a400f | 2021-01-22 17:41:47 +0100 | [diff] [blame] | 160 | static async linkifySourceLocationDetails(el: Element): Promise<void> { |
Connor Clark | 0403a42 | 2019-11-18 18:03:18 -0800 | [diff] [blame] | 161 | for (const origElement of el.getElementsByClassName('lh-source-location')) { |
Jan Scheffler | c5a400f | 2021-01-22 17:41:47 +0100 | [diff] [blame] | 162 | const origHTMLElement = origElement as HTMLElement; |
Connor Clark | 4a7d834 | 2021-07-19 13:38:37 -0700 | [diff] [blame] | 163 | const detailsItem = origHTMLElement.dataset as SourceLocationDetailsJSON; |
Connor Clark | 0403a42 | 2019-11-18 18:03:18 -0800 | [diff] [blame] | 164 | if (!detailsItem.sourceUrl || !detailsItem.sourceLine || !detailsItem.sourceColumn) { |
| 165 | continue; |
| 166 | } |
| 167 | const url = detailsItem.sourceUrl; |
| 168 | const line = Number(detailsItem.sourceLine); |
| 169 | const column = Number(detailsItem.sourceColumn); |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 170 | const element = await Components.Linkifier.Linkifier.linkifyURL(url, { |
| 171 | lineNumber: line, |
| 172 | columnNumber: column, |
Brandon Walderman | 666b40a | 2021-10-19 13:29:05 -0700 | [diff] [blame] | 173 | showColumnNumber: false, |
Philip Pfaffe | 068b01d | 2021-03-22 10:46:26 +0100 | [diff] [blame] | 174 | inlineFrameIndex: 0, |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 175 | maxLength: MaxLengthForLinks, |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 176 | }); |
Tim van der Lippe | 70842f3 | 2020-11-23 16:56:57 +0000 | [diff] [blame] | 177 | UI.Tooltip.Tooltip.install(origHTMLElement, ''); |
Tim van der Lippe | fe3cdc9 | 2020-11-06 15:23:13 +0000 | [diff] [blame] | 178 | origHTMLElement.textContent = ''; |
| 179 | origHTMLElement.appendChild(element); |
Connor Clark | 0403a42 | 2019-11-18 18:03:18 -0800 | [diff] [blame] | 180 | } |
| 181 | } |
Paul Lewis | cf2ef22 | 2019-11-22 14:55:35 +0000 | [diff] [blame] | 182 | } |