Files
react-native-devtools-frontend/front_end/panels/coverage/CoverageListView.ts
T
Jack Franklin 84442c6766 [DarkMode] remove enableLegacyPatching option
The option was only allowed to be set to `false`, so it's not been
used at all, so now we can remove it, given that we've had the dark
mode legacy patching disabled without any major bugs in the past few
weeks.

Bug: chromium:1152736
Change-Id: I1e794d75f9c354a0d16b1b3975f7ebdf82b3448b
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2988303
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
2021-06-29 10:07:32 +00:00

458 lines
17 KiB
TypeScript

// Copyright (c) 2017 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 '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Formatter from '../../models/formatter/formatter.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as UI from '../../ui/legacy/legacy.js';
import type {URLCoverageInfo} from './CoverageModel.js';
import {CoverageType} from './CoverageModel.js';
const UIStrings = {
/**
*@description Text that appears on a button for the css resource type filter.
*/
css: 'CSS',
/**
*@description Text in Coverage List View of the Coverage tab
*/
jsPerFunction: 'JS (per function)',
/**
*@description Text in Coverage List View of the Coverage tab
*/
jsPerBlock: 'JS (per block)',
/**
*@description Text for web URLs
*/
url: 'URL',
/**
*@description Text that refers to some types
*/
type: 'Type',
/**
*@description Text in Coverage List View of the Coverage tab
*/
totalBytes: 'Total Bytes',
/**
*@description Text in Coverage List View of the Coverage tab
*/
unusedBytes: 'Unused Bytes',
/**
*@description Text in the Coverage List View of the Coverage Tab
*/
usageVisualization: 'Usage Visualization',
/**
*@description Data grid name for Coverage data grids
*/
codeCoverage: 'Code Coverage',
/**
*@description Cell title in Coverage List View of the Coverage tab. The coverage tool tells
*developers which functions (logical groups of lines of code) were actually run/executed. If a
*function does get run, then it is marked in the UI to indicate that it was covered.
*/
jsCoverageWithPerFunction:
'JS coverage with per function granularity: Once a function was executed, the whole function is marked as covered.',
/**
*@description Cell title in Coverage List View of the Coverage tab. The coverage tool tells
*developers which blocks (logical groups of lines of code, smaller than a function) were actually
*run/executed. If a block does get run, then it is marked in the UI to indicate that it was
*covered.
*/
jsCoverageWithPerBlock:
'JS coverage with per block granularity: Once a block of JavaScript was executed, that block is marked as covered.',
/**
*@description Accessible text for the value in bytes in memory allocation or coverage view.
*/
sBytes: '{n, plural, =1 {# byte} other {# bytes}}',
/**
*@description Message in Coverage View of the Coverage tab
*@example {12.34} PH1
*/
sPercent: '{PH1} %',
/**
*@description Accessible text for the unused bytes column in the coverage tool that describes the total unused bytes and percentage of the file unused.
*@example {88%} percentage
*/
sBytesS: '{n, plural, =1 {# byte, {percentage}} other {# bytes, {percentage}}}',
/**
*@description Tooltip text for the bar in the coverage list view of the coverage tool that illustrates the relation between used and unused bytes.
*@example {1000} PH1
*@example {12.34} PH2
*/
sBytesSBelongToFunctionsThatHave: '{PH1} bytes ({PH2} %) belong to functions that have not (yet) been executed.',
/**
*@description Tooltip text for the bar in the coverage list view of the coverage tool that illustrates the relation between used and unused bytes.
*@example {1000} PH1
*@example {12.34} PH2
*/
sBytesSBelongToBlocksOf: '{PH1} bytes ({PH2} %) belong to blocks of JavaScript that have not (yet) been executed.',
/**
*@description Message in Coverage View of the Coverage tab
*@example {1000} PH1
*@example {12.34} PH2
*/
sBytesSBelongToFunctionsThatHaveExecuted:
'{PH1} bytes ({PH2} %) belong to functions that have executed at least once.',
/**
*@description Message in Coverage View of the Coverage tab
*@example {1000} PH1
*@example {12.34} PH2
*/
sBytesSBelongToBlocksOfJavascript:
'{PH1} bytes ({PH2} %) belong to blocks of JavaScript that have executed at least once.',
/**
*@description Accessible text for the visualization column of coverage tool. Contains percentage of unused bytes to used bytes.
*@example {12.3} PH1
*@example {12.3} PH2
*/
sOfFileUnusedSOfFileUsed: '{PH1} % of file unused, {PH2} % of file used',
};
const str_ = i18n.i18n.registerUIStrings('panels/coverage/CoverageListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export function coverageTypeToString(type: CoverageType): string {
const types = [];
if (type & CoverageType.CSS) {
types.push(i18nString(UIStrings.css));
}
if (type & CoverageType.JavaScriptPerFunction) {
types.push(i18nString(UIStrings.jsPerFunction));
} else if (type & CoverageType.JavaScript) {
types.push(i18nString(UIStrings.jsPerBlock));
}
return types.join('+');
}
export class CoverageListView extends UI.Widget.VBox {
_nodeForCoverageInfo: Map<URLCoverageInfo, GridNode>;
_isVisibleFilter: (arg0: URLCoverageInfo) => boolean;
_highlightRegExp: RegExp|null;
_dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<GridNode>;
constructor(isVisibleFilter: (arg0: URLCoverageInfo) => boolean) {
super(true);
this._nodeForCoverageInfo = new Map();
this._isVisibleFilter = isVisibleFilter;
this._highlightRegExp = null;
this.registerRequiredCSS('panels/coverage/coverageListView.css');
const columns: DataGrid.DataGrid.ColumnDescriptor[] = [
{id: 'url', title: i18nString(UIStrings.url), width: '250px', fixedWidth: false, sortable: true},
{id: 'type', title: i18nString(UIStrings.type), width: '45px', fixedWidth: true, sortable: true},
{
id: 'size',
title: i18nString(UIStrings.totalBytes),
width: '60px',
fixedWidth: true,
sortable: true,
align: DataGrid.DataGrid.Align.Right,
},
{
id: 'unusedSize',
title: i18nString(UIStrings.unusedBytes),
width: '100px',
fixedWidth: true,
sortable: true,
align: DataGrid.DataGrid.Align.Right,
sort: DataGrid.DataGrid.Order.Descending,
},
{id: 'bars', title: i18nString(UIStrings.usageVisualization), width: '250px', fixedWidth: false, sortable: true},
] as DataGrid.DataGrid.ColumnDescriptor[];
this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid<GridNode>({
displayName: i18nString(UIStrings.codeCoverage),
columns,
editCallback: undefined,
refreshCallback: undefined,
deleteCallback: undefined,
});
this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
this._dataGrid.element.classList.add('flex-auto');
this._dataGrid.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.OpenedNode, this._onOpenedNode, this);
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this);
const dataGridWidget = this._dataGrid.asWidget();
dataGridWidget.show(this.contentElement);
this.setDefaultFocusedChild(dataGridWidget);
}
update(coverageInfo: URLCoverageInfo[]): void {
let hadUpdates = false;
const maxSize = coverageInfo.reduce((acc, entry) => Math.max(acc, entry.size()), 0);
const rootNode = this._dataGrid.rootNode();
for (const entry of coverageInfo) {
let node = this._nodeForCoverageInfo.get(entry);
if (node) {
if (this._isVisibleFilter(node._coverageInfo)) {
hadUpdates = node._refreshIfNeeded(maxSize) || hadUpdates;
}
continue;
}
node = new GridNode(entry, maxSize);
this._nodeForCoverageInfo.set(entry, node);
if (this._isVisibleFilter(node._coverageInfo)) {
rootNode.appendChild(node);
hadUpdates = true;
}
}
if (hadUpdates) {
this._sortingChanged();
}
}
reset(): void {
this._nodeForCoverageInfo.clear();
this._dataGrid.rootNode().removeChildren();
}
updateFilterAndHighlight(highlightRegExp: RegExp|null): void {
this._highlightRegExp = highlightRegExp;
let hadTreeUpdates = false;
for (const node of this._nodeForCoverageInfo.values()) {
const shouldBeVisible = this._isVisibleFilter(node._coverageInfo);
const isVisible = Boolean(node.parent);
if (shouldBeVisible) {
node._setHighlight(this._highlightRegExp);
}
if (shouldBeVisible === isVisible) {
continue;
}
hadTreeUpdates = true;
if (!shouldBeVisible) {
node.remove();
} else {
this._dataGrid.rootNode().appendChild(node);
}
}
if (hadTreeUpdates) {
this._sortingChanged();
}
}
selectByUrl(url: string): void {
for (const [info, node] of this._nodeForCoverageInfo.entries()) {
if (info.url() === url) {
node.revealAndSelect();
break;
}
}
}
_onOpenedNode(): void {
this._revealSourceForSelectedNode();
}
_onKeyDown(event: KeyboardEvent): void {
if (!(event.key === 'Enter')) {
return;
}
event.consume(true);
this._revealSourceForSelectedNode();
}
async _revealSourceForSelectedNode(): Promise<void> {
const node = this._dataGrid.selectedNode;
if (!node) {
return;
}
const coverageInfo = (node as GridNode)._coverageInfo;
let sourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(coverageInfo.url());
if (!sourceCode) {
return;
}
const formatData = await Formatter.SourceFormatter.SourceFormatter.instance().format(sourceCode);
sourceCode = formatData.formattedSourceCode;
if (this._dataGrid.selectedNode !== node) {
return;
}
Common.Revealer.reveal(sourceCode);
}
_sortingChanged(): void {
const columnId = this._dataGrid.sortColumnId();
if (!columnId) {
return;
}
const sortFunction = GridNode.sortFunctionForColumn(columnId) as (
(arg0: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>,
arg1: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>) => number) |
null;
if (!sortFunction) {
return;
}
this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());
}
}
export class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode<GridNode> {
_coverageInfo: URLCoverageInfo;
_lastUsedSize!: number|undefined;
_url: string;
_maxSize: number;
_highlightRegExp: RegExp|null;
constructor(coverageInfo: URLCoverageInfo, maxSize: number) {
super();
this._coverageInfo = coverageInfo;
this._url = coverageInfo.url();
this._maxSize = maxSize;
this._highlightRegExp = null;
}
_setHighlight(highlightRegExp: RegExp|null): void {
if (this._highlightRegExp === highlightRegExp) {
return;
}
this._highlightRegExp = highlightRegExp;
this.refresh();
}
_refreshIfNeeded(maxSize: number): boolean {
if (this._lastUsedSize === this._coverageInfo.usedSize() && maxSize === this._maxSize) {
return false;
}
this._lastUsedSize = this._coverageInfo.usedSize();
this._maxSize = maxSize;
this.refresh();
return true;
}
createCell(columnId: string): HTMLElement {
const cell = this.createTD(columnId);
switch (columnId) {
case 'url': {
UI.Tooltip.Tooltip.install(cell, this._url);
const outer = cell.createChild('div', 'url-outer');
const prefix = outer.createChild('div', 'url-prefix');
const suffix = outer.createChild('div', 'url-suffix');
const splitURL = /^(.*)(\/[^/]*)$/.exec(this._url);
prefix.textContent = splitURL ? splitURL[1] : this._url;
suffix.textContent = splitURL ? splitURL[2] : '';
if (this._highlightRegExp) {
this._highlight(outer, this._url);
}
this.setCellAccessibleName(this._url, cell, columnId);
break;
}
case 'type': {
cell.textContent = coverageTypeToString(this._coverageInfo.type());
if (this._coverageInfo.type() & CoverageType.JavaScriptPerFunction) {
UI.Tooltip.Tooltip.install(cell, i18nString(UIStrings.jsCoverageWithPerFunction));
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
UI.Tooltip.Tooltip.install(cell, i18nString(UIStrings.jsCoverageWithPerBlock));
}
break;
}
case 'size': {
const sizeSpan = cell.createChild('span');
sizeSpan.textContent = Platform.NumberUtilities.withThousandsSeparator(this._coverageInfo.size() || 0);
const sizeAccessibleName = i18nString(UIStrings.sBytes, {n: this._coverageInfo.size() || 0});
this.setCellAccessibleName(sizeAccessibleName, cell, columnId);
break;
}
case 'unusedSize': {
const unusedSize = this._coverageInfo.unusedSize() || 0;
const unusedSizeSpan = cell.createChild('span');
const unusedPercentsSpan = cell.createChild('span', 'percent-value');
unusedSizeSpan.textContent = Platform.NumberUtilities.withThousandsSeparator(unusedSize);
// TODO(l10n): Don't concatenate the % string here. Do we need to use Intl number formatter instead?
const unusedPercentFormatted =
i18nString(UIStrings.sPercent, {PH1: this._percentageString(this._coverageInfo.unusedPercentage())});
unusedPercentsSpan.textContent = unusedPercentFormatted;
const unusedAccessibleName = i18nString(UIStrings.sBytesS, {n: unusedSize, percentage: unusedPercentFormatted});
this.setCellAccessibleName(unusedAccessibleName, cell, columnId);
break;
}
case 'bars': {
const barContainer = cell.createChild('div', 'bar-container');
const unusedPercent = this._percentageString(this._coverageInfo.unusedPercentage());
const usedPercent = this._percentageString(this._coverageInfo.usedPercentage());
if (this._coverageInfo.unusedSize() > 0) {
const unusedSizeBar = barContainer.createChild('div', 'bar bar-unused-size');
unusedSizeBar.style.width = ((this._coverageInfo.unusedSize() / this._maxSize) * 100 || 0) + '%';
if (this._coverageInfo.type() & CoverageType.JavaScriptPerFunction) {
UI.Tooltip.Tooltip.install(
unusedSizeBar,
i18nString(
UIStrings.sBytesSBelongToFunctionsThatHave,
{PH1: this._coverageInfo.unusedSize(), PH2: unusedPercent}));
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
UI.Tooltip.Tooltip.install(
unusedSizeBar,
i18nString(
UIStrings.sBytesSBelongToBlocksOf, {PH1: this._coverageInfo.unusedSize(), PH2: unusedPercent}));
}
}
if (this._coverageInfo.usedSize() > 0) {
const usedSizeBar = barContainer.createChild('div', 'bar bar-used-size');
usedSizeBar.style.width = ((this._coverageInfo.usedSize() / this._maxSize) * 100 || 0) + '%';
if (this._coverageInfo.type() & CoverageType.JavaScriptPerFunction) {
UI.Tooltip.Tooltip.install(
usedSizeBar,
i18nString(
UIStrings.sBytesSBelongToFunctionsThatHaveExecuted,
{PH1: this._coverageInfo.usedSize(), PH2: usedPercent}));
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
UI.Tooltip.Tooltip.install(
usedSizeBar,
i18nString(
UIStrings.sBytesSBelongToBlocksOfJavascript,
{PH1: this._coverageInfo.usedSize(), PH2: usedPercent}));
}
}
this.setCellAccessibleName(
i18nString(UIStrings.sOfFileUnusedSOfFileUsed, {PH1: unusedPercent, PH2: usedPercent}), cell, columnId);
}
}
return cell;
}
_percentageString(value: number): string {
return value.toFixed(1);
}
_highlight(element: Element, textContent: string): void {
if (!this._highlightRegExp) {
return;
}
const matches = this._highlightRegExp.exec(textContent);
if (!matches || !matches.length) {
return;
}
const range = new TextUtils.TextRange.SourceRange(matches.index, matches[0].length);
UI.UIUtils.highlightRangesWithStyleClass(element, [range], 'filter-highlight');
}
static sortFunctionForColumn(columnId: string): ((arg0: GridNode, arg1: GridNode) => number)|null {
const compareURL = (a: GridNode, b: GridNode): number => a._url.localeCompare(b._url);
switch (columnId) {
case 'url':
return compareURL;
case 'type':
return (a: GridNode, b: GridNode): number => {
const typeA = coverageTypeToString(a._coverageInfo.type());
const typeB = coverageTypeToString(b._coverageInfo.type());
return typeA.localeCompare(typeB) || compareURL(a, b);
};
case 'size':
return (a: GridNode, b: GridNode): number =>
a._coverageInfo.size() - b._coverageInfo.size() || compareURL(a, b);
case 'bars':
case 'unusedSize':
return (a: GridNode, b: GridNode): number =>
a._coverageInfo.unusedSize() - b._coverageInfo.unusedSize() || compareURL(a, b);
default:
console.assert(false, 'Unknown sort field: ' + columnId);
return null;
}
}
}