blob: 9339357a79084929b26a4c5d369b98a29fc48a1b [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Components from '../../components/components.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as Timeline from '../../timeline/timeline.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import type * as ReportRenderer from './LighthouseReporterTypes.js';
const UIStrings = {
/**
*@description Label for view trace button when simulated throttling is enabled
*/
viewOriginalTrace: 'View Original Trace',
/**
*@description Text of the timeline button in Lighthouse Report Renderer
*/
viewTrace: 'View Trace',
/**
*@description Help text for 'View Trace' button
*/
thePerformanceMetricsAboveAre:
'The performance metrics above are simulated and won\'t match the timings found in this trace. Disable simulated throttling in "Lighthouse Settings" if you want the timings to match.',
};
const str_ = i18n.i18n.registerUIStrings('panels/lighthouse/LighthouseReportRenderer.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const MaxLengthForLinks = 40;
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
export class LighthouseReportRenderer extends self.ReportRenderer {
constructor(dom: DOM) {
super(dom);
}
static addViewTraceButton(el: Element, artifacts?: ReportRenderer.RunnerResultArtifacts): void {
if (!artifacts || !artifacts.traces || !artifacts.traces.defaultPass) {
return;
}
const simulated = artifacts.settings.throttlingMethod === 'simulate';
const container = el.querySelector('.lh-audit-group');
if (!container) {
return;
}
const disclaimerEl = container.querySelector('.lh-metrics__disclaimer');
// If it was a PWA-only run, we'd have a trace but no perf category to add the button to
if (!disclaimerEl) {
return;
}
const defaultPassTrace = artifacts.traces.defaultPass;
const label = simulated ? i18nString(UIStrings.viewOriginalTrace) : i18nString(UIStrings.viewTrace);
const timelineButton = UI.UIUtils.createTextButton(label, onViewTraceClick, 'view-trace');
if (simulated) {
UI.Tooltip.Tooltip.install(timelineButton, i18nString(UIStrings.thePerformanceMetricsAboveAre));
}
container.insertBefore(timelineButton, disclaimerEl.nextSibling);
async function onViewTraceClick(): Promise<void> {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.LighthouseViewTrace);
await UI.InspectorView.InspectorView.instance().showPanel('timeline');
Timeline.TimelinePanel.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
}
}
static async linkifyNodeDetails(el: Element): Promise<void> {
const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget();
if (!mainTarget) {
return;
}
const domModel = mainTarget.model(SDK.DOMModel.DOMModel);
if (!domModel) {
return;
}
for (const origElement of el.getElementsByClassName('lh-node')) {
const origHTMLElement = origElement as HTMLElement;
const detailsItem = origHTMLElement.dataset as unknown as ReportRenderer.NodeDetailsJSON;
if (!detailsItem.path) {
continue;
}
const nodeId = await domModel.pushNodeByPathToFrontend(detailsItem.path);
if (!nodeId) {
continue;
}
const node = domModel.nodeForId(nodeId);
if (!node) {
continue;
}
const element = await Common.Linkifier.Linkifier.linkify(
node, {tooltip: detailsItem.snippet, preventKeyboardFocus: undefined});
UI.Tooltip.Tooltip.install(origHTMLElement, '');
const screenshotElement = origHTMLElement.querySelector('.lh-element-screenshot');
origHTMLElement.textContent = '';
if (screenshotElement) {
origHTMLElement.append(screenshotElement);
}
origHTMLElement.appendChild(element);
}
}
static async linkifySourceLocationDetails(el: Element): Promise<void> {
for (const origElement of el.getElementsByClassName('lh-source-location')) {
const origHTMLElement = origElement as HTMLElement;
const detailsItem = origHTMLElement.dataset as ReportRenderer.SourceLocationDetailsJSON;
if (!detailsItem.sourceUrl || !detailsItem.sourceLine || !detailsItem.sourceColumn) {
continue;
}
const url = detailsItem.sourceUrl;
const line = Number(detailsItem.sourceLine);
const column = Number(detailsItem.sourceColumn);
const element = await Components.Linkifier.Linkifier.linkifyURL(url, {
lineNumber: line,
columnNumber: column,
inlineFrameIndex: 0,
maxLength: MaxLengthForLinks,
bypassURLTrimming: undefined,
className: undefined,
preventClick: undefined,
tabStop: undefined,
text: undefined,
});
UI.Tooltip.Tooltip.install(origHTMLElement, '');
origHTMLElement.textContent = '';
origHTMLElement.appendChild(element);
}
}
static handleDarkMode(el: Element): void {
if (ThemeSupport.ThemeSupport.instance().themeName() === 'dark') {
el.classList.add('dark');
}
}
}
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
export class LighthouseReportUIFeatures extends self.ReportUIFeatures {
_beforePrint: (() => void)|null;
_afterPrint: (() => void)|null;
constructor(dom: DOM) {
super(dom);
this._beforePrint = null;
this._afterPrint = null;
}
setBeforePrint(beforePrint: (() => void)|null): void {
this._beforePrint = beforePrint;
}
setAfterPrint(afterPrint: (() => void)|null): void {
this._afterPrint = afterPrint;
}
/**
* Returns the html that recreates this report.
*/
getReportHtml(): string {
this.resetUIState();
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
return Lighthouse.ReportGenerator.generateReportHtml(this.json);
}
/**
* Downloads a file (blob) using the system dialog prompt.
*/
async _saveFile(blob: Blob|File): Promise<void> {
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
const domain = new Common.ParsedURL.ParsedURL(this.json.finalUrl).domain();
const sanitizedDomain = domain.replace(/[^a-z0-9.-]+/gi, '_');
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
const timestamp = Platform.DateUtilities.toISO8601Compact(new Date(this.json.fetchTime));
const ext = blob.type.match('json') ? '.json' : '.html';
const basename = `${sanitizedDomain}-${timestamp}${ext}`;
const text = await blob.text();
Workspace.FileManager.FileManager.instance().save(basename, text, true /* forceSaveAs */);
}
async _print(): Promise<void> {
const document = this.getDocument();
const clonedReport = (document.querySelector('.lh-root') as HTMLElement).cloneNode(true);
const printWindow = window.open('', '_blank', 'channelmode=1,status=1,resizable=1');
if (!printWindow) {
return;
}
const style = printWindow.document.createElement('style');
style.textContent = Root.Runtime.cachedResources.get('third_party/lighthouse/report-assets/report.css') || '';
printWindow.document.head.appendChild(style);
printWindow.document.body.replaceWith(clonedReport);
// Linkified nodes are shadow elements, which aren't exposed via `cloneNode`.
await LighthouseReportRenderer.linkifyNodeDetails(clonedReport as HTMLElement);
if (this._beforePrint) {
this._beforePrint();
}
printWindow.focus();
printWindow.print();
printWindow.close();
if (this._afterPrint) {
this._afterPrint();
}
}
getDocument(): Document {
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
return this._document;
}
resetUIState(): void {
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
this._resetUIState();
}
}