UNPKG

chrome-devtools-frontend

Version:
661 lines (595 loc) • 25.4 kB
// Copyright (c) 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 Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as SourceFrame from '../source_frame/source_frame.js'; import * as UI from '../ui/ui.js'; import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars import {CoverageDecorationManager, decoratorType} from './CoverageDecorationManager.js'; import {CoverageListView} from './CoverageListView.js'; import {CoverageInfo, CoverageModel, CoverageType, Events, URLCoverageInfo} from './CoverageModel.js'; // eslint-disable-line no-unused-vars export const UIStrings = { /** *@description Tooltip in Coverage List View of the Coverage tab for selecting JavaScript coverage mode */ chooseCoverageGranularityPer: 'Choose coverage granularity: Per function has low overhead, per block has significant overhead.', /** *@description Text in Coverage List View of the Coverage tab */ perFunction: 'Per function', /** *@description Text in Coverage List View of the Coverage tab */ perBlock: 'Per block', /** *@description Text to clear everything */ clearAll: 'Clear all', /** *@description Tooltip text that appears when hovering over the largeicon download button in the Coverage View of the Coverage tab */ export: 'Export...', /** *@description Text in Coverage View of the Coverage tab */ urlFilter: 'URL filter', /** *@description Label for the type filter in the Converage Panel */ filterCoverageByType: 'Filter coverage by type', /** *@description Text for everything */ all: 'All', /** *@description Text that appears on a button for the css resource type filter. */ css: 'CSS', /** *@description Text in Timeline Tree View of the Performance panel */ javascript: 'JavaScript', /** *@description Tooltip text that appears on the setting when hovering over it in Coverage View of the Coverage tab */ includeExtensionContentScripts: 'Include extension content scripts', /** *@description Title for a type of source files */ contentScripts: 'Content scripts', /** *@description Message in Coverage View of the Coverage tab *@example {record button icon} PH1 */ clickTheReloadButtonSToReloadAnd: 'Click the reload button {PH1} to reload and start capturing coverage.', /** *@description Message in Coverage View of the Coverage tab *@example {record button icon} PH1 */ clickTheRecordButtonSToStart: 'Click the record button {PH1} to start capturing coverage.', /** *@description Footer message in Coverage View of the Coverage tab *@example {300k used, 600k unused} PH1 *@example {500k used, 800k unused} PH2 */ filteredSTotalS: 'Filtered: {PH1} Total: {PH2}', /** *@description Footer message in Coverage View of the Coverage tab *@example {1.5 MB} PH1 *@example {2.1 MB} PH2 *@example {71%} PH3 *@example {29%} PH4 */ sOfSSUsedSoFarSUnused: '{PH1} of {PH2} ({PH3}%) used so far, {PH4} unused.', }; const str_ = i18n.i18n.registerUIStrings('coverage/CoverageView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let coverageViewInstance: CoverageView; export class CoverageView extends UI.Widget.VBox { _model: CoverageModel|null; _decorationManager: CoverageDecorationManager|null; _resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null; _coverageTypeComboBox: UI.Toolbar.ToolbarComboBox; _coverageTypeComboBoxSetting: Common.Settings.Setting<number>; _toggleRecordAction: UI.ActionRegistration.Action; _toggleRecordButton: UI.Toolbar.ToolbarButton; _inlineReloadButton: Element|null; _startWithReloadButton: UI.Toolbar.ToolbarButton|undefined; _clearButton: UI.Toolbar.ToolbarButton; _saveButton: UI.Toolbar.ToolbarButton; _textFilterRegExp: RegExp|null; _filterInput: UI.Toolbar.ToolbarInput; _typeFilterValue: number|null; _filterByTypeComboBox: UI.Toolbar.ToolbarComboBox; _showContentScriptsSetting: Common.Settings.Setting<boolean>; _contentScriptsCheckbox: UI.Toolbar.ToolbarSettingCheckbox; _coverageResultsElement: HTMLElement; _landingPage: UI.Widget.VBox; _listView: CoverageListView; _statusToolbarElement: HTMLElement; _statusMessageElement: HTMLElement; private constructor() { super(true); this._model = null; this._decorationManager = null; this._resourceTreeModel = null; this.registerRequiredCSS('coverage/coverageView.css', {enableLegacyPatching: true}); const toolbarContainer = this.contentElement.createChild('div', 'coverage-toolbar-container'); const toolbar = new UI.Toolbar.Toolbar('coverage-toolbar', toolbarContainer); this._coverageTypeComboBox = new UI.Toolbar.ToolbarComboBox( this._onCoverageTypeComboBoxSelectionChanged.bind(this), i18nString(UIStrings.chooseCoverageGranularityPer)); const coverageTypes = [ { label: i18nString(UIStrings.perFunction), value: CoverageType.JavaScript | CoverageType.JavaScriptPerFunction, }, { label: i18nString(UIStrings.perBlock), value: CoverageType.JavaScript, }, ]; for (const type of coverageTypes) { this._coverageTypeComboBox.addOption(this._coverageTypeComboBox.createOption(type.label, `${type.value}`)); } this._coverageTypeComboBoxSetting = Common.Settings.Settings.instance().createSetting('coverageViewCoverageType', 0); this._coverageTypeComboBox.setSelectedIndex(this._coverageTypeComboBoxSetting.get()); this._coverageTypeComboBox.setEnabled(true); toolbar.appendToolbarItem(this._coverageTypeComboBox); this._toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().action('coverage.toggle-recording') as UI.ActionRegistration.Action; this._toggleRecordButton = UI.Toolbar.Toolbar.createActionButton(this._toggleRecordAction); toolbar.appendToolbarItem(this._toggleRecordButton); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); const mainTargetSupportsRecordOnReload = mainTarget && mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel); this._inlineReloadButton = null; if (mainTargetSupportsRecordOnReload) { const startWithReloadAction = UI.ActionRegistry.ActionRegistry.instance().action('coverage.start-with-reload') as UI.ActionRegistration.Action; this._startWithReloadButton = UI.Toolbar.Toolbar.createActionButton(startWithReloadAction); toolbar.appendToolbarItem(this._startWithReloadButton); this._toggleRecordButton.setEnabled(false); this._toggleRecordButton.setVisible(false); } this._clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'largeicon-clear'); this._clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clear.bind(this)); toolbar.appendToolbarItem(this._clearButton); toolbar.appendSeparator(); this._saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.export), 'largeicon-download'); this._saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, _event => { this._exportReport(); }); toolbar.appendToolbarItem(this._saveButton); this._saveButton.setEnabled(false); this._textFilterRegExp = null; toolbar.appendSeparator(); this._filterInput = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.urlFilter), '', 0.4, 1); this._filterInput.setEnabled(false); this._filterInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, this._onFilterChanged, this); toolbar.appendToolbarItem(this._filterInput); toolbar.appendSeparator(); this._typeFilterValue = null; this._filterByTypeComboBox = new UI.Toolbar.ToolbarComboBox( this._onFilterByTypeChanged.bind(this), i18nString(UIStrings.filterCoverageByType)); const options = [ { label: i18nString(UIStrings.all), value: '', }, { label: i18nString(UIStrings.css), value: CoverageType.CSS, }, { label: i18nString(UIStrings.javascript), value: CoverageType.JavaScript | CoverageType.JavaScriptPerFunction, }, ]; for (const option of options) { this._filterByTypeComboBox.addOption(this._filterByTypeComboBox.createOption(option.label, `${option.value}`)); } this._filterByTypeComboBox.setSelectedIndex(0); this._filterByTypeComboBox.setEnabled(false); toolbar.appendToolbarItem(this._filterByTypeComboBox); toolbar.appendSeparator(); this._showContentScriptsSetting = Common.Settings.Settings.instance().createSetting('showContentScripts', false); this._showContentScriptsSetting.addChangeListener(this._onFilterChanged, this); this._contentScriptsCheckbox = new UI.Toolbar.ToolbarSettingCheckbox( this._showContentScriptsSetting, i18nString(UIStrings.includeExtensionContentScripts), i18nString(UIStrings.contentScripts)); this._contentScriptsCheckbox.setEnabled(false); toolbar.appendToolbarItem(this._contentScriptsCheckbox); this._coverageResultsElement = this.contentElement.createChild('div', 'coverage-results'); this._landingPage = this._buildLandingPage(); this._listView = new CoverageListView(this._isVisible.bind(this, false)); this._statusToolbarElement = this.contentElement.createChild('div', 'coverage-toolbar-summary'); this._statusMessageElement = this._statusToolbarElement.createChild('div', 'coverage-message'); this._landingPage.show(this._coverageResultsElement); } static instance(): CoverageView { if (!coverageViewInstance) { coverageViewInstance = new CoverageView(); } return coverageViewInstance; } _buildLandingPage(): UI.Widget.VBox { const widget = new UI.Widget.VBox(); let message; if (this._startWithReloadButton) { this._inlineReloadButton = UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButtonForId('coverage.start-with-reload')); message = i18n.i18n.getFormatLocalizedString( str_, UIStrings.clickTheReloadButtonSToReloadAnd, {PH1: this._inlineReloadButton}); } else { const recordButton = UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButton(this._toggleRecordAction)); message = i18n.i18n.getFormatLocalizedString(str_, UIStrings.clickTheRecordButtonSToStart, {PH1: recordButton}); } message.classList.add('message'); widget.contentElement.appendChild(message); widget.element.classList.add('landing-page'); return widget; } _clear(): void { if (this._model) { this._model.reset(); } this._reset(); } _reset(): void { if (this._decorationManager) { this._decorationManager.dispose(); this._decorationManager = null; } this._listView.reset(); this._listView.detach(); this._landingPage.show(this._coverageResultsElement); this._statusMessageElement.textContent = ''; this._filterInput.setEnabled(false); this._filterByTypeComboBox.setEnabled(false); this._contentScriptsCheckbox.setEnabled(false); this._saveButton.setEnabled(false); } _toggleRecording(): void { const enable = !this._toggleRecordAction.toggled(); if (enable) { this._startRecording({reload: false, jsCoveragePerBlock: this.isBlockCoverageSelected()}); } else { this.stopRecording(); } } isBlockCoverageSelected(): boolean { const option = this._coverageTypeComboBox.selectedOption(); const coverageType = Number(option ? option.value : Number.NaN); // Check that Coverage.CoverageType.JavaScriptPerFunction is not present. return coverageType === CoverageType.JavaScript; } _selectCoverageType(jsCoveragePerBlock: boolean): void { const selectedIndex = jsCoveragePerBlock ? 1 : 0; this._coverageTypeComboBox.setSelectedIndex(selectedIndex); } _onCoverageTypeComboBoxSelectionChanged(): void { this._coverageTypeComboBoxSetting.set(this._coverageTypeComboBox.selectedIndex()); } async ensureRecordingStarted(): Promise<void> { const enabled = this._toggleRecordAction.toggled(); if (enabled) { await this.stopRecording(); } await this._startRecording({reload: false, jsCoveragePerBlock: false}); } async _startRecording(options: {reload: (boolean|undefined), jsCoveragePerBlock: (boolean|undefined)}| null): Promise<void> { let hadFocus, reloadButtonFocused; if ((this._startWithReloadButton && this._startWithReloadButton.element.hasFocus()) || (this._inlineReloadButton && this._inlineReloadButton.hasFocus())) { reloadButtonFocused = true; } else if (this.hasFocus()) { hadFocus = true; } this._reset(); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); if (!mainTarget) { return; } const {reload, jsCoveragePerBlock} = {reload: false, jsCoveragePerBlock: false, ...options}; if (!this._model || reload) { this._model = mainTarget.model(CoverageModel); } if (!this._model) { return; } Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageStarted); if (jsCoveragePerBlock) { Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageStartedPerBlock); } const success = await this._model.start(Boolean(jsCoveragePerBlock)); if (!success) { return; } this._selectCoverageType(Boolean(jsCoveragePerBlock)); this._model.addEventListener(Events.CoverageUpdated, this._onCoverageDataReceived, this); this._resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel) as SDK.ResourceTreeModel.ResourceTreeModel | null; if (this._resourceTreeModel) { this._resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.MainFrameNavigated, this._onMainFrameNavigated, this); } this._decorationManager = new CoverageDecorationManager(this._model as CoverageModel); this._toggleRecordAction.setToggled(true); this._clearButton.setEnabled(false); if (this._startWithReloadButton) { this._startWithReloadButton.setEnabled(false); this._startWithReloadButton.setVisible(false); this._toggleRecordButton.setEnabled(true); this._toggleRecordButton.setVisible(true); if (reloadButtonFocused) { this._toggleRecordButton.focus(); } } this._coverageTypeComboBox.setEnabled(false); this._filterInput.setEnabled(true); this._filterByTypeComboBox.setEnabled(true); this._contentScriptsCheckbox.setEnabled(true); if (this._landingPage.isShowing()) { this._landingPage.detach(); } this._listView.show(this._coverageResultsElement); if (hadFocus && !reloadButtonFocused) { this._listView.focus(); } if (reload && this._resourceTreeModel) { this._resourceTreeModel.reloadPage(); } else { this._model.startPolling(); } } _onCoverageDataReceived(event: Common.EventTarget.EventTargetEvent): void { const data = event.data as CoverageInfo[]; this._updateViews(data); } async stopRecording(): Promise<void> { if (this._resourceTreeModel) { this._resourceTreeModel.removeEventListener( SDK.ResourceTreeModel.Events.MainFrameNavigated, this._onMainFrameNavigated, this); this._resourceTreeModel = null; } if (this.hasFocus()) { this._listView.focus(); } // Stopping the model triggers one last poll to get the final data. if (this._model) { await this._model.stop(); this._model.removeEventListener(Events.CoverageUpdated, this._onCoverageDataReceived, this); } this._toggleRecordAction.setToggled(false); this._coverageTypeComboBox.setEnabled(true); if (this._startWithReloadButton) { this._startWithReloadButton.setEnabled(true); this._startWithReloadButton.setVisible(true); this._toggleRecordButton.setEnabled(false); this._toggleRecordButton.setVisible(false); } this._clearButton.setEnabled(true); } processBacklog(): void { this._model && this._model.processJSBacklog(); } _onMainFrameNavigated(): void { this._model && this._model.reset(); this._decorationManager && this._decorationManager.reset(); this._listView.reset(); this._model && this._model.startPolling(); } _updateViews(updatedEntries: CoverageInfo[]): void { this._updateStats(); this._listView.update(this._model && this._model.entries() || []); this._saveButton.setEnabled(this._model !== null && this._model.entries().length > 0); this._decorationManager && this._decorationManager.update(updatedEntries); } _updateStats(): void { const all = {total: 0, unused: 0}; const filtered = {total: 0, unused: 0}; let filterApplied = false; if (this._model) { for (const info of this._model.entries()) { all.total += info.size(); all.unused += info.unusedSize(); if (this._isVisible(false, info)) { filtered.total += info.size(); filtered.unused += info.unusedSize(); } else { filterApplied = true; } } } this._statusMessageElement.textContent = filterApplied ? i18nString(UIStrings.filteredSTotalS, {PH1: formatStat(filtered), PH2: formatStat(all)}) : formatStat(all); function formatStat({total, unused}: {total: number, unused: number}): string { const used = total - unused; const percentUsed = total ? Math.round(100 * used / total) : 0; return i18nString(UIStrings.sOfSSUsedSoFarSUnused, { PH1: Platform.NumberUtilities.bytesToString(used), PH2: Platform.NumberUtilities.bytesToString(total), PH3: percentUsed, PH4: Platform.NumberUtilities.bytesToString(unused), }); } } _onFilterChanged(): void { if (!this._listView) { return; } const text = this._filterInput.value(); this._textFilterRegExp = text ? createPlainTextSearchRegex(text, 'i') : null; this._listView.updateFilterAndHighlight(this._textFilterRegExp); this._updateStats(); } _onFilterByTypeChanged(): void { if (!this._listView) { return; } Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageReportFiltered); const option = this._filterByTypeComboBox.selectedOption(); const type = option && option.value; this._typeFilterValue = parseInt(type || '', 10) || null; this._listView.updateFilterAndHighlight(this._textFilterRegExp); this._updateStats(); } _isVisible(ignoreTextFilter: boolean, coverageInfo: URLCoverageInfo): boolean { const url = coverageInfo.url(); if (url.startsWith(CoverageView.EXTENSION_BINDINGS_URL_PREFIX)) { return false; } if (coverageInfo.isContentScript() && !this._showContentScriptsSetting.get()) { return false; } if (this._typeFilterValue && !(coverageInfo.type() & this._typeFilterValue)) { return false; } return ignoreTextFilter || !this._textFilterRegExp || this._textFilterRegExp.test(url); } async _exportReport(): Promise<void> { const fos = new Bindings.FileUtils.FileOutputStream(); const fileName = `Coverage-${Platform.DateUtilities.toISO8601Compact(new Date())}.json`; const accepted = await fos.open(fileName); if (!accepted) { return; } this._model && this._model.exportReport(fos); } selectCoverageItemByUrl(url: string): void { this._listView.selectByUrl(url); } static readonly EXTENSION_BINDINGS_URL_PREFIX = 'extensions::'; } let actionDelegateInstance: ActionDelegate; export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { handleAction(context: UI.Context.Context, actionId: string): boolean { const coverageViewId = 'coverage'; UI.ViewManager.ViewManager.instance() .showView(coverageViewId, /** userGesture= */ false, /** omitFocus= */ true) .then(() => { const view = UI.ViewManager.ViewManager.instance().view(coverageViewId); return view && view.widget(); }) .then(widget => this._innerHandleAction(widget as CoverageView, actionId)); return true; } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ActionDelegate { const {forceNew} = opts; if (!actionDelegateInstance || forceNew) { actionDelegateInstance = new ActionDelegate(); } return actionDelegateInstance; } _innerHandleAction(coverageView: CoverageView, actionId: string): void { switch (actionId) { case 'coverage.toggle-recording': coverageView._toggleRecording(); break; case 'coverage.start-with-reload': coverageView._startRecording({reload: true, jsCoveragePerBlock: coverageView.isBlockCoverageSelected()}); break; default: console.assert(false, `Unknown action: ${actionId}`); } } } let lineDecoratorInstance: LineDecorator; export class LineDecorator implements SourceFrame.SourceFrame.LineDecorator { static instance({forceNew}: {forceNew: boolean} = {forceNew: false}): LineDecorator { if (!lineDecoratorInstance || forceNew) { lineDecoratorInstance = new LineDecorator(); } return lineDecoratorInstance; } _listeners: WeakMap<SourceFrame.SourcesTextEditor.SourcesTextEditor, (arg0: Common.EventTarget.EventTargetEvent) => void>; constructor() { this._listeners = new WeakMap(); } decorate( uiSourceCode: Workspace.UISourceCode.UISourceCode, textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor): void { const decorations = uiSourceCode.decorationsForType(decoratorType); if (!decorations || !decorations.size) { this._uninstallGutter(textEditor); return; } const decorationManager = decorations.values().next().value.data() as CoverageDecorationManager; decorationManager.usageByLine(uiSourceCode).then(lineUsage => { textEditor.operation(() => this._innerDecorate(uiSourceCode, textEditor, lineUsage)); }); } _innerDecorate( uiSourceCode: Workspace.UISourceCode.UISourceCode, textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor, lineUsage: (boolean|undefined)[]): void { const gutterType = LineDecorator.GUTTER_TYPE; this._uninstallGutter(textEditor); if (lineUsage.length) { this._installGutter(textEditor, uiSourceCode.url()); } for (let line = 0; line < lineUsage.length; ++line) { // Do not decorate the line if we don't have data. if (typeof lineUsage[line] !== 'boolean') { continue; } const className = lineUsage[line] ? 'text-editor-coverage-used-marker' : 'text-editor-coverage-unused-marker'; const gutterElement = document.createElement('div'); gutterElement.classList.add(className); textEditor.setGutterDecoration(line, gutterType, gutterElement); } } makeGutterClickHandler(url: string): (arg0: Common.EventTarget.EventTargetEvent) => void { function handleGutterClick(event: Common.EventTarget.EventTargetEvent): void { const eventData = event.data as SourceFrame.SourcesTextEditor.GutterClickEventData; if (eventData.gutterType !== LineDecorator.GUTTER_TYPE) { return; } const coverageViewId = 'coverage'; UI.ViewManager.ViewManager.instance() .showView(coverageViewId) .then(() => { const view = UI.ViewManager.ViewManager.instance().view(coverageViewId); return view && view.widget(); }) .then(widget => { const matchFormattedSuffix = url.match(/(.*):formatted$/); const urlWithoutFormattedSuffix = (matchFormattedSuffix && matchFormattedSuffix[1]) || url; (widget as CoverageView).selectCoverageItemByUrl(urlWithoutFormattedSuffix); }); } return handleGutterClick; } _installGutter(textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor, url: string): void { let listener = this._listeners.get(textEditor); if (!listener) { listener = this.makeGutterClickHandler(url); this._listeners.set(textEditor, listener); } textEditor.installGutter(LineDecorator.GUTTER_TYPE, false); textEditor.addEventListener(SourceFrame.SourcesTextEditor.Events.GutterClick, listener, this); } _uninstallGutter(textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor): void { textEditor.uninstallGutter(LineDecorator.GUTTER_TYPE); const listener = this._listeners.get(textEditor); if (listener) { textEditor.removeEventListener(SourceFrame.SourcesTextEditor.Events.GutterClick, listener, this); this._listeners.delete(textEditor); } } static readonly GUTTER_TYPE = 'CodeMirror-gutter-coverage'; } SourceFrame.SourceFrame.registerLineDecorator({ lineDecorator: LineDecorator.instance, decoratorType: SourceFrame.SourceFrame.DecoratorType.COVERAGE, });