UNPKG

chrome-devtools-frontend

Version:
464 lines (389 loc) • 17.8 kB
// Copyright 2016 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 Common from '../common/common.js'; import * as Emulation from '../emulation/emulation.js'; // eslint-disable-line no-unused-vars import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {Events, LighthouseController} from './LighthouseController.js'; import {ProtocolService} from './LighthouseProtocolService.js'; import * as ReportRenderer from './LighthouseReporterTypes.js'; // eslint-disable-line no-unused-vars import {LighthouseReportRenderer, LighthouseReportUIFeatures} from './LighthouseReportRenderer.js'; import {Item, ReportSelector} from './LighthouseReportSelector.js'; import {StartView} from './LighthouseStartView.js'; import {StatusView} from './LighthouseStatusView.js'; export const UIStrings = { /** *@description Text that appears when user drag and drop something (for example, a file) in Lighthouse Panel */ dropLighthouseJsonHere: 'Drop `Lighthouse` JSON here', /** *@description Tooltip text that appears when hovering over the largeicon add button in the Lighthouse Panel */ performAnAudit: 'Perform an audit…', /** *@description Text to clear everything */ clearAll: 'Clear all', /** *@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in start view of the audits panel */ lighthouseSettings: '`Lighthouse` settings', /** *@description Status header in the Lighthouse panel */ printing: 'Printing', /** *@description Status text in the Lighthouse panel */ thePrintPopupWindowIsOpenPlease: 'The print popup window is open. Please close it to continue.', /** *@description Text in Lighthouse Panel */ cancelling: 'Cancelling', }; const str_ = i18n.i18n.registerUIStrings('lighthouse/LighthousePanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let lighthousePanelInstace: LighthousePanel; export class LighthousePanel extends UI.Panel.Panel { _protocolService: ProtocolService; _controller: LighthouseController; _startView: StartView; _statusView: StatusView; _warningText: null; _unauditableExplanation: null; _cachedRenderedReports: Map<ReportRenderer.ReportJSON, HTMLElement>; _dropTarget: UI.DropTarget.DropTarget; _auditResultsElement: HTMLElement; _clearButton!: UI.Toolbar.ToolbarButton; _newButton!: UI.Toolbar.ToolbarButton; _reportSelector!: ReportSelector; _settingsPane!: UI.Widget.Widget; _rightToolbar!: UI.Toolbar.Toolbar; _showSettingsPaneSetting!: Common.Settings.Setting<boolean>; _stateBefore?: { emulation: {enabled: boolean, outlineEnabled: boolean, toolbarControlsEnabled: boolean}, network: {conditions: SDK.NetworkManager.Conditions}, }; _isLHAttached?: boolean; private constructor() { super('lighthouse'); this.registerRequiredCSS('third_party/lighthouse/report-assets/report.css', {enableLegacyPatching: false}); this.registerRequiredCSS('lighthouse/lighthousePanel.css', {enableLegacyPatching: false}); this._protocolService = new ProtocolService(); this._controller = new LighthouseController(this._protocolService); this._startView = new StartView(this._controller); this._statusView = new StatusView(this._controller); this._warningText = null; this._unauditableExplanation = null; this._cachedRenderedReports = new Map(); this._dropTarget = new UI.DropTarget.DropTarget( this.contentElement, [UI.DropTarget.Type.File], i18nString(UIStrings.dropLighthouseJsonHere), this._handleDrop.bind(this)); this._controller.addEventListener(Events.PageAuditabilityChanged, this._refreshStartAuditUI.bind(this)); this._controller.addEventListener(Events.PageWarningsChanged, this._refreshWarningsUI.bind(this)); this._controller.addEventListener(Events.AuditProgressChanged, this._refreshStatusUI.bind(this)); this._controller.addEventListener(Events.RequestLighthouseStart, event => { this._startLighthouse(event); }); this._controller.addEventListener(Events.RequestLighthouseCancel, _event => { this._cancelLighthouse(); }); this._renderToolbar(); this._auditResultsElement = this.contentElement.createChild('div', 'lighthouse-results-container'); this._renderStartView(); this._controller.recomputePageAuditability(); } static instance(opts = {forceNew: null}): LighthousePanel { const {forceNew} = opts; if (!lighthousePanelInstace || forceNew) { lighthousePanelInstace = new LighthousePanel(); } return lighthousePanelInstace; } static getEvents(): typeof Events { return Events; } _refreshWarningsUI(evt: Common.EventTarget.EventTargetEvent): void { // PageWarningsChanged fires multiple times during an audit, which we want to ignore. if (this._isLHAttached) { return; } this._warningText = evt.data.warning; this._startView.setWarningText(evt.data.warning); } _refreshStartAuditUI(evt: Common.EventTarget.EventTargetEvent): void { // PageAuditabilityChanged fires multiple times during an audit, which we want to ignore. if (this._isLHAttached) { return; } this._unauditableExplanation = evt.data.helpText; this._startView.setUnauditableExplanation(evt.data.helpText); this._startView.setStartButtonEnabled(!evt.data.helpText); } _refreshStatusUI(evt: Common.EventTarget.EventTargetEvent): void { this._statusView.updateStatus(evt.data.message); } _refreshToolbarUI(): void { this._clearButton.setEnabled(this._reportSelector.hasItems()); } _clearAll(): void { this._reportSelector.clearAll(); this._renderStartView(); this._refreshToolbarUI(); } _renderToolbar(): void { const lighthouseToolbarContainer = this.element.createChild('div', 'lighthouse-toolbar-container'); const toolbar = new UI.Toolbar.Toolbar('', lighthouseToolbarContainer); this._newButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.performAnAudit), 'largeicon-add'); toolbar.appendToolbarItem(this._newButton); this._newButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._renderStartView.bind(this)); toolbar.appendSeparator(); this._reportSelector = new ReportSelector(() => this._renderStartView()); toolbar.appendToolbarItem(this._reportSelector.comboBox()); this._clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'largeicon-clear'); toolbar.appendToolbarItem(this._clearButton); this._clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clearAll.bind(this)); this._settingsPane = new UI.Widget.HBox(); this._settingsPane.show(this.contentElement); this._settingsPane.element.classList.add('lighthouse-settings-pane'); this._settingsPane.element.appendChild(this._startView.settingsToolbar().element); this._showSettingsPaneSetting = Common.Settings.Settings.instance().createSetting('lighthouseShowSettingsToolbar', false); this._rightToolbar = new UI.Toolbar.Toolbar('', lighthouseToolbarContainer); this._rightToolbar.appendSeparator(); this._rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingToggle( this._showSettingsPaneSetting, 'largeicon-settings-gear', i18nString(UIStrings.lighthouseSettings))); this._showSettingsPaneSetting.addChangeListener(this._updateSettingsPaneVisibility.bind(this)); this._updateSettingsPaneVisibility(); this._refreshToolbarUI(); } _updateSettingsPaneVisibility(): void { this._settingsPane.element.classList.toggle('hidden', !this._showSettingsPaneSetting.get()); } _toggleSettingsDisplay(show: boolean): void { this._rightToolbar.element.classList.toggle('hidden', !show); this._settingsPane.element.classList.toggle('hidden', !show); this._updateSettingsPaneVisibility(); } _renderStartView(): void { this._auditResultsElement.removeChildren(); this._statusView.hide(); this._reportSelector.selectNewReport(); this.contentElement.classList.toggle('in-progress', false); this._startView.show(this.contentElement); this._toggleSettingsDisplay(true); this._startView.setUnauditableExplanation(this._unauditableExplanation); this._startView.setStartButtonEnabled(!this._unauditableExplanation); if (!this._unauditableExplanation) { this._startView.focusStartButton(); } this._startView.setWarningText(this._warningText); this._newButton.setEnabled(false); this._refreshToolbarUI(); this.setDefaultFocusedChild(this._startView); } _renderStatusView(inspectedURL: string): void { this.contentElement.classList.toggle('in-progress', true); this._statusView.setInspectedURL(inspectedURL); this._statusView.show(this.contentElement); } _beforePrint(): void { this._statusView.show(this.contentElement); this._statusView.toggleCancelButton(false); this._statusView.renderText(i18nString(UIStrings.printing), i18nString(UIStrings.thePrintPopupWindowIsOpenPlease)); } _afterPrint(): void { this._statusView.hide(); this._statusView.toggleCancelButton(true); } _renderReport(lighthouseResult: ReportRenderer.ReportJSON, artifacts?: ReportRenderer.RunnerResultArtifacts): void { this._toggleSettingsDisplay(false); this.contentElement.classList.toggle('in-progress', false); this._startView.hideWidget(); this._statusView.hide(); this._auditResultsElement.removeChildren(); this._newButton.setEnabled(true); this._refreshToolbarUI(); const cachedRenderedReport = this._cachedRenderedReports.get(lighthouseResult); if (cachedRenderedReport) { this._auditResultsElement.appendChild(cachedRenderedReport); return; } const reportContainer = this._auditResultsElement.createChild('div', 'lh-vars lh-root lh-devtools'); const dom = new DOM(this._auditResultsElement.ownerDocument as Document); const renderer = new LighthouseReportRenderer(dom) as ReportRenderer.ReportRenderer; const templatesHTML = Root.Runtime.cachedResources.get('third_party/lighthouse/report-assets/templates.html'); if (!templatesHTML) { return; } const templatesDOM = new DOMParser().parseFromString(templatesHTML, 'text/html'); if (!templatesDOM) { return; } renderer.setTemplateContext(templatesDOM); const el = renderer.renderReport(lighthouseResult, reportContainer); LighthouseReportRenderer.addViewTraceButton(el, artifacts); // Linkifying requires the target be loaded. Do not block the report // from rendering, as this is just an embellishment and the main target // could take awhile to load. this._waitForMainTargetLoad().then(() => { LighthouseReportRenderer.linkifyNodeDetails(el); LighthouseReportRenderer.linkifySourceLocationDetails(el); }); LighthouseReportRenderer.handleDarkMode(el); // eslint-disable-next-line @typescript-eslint/no-explicit-any const features = new LighthouseReportUIFeatures(dom) as any; features.setBeforePrint(this._beforePrint.bind(this)); features.setAfterPrint(this._afterPrint.bind(this)); features.setTemplateContext(templatesDOM); features.initFeatures(lighthouseResult); this._cachedRenderedReports.set(lighthouseResult, reportContainer); } async _waitForMainTargetLoad(): Promise<void> { const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); if (!mainTarget) { return; } const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel); if (!resourceTreeModel) { return; } return resourceTreeModel.once(SDK.ResourceTreeModel.Events.Load); } _buildReportUI(lighthouseResult: ReportRenderer.ReportJSON, artifacts?: ReportRenderer.RunnerResultArtifacts): void { if (lighthouseResult === null) { return; } const optionElement = new Item( lighthouseResult, () => this._renderReport(lighthouseResult, artifacts), this._renderStartView.bind(this)); this._reportSelector.prepend(optionElement); this._refreshToolbarUI(); this._renderReport(lighthouseResult); } _handleDrop(dataTransfer: DataTransfer): void { const items = dataTransfer.items; if (!items.length) { return; } const item = items[0]; if (item.kind === 'file') { const entry = items[0].webkitGetAsEntry(); if (!entry.isFile) { return; } entry.file((file: Blob) => { const reader = new FileReader(); reader.onload = (): void => this._loadedFromFile(reader.result as string); reader.readAsText(file); }); } } _loadedFromFile(report: string): void { const data = JSON.parse(report); if (!data['lighthouseVersion']) { return; } this._buildReportUI(data as ReportRenderer.ReportJSON); } async _startLighthouse(_event: Common.EventTarget.EventTargetEvent): Promise<void> { Host.userMetrics.actionTaken(Host.UserMetrics.Action.LighthouseStarted); try { const inspectedURL = await this._controller.getInspectedURL({force: true}); const categoryIDs = this._controller.getCategoryIDs(); const flags = this._controller.getFlags(); await this._setupEmulationAndProtocolConnection(); this._renderStatusView(inspectedURL); const lighthouseResponse = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags); if (lighthouseResponse && lighthouseResponse.fatal) { const error = new Error(lighthouseResponse.message); error.stack = lighthouseResponse.stack; throw error; } if (!lighthouseResponse) { throw new Error('Auditing failed to produce a result'); } Host.userMetrics.actionTaken(Host.UserMetrics.Action.LighthouseFinished); await this._resetEmulationAndProtocolConnection(); this._buildReportUI(lighthouseResponse.lhr, lighthouseResponse.artifacts); // Give focus to the new audit button when completed this._newButton.element.focus(); } catch (err) { await this._resetEmulationAndProtocolConnection(); if (err instanceof Error) { this._statusView.renderBugReport(err); } } } async _cancelLighthouse(): Promise<void> { this._statusView.updateStatus(i18nString(UIStrings.cancelling)); await this._resetEmulationAndProtocolConnection(); this._renderStartView(); } /** * We set the device emulation on the DevTools-side for two reasons: * 1. To workaround some odd device metrics emulation bugs like occuluding viewports * 2. To get the attractive device outline * * We also set flags.internalDisableDeviceScreenEmulation = true to let LH only apply UA emulation */ async _setupEmulationAndProtocolConnection(): Promise<void> { const flags = this._controller.getFlags(); const emulationModel = Emulation.DeviceModeModel.DeviceModeModel.instance(); this._stateBefore = { emulation: { enabled: emulationModel.enabledSetting().get(), outlineEnabled: emulationModel.deviceOutlineSetting().get(), toolbarControlsEnabled: emulationModel.toolbarControlsEnabledSetting().get(), }, network: {conditions: SDK.NetworkManager.MultitargetNetworkManager.instance().networkConditions()}, }; emulationModel.toolbarControlsEnabledSetting().set(false); if ('emulatedFormFactor' in flags && flags.emulatedFormFactor === 'desktop') { emulationModel.enabledSetting().set(false); emulationModel.emulate(Emulation.DeviceModeModel.Type.None, null, null); } else if (flags.emulatedFormFactor === 'mobile') { emulationModel.enabledSetting().set(true); emulationModel.deviceOutlineSetting().set(true); for (const device of Emulation.EmulatedDevices.EmulatedDevicesList.instance().standard()) { if (device.title === 'Moto G4') { emulationModel.emulate(Emulation.DeviceModeModel.Type.Device, device, device.modes[0], 1); } } } await this._protocolService.attach(); this._isLHAttached = true; } async _resetEmulationAndProtocolConnection(): Promise<void> { if (!this._isLHAttached) { return; } this._isLHAttached = false; await this._protocolService.detach(); if (this._stateBefore) { const emulationModel = Emulation.DeviceModeModel.DeviceModeModel.instance(); emulationModel.enabledSetting().set(this._stateBefore.emulation.enabled); emulationModel.deviceOutlineSetting().set(this._stateBefore.emulation.outlineEnabled); emulationModel.toolbarControlsEnabledSetting().set(this._stateBefore.emulation.toolbarControlsEnabled); SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions( this._stateBefore.network.conditions); delete this._stateBefore; } Emulation.InspectedPagePlaceholder.InspectedPagePlaceholder.instance().update(true); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); if (!mainTarget) { return; } const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel); if (!resourceTreeModel) { return; } // reload to reset the page state const inspectedURL = await this._controller.getInspectedURL(); await resourceTreeModel.navigate(inspectedURL); } }