chrome-devtools-frontend
Version:
Chrome DevTools UI
705 lines (639 loc) • 27.5 kB
text/typescript
// Copyright 2016 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/legacy/legacy.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, i18nTemplate as unboundI18nTemplate, render, type TemplateResult} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {CoverageDecorationManager} from './CoverageDecorationManager.js';
import {type CoverageListItem, CoverageListView} from './CoverageListView.js';
import {
type CoverageInfo,
CoverageModel,
CoverageType,
Events,
SourceURLCoverageInfo,
type URLCoverageInfo,
} from './CoverageModel.js';
import coverageViewStyles from './coverageView.css.js';
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 in Coverage View of the Coverage tab
*/
filterByUrl: 'Filter by URL',
/**
* @description Label for the type filter in the Coverage 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
*/
noCoverageData: 'No coverage data',
/**
* @description Message in Coverage View of the Coverage tab
*/
reloadPage: 'Reload page',
/**
* @description Message in Coverage View of the Coverage tab
*/
startRecording: 'Start recording',
/**
* @description Message in Coverage View of the Coverage tab
* @example {Reload page} PH1
*/
clickTheReloadButtonSToReloadAnd: 'Click the "{PH1}" button to reload and start capturing coverage.',
/**
* @description Message in Coverage View of the Coverage tab
* @example {Start recording} PH1
*/
clickTheRecordButtonSToStart: 'Click the "{PH1}" button to start capturing coverage.',
/**
* @description Message in the Coverage View explaining that DevTools could not capture coverage.
*/
bfcacheNoCapture: 'Could not capture coverage info because the page was served from the back/forward cache.',
/**
* @description Message in the Coverage View explaining that DevTools could not capture coverage.
*/
activationNoCapture: 'Could not capture coverage info because the page was prerendered in the background.',
/**
* @description Message in the Coverage View prompting the user to reload the page.
* @example {reload button icon} PH1
*/
reloadPrompt: 'Click the reload button {PH1} to reload and get 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.',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/coverage/CoverageView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nTemplate = unboundI18nTemplate.bind(undefined, str_);
const {ref} = Directives;
const {bindToAction, bindToSetting} = UI.UIUtils;
const {widgetConfig} = UI.Widget;
let coverageViewInstance: CoverageView|undefined;
export interface CoverageViewInput {
coverageType: number;
recording: boolean;
supportsRecordOnReload: boolean;
textFilter: RegExp|null;
typeFilter: number|null;
showContentScriptsSetting: Common.Settings.Setting<boolean>;
needsReload: 'bfcache-page'|'prerender-page'|null;
coverageInfo: CoverageListItem[]|null;
selectedUrl: Platform.DevToolsPath.UrlString|null;
statusMessage: string;
onCoverageTypeChanged: (newValue: number) => void;
onFilterChanged: (e: string) => void;
onTypeFilterChanged: (newValue: number) => void;
}
export interface CoverageViewOutput {
focusResults: () => void;
}
export type View = (input: CoverageViewInput, output: CoverageViewOutput, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
// clang-format off
render(html`
<style>${coverageViewStyles}</style>
<div class="coverage-toolbar-container" jslog=${VisualLogging.toolbar()} role="toolbar">
<devtools-toolbar class="coverage-toolbar" role="presentation" wrappable>
<select title=${i18nString(UIStrings.chooseCoverageGranularityPer)}
aria-label=${i18nString(UIStrings.chooseCoverageGranularityPer)}
jslog=${VisualLogging.dropDown('coverage-type').track({change: true})}
=${(event: Event) => input.onCoverageTypeChanged((event.target as HTMLSelectElement).selectedIndex)}
.selectedIndex=${input.coverageType}
?disabled=${input.recording}>
<option value=${CoverageType.JAVA_SCRIPT | CoverageType.JAVA_SCRIPT_PER_FUNCTION}
jslog=${VisualLogging.item(`${CoverageType.JAVA_SCRIPT | CoverageType.JAVA_SCRIPT_PER_FUNCTION}`).track({click: true})}>
${i18nString(UIStrings.perFunction)}
</option>
<option value=${CoverageType.JAVA_SCRIPT}
jslog=${VisualLogging.item(`${CoverageType.JAVA_SCRIPT}`).track({click: true})}>
${i18nString(UIStrings.perBlock)}
</option>
</select>
<devtools-button ${bindToAction(input.supportsRecordOnReload && !input.recording ?
'coverage.start-with-reload' : 'coverage.toggle-recording')}>
</devtools-button>
<devtools-button ${bindToAction('coverage.clear')}></devtools-button>
<div class="toolbar-divider"></div>
<devtools-button ${bindToAction('coverage.export')}></devtools-button>
<div class="toolbar-divider"></div>
<devtools-toolbar-input type="filter" placeholder=${i18nString(UIStrings.filterByUrl)}
?disabled=${!Boolean(input.coverageInfo)}
=${(e: CustomEvent<string>) => input.onFilterChanged(e.detail)}
style="flex-grow:1; flex-shrink:1">
</devtools-toolbar-input>
<div class="toolbar-divider"></div>
<select title=${i18nString(UIStrings.filterCoverageByType)}
aria-label=${i18nString(UIStrings.filterCoverageByType)}
jslog=${VisualLogging.dropDown('coverage-by-type').track({change: true})}
?disabled=${!Boolean(input.coverageInfo)}
=${(event: Event) => input.onTypeFilterChanged(
Number((event.target as HTMLSelectElement).selectedOptions[0]?.value))}>
<option value="" jslog=${VisualLogging.item('').track({click: true})}
.selected=${input.typeFilter === null}>${i18nString(UIStrings.all)}</option>
<option value=${CoverageType.CSS}
jslog=${VisualLogging.item(`${CoverageType.CSS}`).track({click: true})}
.selected=${input.typeFilter === CoverageType.CSS}>
${i18nString(UIStrings.css)}
</option>
<option value=${CoverageType.JAVA_SCRIPT | CoverageType.JAVA_SCRIPT_PER_FUNCTION}
jslog=${VisualLogging.item(`${CoverageType.JAVA_SCRIPT | CoverageType.JAVA_SCRIPT_PER_FUNCTION}`).track({click: true})}
.selected=${(input.typeFilter !== null && Boolean(input.typeFilter & (CoverageType.JAVA_SCRIPT | CoverageType.JAVA_SCRIPT_PER_FUNCTION)))}>
${i18nString(UIStrings.javascript)}
</option>
</select>
<div class="toolbar-divider"></div>
<devtools-checkbox title=${i18nString(UIStrings.includeExtensionContentScripts)}
${bindToSetting(input.showContentScriptsSetting)}
?disabled=${!Boolean(input.coverageInfo)}>
${i18nString(UIStrings.contentScripts)}
</devtools-checkbox>
</devtools-toolbar>
</div>
<div class="coverage-results">
${input.needsReload ?
renderReloadPromptPage(input.needsReload === 'bfcache-page' ?
i18nString(UIStrings.bfcacheNoCapture) : i18nString(UIStrings.activationNoCapture),
input.needsReload)
: input.coverageInfo ? html`
<devtools-widget autofocus class="results" .widgetConfig=${widgetConfig(CoverageListView, {
coverageInfo: input.coverageInfo,
highlightRegExp: input.textFilter,
selectedUrl: input.selectedUrl,
})}
${ref(e => { if (e instanceof HTMLElement) { output.focusResults = () => { e.focus(); };}})}>`
: renderLandingPage(input.supportsRecordOnReload)}
</div>
<div class="coverage-toolbar-summary">
<div class="coverage-message">
${input.statusMessage}
</div>
</div>`, target);
// clang-format on
};
function renderLandingPage(supportsRecordOnReload: boolean): TemplateResult {
if (supportsRecordOnReload) {
// clang-format off
return html`
<devtools-widget .widgetConfig=${widgetConfig(UI.EmptyWidget.EmptyWidget,{
header: i18nString(UIStrings.noCoverageData),
link: 'https://developer.chrome.com/docs/devtools/coverage' as Platform.DevToolsPath.UrlString,
text: i18nString(UIStrings.clickTheReloadButtonSToReloadAnd, {PH1: i18nString(UIStrings.reloadPage)}),
})}>
<devtools-button ${bindToAction('coverage.start-with-reload')}
.variant=${Buttons.Button.Variant.TONAL} .iconName=${undefined}>
${i18nString(UIStrings.reloadPage)}
</devtools-button>
</devtools-widget>`;
// clang-format on
}
// clang-format off
return html`
<devtools-widget .widgetConfig=${widgetConfig(UI.EmptyWidget.EmptyWidget,{
header: i18nString(UIStrings.noCoverageData),
link: 'https://developer.chrome.com/docs/devtools/coverage' as Platform.DevToolsPath.UrlString,
text: i18nString(UIStrings.clickTheRecordButtonSToStart, {PH1: i18nString(UIStrings.startRecording)}),
})}>
<devtools-button ${bindToAction('coverage.toggle-recording')}
.variant=${Buttons.Button.Variant.TONAL} .iconName=${undefined}>
${i18nString(UIStrings.startRecording)}
</devtools-button>
</devtools-widget>`;
// clang-format on
}
function renderReloadPromptPage(message: Common.UIString.LocalizedString, className: string): TemplateResult {
// clang-format off
return html`
<div class="widget vbox ${className}">
<div class="message">${message}</div>
<span class="message">
${i18nTemplate(UIStrings.reloadPrompt, {PH1: html`
<devtools-button class="inline-button" ${bindToAction('inspector-main.reload')}></devtools-button>`})}
</span>
</div>`;
// clang-format on
}
export class CoverageView extends UI.Widget.VBox {
#model: CoverageModel|null;
#decorationManager: CoverageDecorationManager|null;
readonly #coverageTypeComboBoxSetting: Common.Settings.Setting<number>;
readonly #toggleRecordAction: UI.ActionRegistration.Action;
readonly #clearAction: UI.ActionRegistration.Action;
readonly #exportAction: UI.ActionRegistration.Action;
#textFilter: RegExp|null;
#typeFilter: number|null;
readonly #showContentScriptsSetting: Common.Settings.Setting<boolean>;
readonly #view: View;
#supportsRecordOnReload: boolean;
#needsReload: 'bfcache-page'|'prerender-page'|null = null;
#statusMessage = '';
#output: CoverageViewOutput = {focusResults: () => {}};
#coverageInfo: CoverageListItem[]|null = null;
#selectedUrl: Platform.DevToolsPath.UrlString|null = null;
constructor(view: View = DEFAULT_VIEW) {
super({
jslog: `${VisualLogging.panel('coverage').track({resize: true})}`,
useShadowDom: true,
delegatesFocus: true,
});
this.registerRequiredCSS(coverageViewStyles);
this.#view = view;
this.#model = null;
this.#decorationManager = null;
this.#coverageTypeComboBoxSetting =
Common.Settings.Settings.instance().createSetting('coverage-view-coverage-type', 0);
this.#toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().getAction('coverage.toggle-recording');
const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
this.#supportsRecordOnReload = Boolean(mainTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel));
this.#clearAction = UI.ActionRegistry.ActionRegistry.instance().getAction('coverage.clear');
this.#clearAction.setEnabled(false);
this.#exportAction = UI.ActionRegistry.ActionRegistry.instance().getAction('coverage.export');
this.#exportAction.setEnabled(false);
this.#textFilter = null;
this.#typeFilter = null;
this.#showContentScriptsSetting = Common.Settings.Settings.instance().createSetting('show-content-scripts', false);
this.#showContentScriptsSetting.addChangeListener(this.#onFilterChanged, this);
this.requestUpdate();
}
override performUpdate(): void {
const input: CoverageViewInput = {
coverageType: this.#coverageTypeComboBoxSetting.get(),
recording: this.#toggleRecordAction.toggled(),
supportsRecordOnReload: this.#supportsRecordOnReload,
typeFilter: this.#typeFilter,
showContentScriptsSetting: this.#showContentScriptsSetting,
needsReload: this.#needsReload,
coverageInfo: this.#coverageInfo,
textFilter: this.#textFilter,
selectedUrl: this.#selectedUrl,
statusMessage: this.#statusMessage,
onCoverageTypeChanged: this.#onCoverageTypeChanged.bind(this),
onFilterChanged: (value: string) => {
this.#textFilter = value ? Platform.StringUtilities.createPlainTextSearchRegex(value, 'i') : null;
this.#onFilterChanged();
},
onTypeFilterChanged: this.#onTypeFilterChanged.bind(this),
};
this.#view(input, this.#output, this.contentElement);
}
static instance(): CoverageView {
if (!coverageViewInstance) {
coverageViewInstance = new CoverageView();
}
return coverageViewInstance;
}
static removeInstance(): void {
coverageViewInstance = undefined;
}
clear(): void {
if (this.#model) {
this.#model.reset();
}
this.#reset();
}
#reset(): void {
if (this.#decorationManager) {
this.#decorationManager.dispose();
this.#decorationManager = null;
}
this.#needsReload = null;
this.#coverageInfo = null;
this.#statusMessage = '';
this.#exportAction.setEnabled(false);
this.requestUpdate();
}
toggleRecording(): void {
const enable = !this.#toggleRecordAction.toggled();
if (enable) {
void this.startRecording({reload: false, jsCoveragePerBlock: this.isBlockCoverageSelected()});
} else {
void this.stopRecording();
}
}
isBlockCoverageSelected(): boolean {
// Check that Coverage.CoverageType.JavaScriptPerFunction is not present.
return this.#coverageTypeComboBoxSetting.get() === CoverageType.JAVA_SCRIPT;
}
#selectCoverageType(jsCoveragePerBlock: boolean): void {
const selectedIndex = jsCoveragePerBlock ? 1 : 0;
this.#coverageTypeComboBoxSetting.set(selectedIndex);
}
#onCoverageTypeChanged(newValue: number): void {
this.#coverageTypeComboBoxSetting.set(newValue);
}
async startRecording(options: {reload: (boolean|undefined), jsCoveragePerBlock: (boolean|undefined)}|null):
Promise<void> {
this.#reset();
const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
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.#model.addEventListener(Events.SourceMapResolved, this.#updateListView, this);
const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
this.#onPrimaryPageChanged, this);
this.#decorationManager = new CoverageDecorationManager(
this.#model, Workspace.Workspace.WorkspaceImpl.instance(),
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(),
Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance());
this.#toggleRecordAction.setToggled(true);
this.#clearAction.setEnabled(false);
this.#coverageInfo = [];
this.#needsReload = null;
this.requestUpdate();
await this.updateComplete;
this.#output.focusResults();
if (reload && resourceTreeModel) {
resourceTreeModel.reloadPage();
} else {
void this.#model.startPolling();
}
}
#onCoverageDataReceived(event: Common.EventTarget.EventTargetEvent<CoverageInfo[]>): void {
const data = event.data;
this.#updateViews(data);
}
#updateListView(): void {
const entries =
(this.#model?.entries() || [])
.map(entry => this.#toCoverageListItem(entry))
.filter(info => this.#isVisible(info))
.map(
(entry: CoverageListItem) =>
({...entry, sources: entry.sources.filter((entry: CoverageListItem) => this.#isVisible(entry))}));
this.#coverageInfo = entries;
}
#toCoverageListItem(info: URLCoverageInfo): CoverageListItem {
return {
url: info.url(),
type: info.type(),
size: info.size(),
usedSize: info.usedSize(),
unusedSize: info.unusedSize(),
usedPercentage: info.usedPercentage(),
unusedPercentage: info.unusedPercentage(),
sources: [...info.sourcesURLCoverageInfo.values()].map(this.#toCoverageListItem, this),
isContentScript: info.isContentScript(),
generatedUrl: info instanceof SourceURLCoverageInfo ? info.generatedURLCoverageInfo.url() : undefined,
};
}
async stopRecording(): Promise<void> {
SDK.TargetManager.TargetManager.instance().removeModelListener(
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
this.#onPrimaryPageChanged, this);
// 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.#clearAction.setEnabled(true);
this.requestUpdate();
}
async #onPrimaryPageChanged(
event: Common.EventTarget.EventTargetEvent<
{frame: SDK.ResourceTreeModel.ResourceTreeFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType}>):
Promise<void> {
const frame = event.data.frame;
const coverageModel = frame.resourceTreeModel().target().model(CoverageModel);
if (!coverageModel) {
return;
}
// If the primary page target has changed (due to MPArch activation), switch to new CoverageModel.
if (this.#model !== coverageModel) {
if (this.#model) {
await this.#model.stop();
this.#model.removeEventListener(Events.CoverageUpdated, this.#onCoverageDataReceived, this);
}
this.#model = coverageModel;
const success = await this.#model.start(this.isBlockCoverageSelected());
if (!success) {
return;
}
this.#model.addEventListener(Events.CoverageUpdated, this.#onCoverageDataReceived, this);
this.#decorationManager = new CoverageDecorationManager(
this.#model, Workspace.Workspace.WorkspaceImpl.instance(),
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(),
Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance());
}
if (event.data.type === SDK.ResourceTreeModel.PrimaryPageChangeType.ACTIVATION) {
this.#needsReload = 'prerender-page';
} else if (frame.backForwardCacheDetails.restoredFromCache) {
this.#needsReload = 'bfcache-page';
} else {
this.#needsReload = null;
this.#coverageInfo = [];
}
this.requestUpdate();
this.#model.reset();
this.#decorationManager?.reset();
void this.#model.startPolling();
}
#updateViews(updatedEntries: CoverageInfo[]): void {
this.#updateStats();
this.#updateListView();
this.#exportAction.setEnabled(this.#model !== null && this.#model.entries().length > 0);
this.#decorationManager?.update(updatedEntries);
this.requestUpdate();
}
#updateStats(): void {
const all = {total: 0, unused: 0};
const filtered = {total: 0, unused: 0};
const filterApplied = this.#textFilter !== null;
if (this.#model) {
for (const info of this.#model.entries()) {
all.total += info.size();
all.unused += info.unusedSize();
const listItem = this.#toCoverageListItem(info);
if (this.#isVisible(listItem)) {
if (this.#textFilter?.test(info.url())) {
filtered.total += info.size();
filtered.unused += info.unusedSize();
} else {
// If it doesn't match the filter, calculate the stats from visible children if there are any
for (const childInfo of info.sourcesURLCoverageInfo.values()) {
if (this.#isVisible(this.#toCoverageListItem(childInfo))) {
filtered.total += childInfo.size();
filtered.unused += childInfo.unusedSize();
}
}
}
}
}
}
this.#statusMessage = 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: i18n.ByteUtilities.bytesToString(used),
PH2: i18n.ByteUtilities.bytesToString(total),
PH3: percentUsed,
PH4: i18n.ByteUtilities.bytesToString(unused),
});
}
}
#onFilterChanged(): void {
this.#updateListView();
this.#updateStats();
this.requestUpdate();
}
#onTypeFilterChanged(typeFilter: number): void {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageReportFiltered);
this.#typeFilter = typeFilter;
this.#updateListView();
this.#updateStats();
this.requestUpdate();
}
#isVisible(coverageInfo: CoverageListItem): 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.#typeFilter && !(coverageInfo.type & this.#typeFilter)) {
return false;
}
// If it's a parent, check if any children are visible
if (coverageInfo.sources.length > 0) {
for (const sourceURLCoverageInfo of coverageInfo.sources) {
if (this.#isVisible(sourceURLCoverageInfo)) {
return true;
}
}
}
return !this.#textFilter || this.#textFilter.test(url);
}
async exportReport(): Promise<void> {
const fos = new Bindings.FileUtils.FileOutputStream();
const fileName =
`Coverage-${Platform.DateUtilities.toISO8601Compact(new Date())}.json` as Platform.DevToolsPath.RawPathString;
const accepted = await fos.open(fileName);
if (!accepted) {
return;
}
this.#model && await this.#model.exportReport(fos);
}
selectCoverageItemByUrl(url: string): void {
this.#selectedUrl = url as Platform.DevToolsPath.UrlString;
this.requestUpdate();
}
static readonly EXTENSION_BINDINGS_URL_PREFIX = 'extensions::';
override wasShown(): void {
UI.Context.Context.instance().setFlavor(CoverageView, this);
super.wasShown();
}
override willHide(): void {
super.willHide();
UI.Context.Context.instance().setFlavor(CoverageView, null);
}
get model(): CoverageModel|null {
return this.#model;
}
}
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
handleAction(_context: UI.Context.Context, actionId: string): boolean {
const coverageViewId = 'coverage';
void UI.ViewManager.ViewManager.instance()
.showView(coverageViewId, /** userGesture= */ false, /** omitFocus= */ true)
.then(() => {
const view = UI.ViewManager.ViewManager.instance().view(coverageViewId);
return view?.widget();
})
.then(widget => this.#handleAction(widget as CoverageView, actionId));
return true;
}
#handleAction(coverageView: CoverageView, actionId: string): void {
switch (actionId) {
case 'coverage.toggle-recording':
coverageView.toggleRecording();
break;
case 'coverage.start-with-reload':
void coverageView.startRecording({reload: true, jsCoveragePerBlock: coverageView.isBlockCoverageSelected()});
break;
case 'coverage.clear':
coverageView.clear();
break;
case 'coverage.export':
void coverageView.exportReport();
break;
default:
console.assert(false, `Unknown action: ${actionId}`);
}
}
}