mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-03 02:21:05 +08:00
This CL fixes two similar issues where focus disappears or moves to invisible objects. The first is that Performance panel timeline currently keeps tabstops while blank, creating the appearance of focus being lost after tabbing past the "Learn more" link. Hiding the flamechart widget when it has nothing to show avoids focus moving to it. In the coverage drawer, focus is lost completely when the user presses the reload and record button. To avoid this, I've made the coverage drawer check for focus when a recording begins and focus the datagrid if the drawer has focus. Video of performance working properly: https://i.imgur.com/OQKmeh9.mp4 Bug: 1055375 Change-Id: I553d48cad29611ae1828bc423f0d6ba7e7a6383e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2070680 Reviewed-by: Brian Cui <brcui@microsoft.com> Reviewed-by: Robert Paveza <Rob.Paveza@microsoft.com> Commit-Queue: Jack Lynch <jalyn@microsoft.com>
388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
// 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.
|
|
|
|
import * as Common from '../common/common.js';
|
|
import * as DataGrid from '../data_grid/data_grid.js';
|
|
import * as Formatter from '../formatter/formatter.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
|
|
import {CoverageType, URLCoverageInfo} from './CoverageModel.js'; // eslint-disable-line no-unused-vars
|
|
|
|
export class CoverageListView extends UI.Widget.VBox {
|
|
/**
|
|
* @param {function(!URLCoverageInfo):boolean} filterCallback
|
|
*/
|
|
constructor(filterCallback) {
|
|
super(true);
|
|
/** @type {!Map<!URLCoverageInfo, !GridNode>} */
|
|
this._nodeForCoverageInfo = new Map();
|
|
this._filterCallback = filterCallback;
|
|
/** @type {?RegExp} */
|
|
this._highlightRegExp = null;
|
|
this.registerRequiredCSS('coverage/coverageListView.css');
|
|
const columns = [
|
|
{id: 'url', title: Common.UIString.UIString('URL'), width: '250px', fixedWidth: false, sortable: true},
|
|
{id: 'type', title: Common.UIString.UIString('Type'), width: '45px', fixedWidth: true, sortable: true}, {
|
|
id: 'size',
|
|
title: Common.UIString.UIString('Total Bytes'),
|
|
width: '60px',
|
|
fixedWidth: true,
|
|
sortable: true,
|
|
align: DataGrid.DataGrid.Align.Right
|
|
},
|
|
{
|
|
id: 'unusedSize',
|
|
title: Common.UIString.UIString('Unused Bytes'),
|
|
width: '100px',
|
|
fixedWidth: true,
|
|
sortable: true,
|
|
align: DataGrid.DataGrid.Align.Right,
|
|
sort: DataGrid.DataGrid.Order.Descending
|
|
},
|
|
{id: 'bars', title: ls`Usage Visualization`, width: '250px', fixedWidth: false, sortable: true}
|
|
];
|
|
this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({displayName: ls`Code Coverage`, columns});
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!URLCoverageInfo>} coverageInfo
|
|
*/
|
|
update(coverageInfo) {
|
|
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._filterCallback(node._coverageInfo)) {
|
|
hadUpdates = node._refreshIfNeeded(maxSize) || hadUpdates;
|
|
}
|
|
continue;
|
|
}
|
|
node = new GridNode(entry, maxSize);
|
|
this._nodeForCoverageInfo.set(entry, node);
|
|
if (this._filterCallback(node._coverageInfo)) {
|
|
rootNode.appendChild(node);
|
|
hadUpdates = true;
|
|
}
|
|
}
|
|
if (hadUpdates) {
|
|
this._sortingChanged();
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this._nodeForCoverageInfo.clear();
|
|
this._dataGrid.rootNode().removeChildren();
|
|
}
|
|
|
|
/**
|
|
* @param {?RegExp} highlightRegExp
|
|
*/
|
|
updateFilterAndHighlight(highlightRegExp) {
|
|
this._highlightRegExp = highlightRegExp;
|
|
let hadTreeUpdates = false;
|
|
for (const node of this._nodeForCoverageInfo.values()) {
|
|
const shouldBeVisible = this._filterCallback(node._coverageInfo);
|
|
const isVisible = !!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) {
|
|
for (const [info, node] of this._nodeForCoverageInfo.entries()) {
|
|
if (info.url() === url) {
|
|
node.revealAndSelect();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_onOpenedNode() {
|
|
this._revealSourceForSelectedNode();
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_onKeyDown(event) {
|
|
if (!isEnterKey(event)) {
|
|
return;
|
|
}
|
|
event.consume(true);
|
|
this._revealSourceForSelectedNode();
|
|
}
|
|
|
|
async _revealSourceForSelectedNode() {
|
|
const node = this._dataGrid.selectedNode;
|
|
if (!node) {
|
|
return;
|
|
}
|
|
const coverageInfo = /** @type {!GridNode} */ (node)._coverageInfo;
|
|
let sourceCode = self.Workspace.workspace.uiSourceCodeForURL(coverageInfo.url());
|
|
if (!sourceCode) {
|
|
return;
|
|
}
|
|
|
|
const formatData = await Formatter.sourceFormatter.format(sourceCode);
|
|
sourceCode = formatData.formattedSourceCode;
|
|
|
|
if (this._dataGrid.selectedNode !== node) {
|
|
return;
|
|
}
|
|
Common.Revealer.reveal(sourceCode);
|
|
}
|
|
|
|
_sortingChanged() {
|
|
const columnId = this._dataGrid.sortColumnId();
|
|
if (!columnId) {
|
|
return;
|
|
}
|
|
let sortFunction;
|
|
switch (columnId) {
|
|
case 'url':
|
|
sortFunction = compareURL;
|
|
break;
|
|
case 'type':
|
|
sortFunction = compareType;
|
|
break;
|
|
case 'size':
|
|
sortFunction = compareNumericField.bind(null, 'size');
|
|
break;
|
|
case 'bars':
|
|
case 'unusedSize':
|
|
sortFunction = compareNumericField.bind(null, 'unusedSize');
|
|
break;
|
|
default:
|
|
console.assert(false, 'Unknown sort field: ' + columnId);
|
|
return;
|
|
}
|
|
|
|
this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());
|
|
|
|
/**
|
|
* @param {!DataGrid.DataGrid.DataGridNode} a
|
|
* @param {!DataGrid.DataGrid.DataGridNode} b
|
|
* @return {number}
|
|
*/
|
|
function compareURL(a, b) {
|
|
const nodeA = /** @type {!GridNode} */ (a);
|
|
const nodeB = /** @type {!GridNode} */ (b);
|
|
|
|
return nodeA._url.localeCompare(nodeB._url);
|
|
}
|
|
|
|
/**
|
|
* @param {string} fieldName
|
|
* @param {!DataGrid.DataGrid.DataGridNode} a
|
|
* @param {!DataGrid.DataGrid.DataGridNode} b
|
|
* @return {number}
|
|
*/
|
|
function compareNumericField(fieldName, a, b) {
|
|
const nodeA = /** @type {!GridNode} */ (a);
|
|
const nodeB = /** @type {!GridNode} */ (b);
|
|
|
|
return nodeA._coverageInfo[fieldName]() - nodeB._coverageInfo[fieldName]() || compareURL(a, b);
|
|
}
|
|
|
|
/**
|
|
* @param {!DataGrid.DataGrid.DataGridNode} a
|
|
* @param {!DataGrid.DataGrid.DataGridNode} b
|
|
* @return {number}
|
|
*/
|
|
function compareType(a, b) {
|
|
const nodeA = /** @type {!GridNode} */ (a);
|
|
const nodeB = /** @type {!GridNode} */ (b);
|
|
const typeA = CoverageListView._typeToString(nodeA._coverageInfo.type());
|
|
const typeB = CoverageListView._typeToString(nodeB._coverageInfo.type());
|
|
return typeA.localeCompare(typeB) || compareURL(a, b);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!CoverageType} type
|
|
*/
|
|
static _typeToString(type) {
|
|
const types = [];
|
|
if (type & CoverageType.CSS) {
|
|
types.push(Common.UIString.UIString('CSS'));
|
|
}
|
|
if (type & CoverageType.JavaScriptPerFunction) {
|
|
types.push(Common.UIString.UIString('JS (per function)'));
|
|
} else if (type & CoverageType.JavaScript) {
|
|
types.push(Common.UIString.UIString('JS (per block)'));
|
|
}
|
|
return types.join('+');
|
|
}
|
|
}
|
|
|
|
export class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode {
|
|
/**
|
|
* @param {!URLCoverageInfo} coverageInfo
|
|
* @param {number} maxSize
|
|
*/
|
|
constructor(coverageInfo, maxSize) {
|
|
super();
|
|
this._coverageInfo = coverageInfo;
|
|
/** @type {number|undefined} */
|
|
this._lastUsedSize;
|
|
this._url = coverageInfo.url();
|
|
this._maxSize = maxSize;
|
|
this._highlightDOMChanges = [];
|
|
/** @type {?RegExp} */
|
|
this._highlightRegExp = null;
|
|
}
|
|
|
|
/**
|
|
* @param {?RegExp} highlightRegExp
|
|
*/
|
|
_setHighlight(highlightRegExp) {
|
|
if (this._highlightRegExp === highlightRegExp) {
|
|
return;
|
|
}
|
|
this._highlightRegExp = highlightRegExp;
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* @param {number} maxSize
|
|
* @return {boolean}
|
|
*/
|
|
_refreshIfNeeded(maxSize) {
|
|
if (this._lastUsedSize === this._coverageInfo.usedSize() && maxSize === this._maxSize) {
|
|
return false;
|
|
}
|
|
this._lastUsedSize = this._coverageInfo.usedSize();
|
|
this._maxSize = maxSize;
|
|
this.refresh();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} columnId
|
|
* @return {!Element}
|
|
*/
|
|
createCell(columnId) {
|
|
const cell = this.createTD(columnId);
|
|
switch (columnId) {
|
|
case 'url':
|
|
cell.title = 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 = CoverageListView._typeToString(this._coverageInfo.type());
|
|
if (this._coverageInfo.type() & CoverageType.JavaScriptPerFunction) {
|
|
cell.title = ls
|
|
`JS coverage with per function granularity: Once a function was executed, the whole function is marked as covered.`;
|
|
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
|
|
cell.title = ls
|
|
`JS coverage with per block granularity: Once a block of JavaScript was executed, that block is marked as covered.`;
|
|
}
|
|
break;
|
|
case 'size':
|
|
const sizeSpan = cell.createChild('span');
|
|
sizeSpan.textContent = Number.withThousandsSeparator(this._coverageInfo.size() || 0);
|
|
const sizeAccessibleName =
|
|
(this._coverageInfo.size() === 1) ? ls`1 byte` : ls`${this._coverageInfo.size() || 0} bytes`;
|
|
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 = Number.withThousandsSeparator(unusedSize);
|
|
const unusedPercentFormatted = ls`${this._percentageString(this._coverageInfo.unusedPercentage())} %`;
|
|
unusedPercentsSpan.textContent = unusedPercentFormatted;
|
|
const unusedAccessibleName = (unusedSize === 1) ? ls`1 byte, ${unusedPercentFormatted}` :
|
|
ls`${unusedSize} bytes, ${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) {
|
|
unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
|
|
unusedPercent} %) belong to functions that have not (yet) been executed.`;
|
|
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
|
|
unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
|
|
unusedPercent} %) belong to blocks of JavaScript that have not (yet) been executed.`;
|
|
}
|
|
}
|
|
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) {
|
|
usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
|
|
usedPercent} %) belong to functions that have executed at least once.`;
|
|
} else if (this._coverageInfo.type() & CoverageType.JavaScript) {
|
|
usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
|
|
usedPercent} %) belong to blocks of JavaScript that have executed at least once.`;
|
|
}
|
|
}
|
|
this.setCellAccessibleName(
|
|
ls`${unusedPercent} % of file unused, ${usedPercent} % of file used`, cell, columnId);
|
|
}
|
|
return cell;
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
* @return {string}
|
|
*/
|
|
_percentageString(value) {
|
|
return value.toFixed(1);
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {string} textContent
|
|
*/
|
|
_highlight(element, textContent) {
|
|
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');
|
|
}
|
|
}
|