UNPKG

chrome-devtools-frontend

Version:
333 lines (306 loc) • 13.3 kB
// Copyright 2017 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../../ui/components/highlighting/highlighting.js'; import '../../ui/legacy/components/data_grid/data_grid.js'; import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Directives, html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js'; import coverageListViewStyles from './coverageListView.css.js'; import {CoverageType} from './CoverageModel.js'; export interface CoverageListItem { url: Platform.DevToolsPath.UrlString; type: CoverageType; size: number; usedSize: number; unusedSize: number; usedPercentage: number; unusedPercentage: number; sources: CoverageListItem[]; isContentScript: boolean; generatedUrl?: Platform.DevToolsPath.UrlString; } 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 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', } as const; const str_ = i18n.i18n.registerUIStrings('panels/coverage/CoverageListView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const {styleMap, repeat} = Directives; export function coverageTypeToString(type: CoverageType): string { const types = []; if (type & CoverageType.CSS) { types.push(i18nString(UIStrings.css)); } if (type & CoverageType.JAVA_SCRIPT_PER_FUNCTION) { types.push(i18nString(UIStrings.jsPerFunction)); } else if (type & CoverageType.JAVA_SCRIPT) { types.push(i18nString(UIStrings.jsPerBlock)); } return types.join('+'); } interface ViewInput { items: CoverageListItem[]; selectedUrl: Platform.DevToolsPath.UrlString|null; maxSize: number; onOpen: (url: Platform.DevToolsPath.UrlString) => void; highlightRegExp: RegExp|null; } type View = (input: ViewInput, output: object, target: HTMLElement) => void; const formatBytes = (value: number|undefined): string => { return getBytesFormatter().format(value ?? 0); }; const formatPercent = (value: number|undefined): string => { return getPercentageFormatter().format(value ?? 0); }; export const DEFAULT_VIEW: View = (input, _output, target) => { // clang-format off render(html` <style>${coverageListViewStyles}</style> <devtools-data-grid class="flex-auto" name=${i18nString(UIStrings.codeCoverage)} striped autofocus resize="last" .template=${html` <table> <tr> <th id="url" width="250px" weight="3" sortable>${i18nString(UIStrings.url)}</th> <th id="type" width="45px" weight="1" fixed sortable>${i18nString(UIStrings.type)}</th> <th id="size" width="60px" align="right" weight="1" fixed sortable>${i18nString(UIStrings.totalBytes)}</th> <th id="unused-size" width="100px" align="right" weight="1" fixed sortable sort="descending">${ i18nString(UIStrings.unusedBytes)}</th> <th id="bars" width="250px" weight="1" sortable>${i18nString(UIStrings.usageVisualization)}</th> </tr> ${repeat(input.items, info => info.url, info => renderItem(info, input))} </table>`}> </devtools-data-grid>`, target); // clang-format on }; export class CoverageListView extends UI.Widget.VBox { #highlightRegExp: RegExp|null; #coverageInfo: CoverageListItem[] = []; #selectedUrl: Platform.DevToolsPath.UrlString|null = null; #maxSize = 0; #view: View; constructor(element?: HTMLElement, view = DEFAULT_VIEW) { super(element, {useShadowDom: true, delegatesFocus: true}); this.#view = view; this.#highlightRegExp = null; } set highlightRegExp(highlightRegExp: RegExp|null) { this.#highlightRegExp = highlightRegExp; this.requestUpdate(); } get highlightRegExp(): RegExp|null { return this.#highlightRegExp; } set coverageInfo(coverageInfo: CoverageListItem[]) { this.#coverageInfo = coverageInfo; this.#maxSize = coverageInfo.reduce((acc, entry) => Math.max(acc, entry.size), 0); this.requestUpdate(); } get coverageInfo(): CoverageListItem[] { return this.#coverageInfo; } override performUpdate(): void { const input: ViewInput = { items: this.#coverageInfo, selectedUrl: this.#selectedUrl, maxSize: this.#maxSize, onOpen: (url: Platform.DevToolsPath.UrlString) => { this.selectedUrl = url; }, highlightRegExp: this.#highlightRegExp, }; this.#view(input, {}, this.contentElement); } reset(): void { this.#coverageInfo = []; this.#maxSize = 0; this.requestUpdate(); } set selectedUrl(url: Platform.DevToolsPath.UrlString|null) { const info = this.#coverageInfo.find(info => info.url === url); if (!info) { return; } if (this.#selectedUrl !== url) { this.#selectedUrl = url as Platform.DevToolsPath.UrlString; this.requestUpdate(); } const sourceCode = url ? Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url) : null; if (!sourceCode) { return; } void Common.Revealer.reveal(sourceCode); } get selectedUrl(): Platform.DevToolsPath.UrlString|null { return this.#selectedUrl; } } let percentageFormatter: Intl.NumberFormat|null = null; function getPercentageFormatter(): Intl.NumberFormat { if (!percentageFormatter) { percentageFormatter = new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, { style: 'percent', maximumFractionDigits: 1, }); } return percentageFormatter; } let bytesFormatter: Intl.NumberFormat|null = null; function getBytesFormatter(): Intl.NumberFormat { if (!bytesFormatter) { bytesFormatter = new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale); } return bytesFormatter; } function renderItem(info: CoverageListItem, input: ViewInput): TemplateResult { function highlightRange(textContent: string): string { const matches = input.highlightRegExp?.exec(textContent); return matches?.length ? `${matches.index},${matches[0].length}` : ''; } const splitURL = /^(.*)(\/[^/]*)$/.exec(info.url); // clang-format off return html` <style>${coverageListViewStyles}</style> <tr data-url=${info.url} selected=${info.url === input.selectedUrl} @open=${() => input.onOpen(info.url)}> <td data-value=${info.url} title=${info.url} aria-label=${info.url}> <devtools-highlight ranges=${highlightRange(info.url)} class="url-outer" aria-hidden="true"> <div class="url-prefix">${splitURL ? splitURL[1] : info.url}</div> <div class="url-suffix">${splitURL ? splitURL[2] : ''}</div> </devtools-highlight> </td> <td data-value=${coverageTypeToString(info.type)} title=${info.type & CoverageType.JAVA_SCRIPT_PER_FUNCTION ? i18nString(UIStrings.jsCoverageWithPerFunction) : info.type & CoverageType.JAVA_SCRIPT ? i18nString(UIStrings.jsCoverageWithPerBlock) : ''}> ${coverageTypeToString(info.type)} </td> <td data-value=${info.size} aria-label=${i18nString(UIStrings.sBytes, {n: info.size || 0})}> <span>${formatBytes(info.size)}</span> </td> <td data-value=${info.unusedSize} aria-label=${i18nString(UIStrings.sBytesS, {n: info.unusedSize, percentage: formatPercent(info.unusedPercentage)})}> <span>${formatBytes(info.unusedSize)}</span> <span class="percent-value"> ${formatPercent(info.unusedPercentage)} </span> </td> <td data-value=${info.unusedSize} aria-label=${i18nString(UIStrings.sOfFileUnusedSOfFileUsed, {PH1: formatPercent(info.unusedPercentage), PH2: formatPercent(info.usedPercentage)})}> <div class="bar-container"> ${info.unusedSize > 0 ? html` <div class="bar bar-unused-size" title=${ info.type & CoverageType.JAVA_SCRIPT_PER_FUNCTION ? i18nString(UIStrings.sBytesSBelongToFunctionsThatHave, {PH1: info.unusedSize, PH2: formatPercent(info.unusedPercentage)}) : info.type & CoverageType.JAVA_SCRIPT ? i18nString(UIStrings.sBytesSBelongToBlocksOf, {PH1: info.unusedSize, PH2: formatPercent(info.unusedPercentage)}) : ''} style=${styleMap({width: ((info.unusedSize / input.maxSize) * 100 || 0) + '%'})}> </div>` : nothing} ${info.usedSize > 0 ? html` <div class="bar bar-used-size" title=${ info.type & CoverageType.JAVA_SCRIPT_PER_FUNCTION ? i18nString(UIStrings.sBytesSBelongToFunctionsThatHaveExecuted, {PH1: info.usedSize, PH2: formatPercent(info.usedPercentage)}) : info.type & CoverageType.JAVA_SCRIPT ? i18nString(UIStrings.sBytesSBelongToBlocksOfJavascript, {PH1: info.usedSize, PH2: formatPercent(info.usedPercentage)}) : ''} style=${styleMap({width:((info.usedSize / input.maxSize) * 100 || 0) + '%'})}> </div>` : nothing} </div> </td> ${info.sources.length > 0 ? html` <td><table> ${repeat(info.sources, source => source.url, source => renderItem(source, input))} </table></td>` : nothing} </tr>`; // clang-format on }