// 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 * 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 DataGrid from '../../ui/legacy/components/data_grid/data_grid.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'; const {ifExpanded} = DataGrid; 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; onExpand: () => void; onCollapse: () => void; highlightRegExp: RegExp|null; expandedUrls: Set; } 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` ${i18nString(UIStrings.url)} ${i18nString(UIStrings.type)} ${i18nString(UIStrings.totalBytes)} ${ i18nString(UIStrings.unusedBytes)} ${i18nString(UIStrings.usageVisualization)} ${repeat(input.items, info => info.url, info => renderItem(info, input))} `}> `, target); // clang-format on }; export class CoverageListView extends UI.Widget.VBox { #highlightRegExp: RegExp|null; #coverageInfo: CoverageListItem[] = []; #selectedUrl: Platform.DevToolsPath.UrlString|null = null; #maxSize = 0; #expandedUrls = new Set(); #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, expandedUrls: this.#expandedUrls, onOpen: (url: Platform.DevToolsPath.UrlString) => { this.selectedUrl = url; }, onExpand: () => { this.requestUpdate(); }, onCollapse: () => { this.requestUpdate(); }, 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` input.onOpen(info.url)} @expand=${() => input.onExpand()} @collapse=${() => input.onCollapse()}> ${coverageTypeToString(info.type)} ${formatBytes(info.size)} ${formatBytes(info.unusedSize)} ${formatPercent(info.unusedPercentage)}
${info.unusedSize > 0 ? html`
` : nothing} ${info.usedSize > 0 ? html`
` : nothing}
${info.sources.length > 0 ? html` ${ifExpanded(() => html`${repeat(info.sources, source => source.url, source => renderItem(source, input))}`)}
` : nothing} `; // clang-format on }