chrome-devtools-frontend
Version:
Chrome DevTools UI
226 lines (199 loc) • 8.5 kB
text/typescript
// 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.
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 SDK from '../../core/sdk/sdk.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as LighthouseReport from '../../third_party/lighthouse/report/report.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import * as Timeline from '../timeline/timeline.js';
import type {RunnerResultArtifacts, NodeDetailsJSON, SourceLocationDetailsJSON} 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;
export class LighthouseReportRenderer extends LighthouseReport.ReportRenderer {
constructor(dom: LighthouseReport.DOM) {
super(dom);
}
static addViewTraceButton(
el: Element, reportUIFeatures: LighthouseReport.ReportUIFeatures, artifacts?: 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 defaultPassTrace = artifacts.traces.defaultPass;
const text = simulated ? i18nString(UIStrings.viewOriginalTrace) : i18nString(UIStrings.viewTrace);
const timelineButton = reportUIFeatures.addButton({
text,
onClick: onViewTraceClick,
});
if (timelineButton) {
timelineButton.classList.add('lh-button--trace');
if (simulated) {
UI.Tooltip.Tooltip.install(timelineButton, i18nString(UIStrings.thePerformanceMetricsAboveAre));
}
}
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.TargetManager.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 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 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,
showColumnNumber: false,
inlineFrameIndex: 0,
maxLength: MaxLengthForLinks,
});
UI.Tooltip.Tooltip.install(origHTMLElement, '');
origHTMLElement.textContent = '';
origHTMLElement.appendChild(element);
}
}
static handleDarkMode(el: Element): void {
const updateDarkModeIfNecessary = (): void => {
el.classList.toggle('lh-dark', ThemeSupport.ThemeSupport.instance().themeName() === 'dark');
};
ThemeSupport.ThemeSupport.instance().addEventListener(
ThemeSupport.ThemeChangeEvent.eventName, updateDarkModeIfNecessary);
updateDarkModeIfNecessary();
}
}
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
export class LighthouseReportUIFeatures extends LighthouseReport.ReportUIFeatures {
private beforePrint: (() => void)|null;
private afterPrint: (() => void)|null;
constructor(dom: LighthouseReport.DOM) {
super(dom);
this.beforePrint = null;
this.afterPrint = null;
this._topbar._print = this._print.bind(this);
}
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-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
return Lighthouse.ReportGenerator.generateReportHtml(this.json);
}
/**
* Downloads a file (blob) using the system dialog prompt.
*/
// This implements the interface ReportUIFeatures from lighthouse
// which follows a different naming convention.
// eslint-disable-next-line rulesdir/no_underscored_properties, @typescript-eslint/naming-convention
async _saveFile(blob: Blob|File): Promise<void> {
const domain = new Common.ParsedURL.ParsedURL(this.json.finalUrl).domain();
const sanitizedDomain = domain.replace(/[^a-z0-9.-]+/gi, '_');
const timestamp = Platform.DateUtilities.toISO8601Compact(new Date(this.json.fetchTime));
const ext = blob.type.match('json') ? '.json' : '.html';
const basename = `${sanitizedDomain}-${timestamp}${ext}` as Platform.DevToolsPath.RawPathString;
const text = await blob.text();
void Workspace.FileManager.FileManager.instance().save(basename, text, true /* forceSaveAs */);
}
// This implements the interface ReportUIFeatures from lighthouse
// which follows a different naming convention.
// eslint-disable-next-line rulesdir/no_underscored_properties, @typescript-eslint/naming-convention
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;
}
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 {
return this._dom.document();
}
resetUIState(): void {
this._resetUIState();
}
}