blob: 4de3b29451321d4da09a7bd3d4678270cc2a6baf [file] [log] [blame]
// 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 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_collapse/cr_collapse.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_icon/cr_icon.js';
import 'chrome://resources/cr_elements/icons.html.js';
import './code_section.js';
import './shared_style.css.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {getCss} from './error_page.css.js';
import {getHtml} from './error_page.html.js';
import type {ItemDelegate} from './item.js';
import {ItemMixin} from './item_mixin.js';
import {navigation, Page} from './navigation_helper.js';
type ManifestError = chrome.developerPrivate.ManifestError;
type RuntimeError = chrome.developerPrivate.RuntimeError;
export interface ErrorPageDelegate {
deleteErrors(
extensionId: string, errorIds?: number[],
type?: chrome.developerPrivate.ErrorType): void;
requestFileSource(args: chrome.developerPrivate.RequestFileSourceProperties):
Promise<chrome.developerPrivate.RequestFileSourceResponse>;
}
/**
* Get the URL relative to the main extension url. If the url is
* unassociated with the extension, this will be the full url.
*/
function getRelativeUrl(
url: string, error: ManifestError|RuntimeError|null): string {
const fullUrl = error ? `chrome-extension://${error.extensionId}/` : '';
return (fullUrl && url.startsWith(fullUrl)) ? url.substring(fullUrl.length) :
url;
}
/**
* Given 3 strings, this function returns the correct one for the type of
* error that |item| is.
*/
function getErrorSeverityText(
item: ManifestError|RuntimeError, log: string, warn: string,
error: string): string {
if (item.type === chrome.developerPrivate.ErrorType.RUNTIME) {
switch ((item as RuntimeError).severity) {
case chrome.developerPrivate.ErrorLevel.LOG:
return log;
case chrome.developerPrivate.ErrorLevel.WARN:
return warn;
case chrome.developerPrivate.ErrorLevel.ERROR:
return error;
default:
assertNotReached();
}
}
assert(item.type === chrome.developerPrivate.ErrorType.MANIFEST);
return warn;
}
export interface ExtensionsErrorPageElement {
$: {
closeButton: HTMLElement,
};
}
const ExtensionsErrorPageElementBase = ItemMixin(CrLitElement);
export class ExtensionsErrorPageElement extends ExtensionsErrorPageElementBase {
static get is() {
return 'extensions-error-page';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
data: {type: Object},
delegate: {type: Object},
// Whether or not dev mode is enabled.
inDevMode: {type: Boolean},
entries_: {type: Array},
code_: {type: Object},
/**
* Index into |entries_|.
*/
selectedEntry_: {type: Number},
selectedStackFrame_: {type: Object},
};
}
accessor data: chrome.developerPrivate.ExtensionInfo|undefined;
accessor delegate: ErrorPageDelegate&ItemDelegate|undefined;
accessor inDevMode: boolean = false;
protected accessor entries_: Array<ManifestError|RuntimeError> = [];
protected accessor code_: chrome.developerPrivate.RequestFileSourceResponse|
null = null;
private accessor selectedEntry_: number = -1;
private accessor selectedStackFrame_: chrome.developerPrivate.StackFrame|
null = null;
override firstUpdated() {
this.addEventListener('view-enter-start', this.onViewEnterStart_);
FocusOutlineManager.forDocument(document);
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has('data') && this.data) {
/**
* Watches for changes to |data| in order to fetch the corresponding
* file source.
*/
this.entries_ = [...this.data.manifestErrors, ...this.data.runtimeErrors];
this.selectedEntry_ = this.entries_.length > 0 ? 0 : -1;
this.onSelectedErrorChanged_();
}
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('inDevMode') && !this.inDevMode) {
this.onCloseButtonClick_();
}
}
getSelectedError(): ManifestError|RuntimeError|null {
return this.selectedEntry_ === -1 ? null :
this.entries_[this.selectedEntry_]!;
}
/**
* Focuses the back button when page is loaded.
*/
private onViewEnterStart_() {
this.updateComplete.then(() => focusWithoutInk(this.$.closeButton));
chrome.metricsPrivate.recordUserAction('Options_ViewExtensionErrors');
}
protected getContextUrl_(error: ManifestError|RuntimeError): string {
return (error as RuntimeError).contextUrl ?
getRelativeUrl((error as RuntimeError).contextUrl, error) :
loadTimeData.getString('errorContextUnknown');
}
protected onCloseButtonClick_() {
navigation.navigateTo({page: Page.LIST});
}
protected onClearAllClick_() {
const ids = this.entries_.map(entry => entry.id);
assert(this.data);
assert(this.delegate);
this.delegate.deleteErrors(this.data.id, ids);
}
protected computeErrorIcon_(error: ManifestError|RuntimeError): string {
// Do not i18n these strings, they're icon names.
return getErrorSeverityText(error, 'cr:info', 'cr:warning', 'cr:error');
}
protected computeErrorTypeLabel_(error: ManifestError|RuntimeError): string {
return getErrorSeverityText(
error, loadTimeData.getString('logLevel'),
loadTimeData.getString('warnLevel'),
loadTimeData.getString('errorLevel'));
}
protected onDeleteErrorAction_(e: Event) {
const id = Number((e.currentTarget as HTMLElement).dataset['errorId']);
assert(this.data);
assert(this.delegate);
this.delegate.deleteErrors(this.data.id, [id]);
e.stopPropagation();
}
/**
* Fetches the source for the selected error and populates the code section.
*/
private onSelectedErrorChanged_() {
this.code_ = null;
if (this.selectedEntry_ < 0) {
return;
}
// Safe to use ! here because we check for selectedEntry_ < 0 above.
const error = this.getSelectedError()!;
const args: chrome.developerPrivate.RequestFileSourceProperties = {
extensionId: error.extensionId,
message: error.message,
pathSuffix: '',
};
switch (error.type) {
case chrome.developerPrivate.ErrorType.MANIFEST:
const manifestError = error as ManifestError;
args.pathSuffix = manifestError.source;
args.manifestKey = manifestError.manifestKey;
args.manifestSpecific = manifestError.manifestSpecific;
break;
case chrome.developerPrivate.ErrorType.RUNTIME:
const runtimeError = error as RuntimeError;
try {
// slice(1) because pathname starts with a /.
args.pathSuffix = new URL(runtimeError.source).pathname.slice(1);
} catch (e) {
// Swallow the invalid URL error and return early. This prevents the
// uncaught error from causing a runtime error as seen in
// crbug.com/1257170.
return;
}
args.lineNumber =
runtimeError.stackTrace && runtimeError.stackTrace[0] ?
runtimeError.stackTrace[0].lineNumber :
0;
this.selectedStackFrame_ =
runtimeError.stackTrace && runtimeError.stackTrace[0] ?
runtimeError.stackTrace[0] :
null;
break;
}
assert(this.delegate);
this.delegate.requestFileSource(args).then(code => this.code_ = code);
}
protected computeIsRuntimeError_(item: ManifestError|RuntimeError): boolean {
return item.type === chrome.developerPrivate.ErrorType.RUNTIME;
}
/**
* The description is a human-readable summation of the frame, in the
* form "<relative_url>:<line_number> (function)", e.g.
* "myfile.js:25 (myFunction)".
*/
protected getStackTraceLabel_(frame: chrome.developerPrivate.StackFrame):
string {
let description = getRelativeUrl(frame.url, this.getSelectedError()) + ':' +
frame.lineNumber;
if (frame.functionName) {
const functionName = frame.functionName === '(anonymous function)' ?
loadTimeData.getString('anonymousFunction') :
frame.functionName;
description += ' (' + functionName + ')';
}
return description;
}
protected getStackFrameClass_(frame: chrome.developerPrivate.StackFrame):
string {
return frame === this.selectedStackFrame_ ? 'selected' : '';
}
protected getStackFrameTabIndex_(frame: chrome.developerPrivate.StackFrame):
number {
return frame === this.selectedStackFrame_ ? 0 : -1;
}
/**
* This function is used to determine whether or not we want to show a
* stack frame. We don't want to show code from internal scripts.
*/
protected shouldDisplayFrame_(url: string): boolean {
// All our internal scripts are in the 'extensions::' namespace.
return !/^extensions::/.test(url);
}
private updateSelected_(frame: chrome.developerPrivate.StackFrame) {
this.selectedStackFrame_ = frame;
const selectedError = this.getSelectedError();
assert(selectedError);
assert(this.delegate);
this.delegate
.requestFileSource({
extensionId: selectedError.extensionId,
message: selectedError.message,
pathSuffix: getRelativeUrl(frame.url, selectedError),
lineNumber: frame.lineNumber,
})
.then(code => this.code_ = code);
}
protected onStackFrameClick_(e: Event) {
const target = e.currentTarget as HTMLElement;
const frameIndex = Number(target.dataset['frameIndex']);
const errorIndex = Number(target.dataset['errorIndex']);
const error = this.entries_[errorIndex] as RuntimeError;
const frame = error.stackTrace[frameIndex]!;
this.updateSelected_(frame);
}
protected onStackKeydown_(e: KeyboardEvent) {
let direction = 0;
if (e.key === 'ArrowDown') {
direction = 1;
} else if (e.key === 'ArrowUp') {
direction = -1;
} else {
return;
}
e.preventDefault();
const list =
(e.target as HTMLElement).parentElement!.querySelectorAll('li');
for (let i = 0; i < list.length; ++i) {
if (list[i]!.classList.contains('selected')) {
const index =
Number((e.currentTarget as HTMLElement).dataset['errorIndex']);
const item = this.entries_[index] as RuntimeError;
const frame = item.stackTrace[i + direction];
if (frame) {
this.updateSelected_(frame);
list[i + direction]!.focus(); // Preserve focus.
}
return;
}
}
}
/**
* Computes the class name for the error item depending on whether its
* the currently selected error.
*/
protected computeErrorClass_(index: number): string {
return index === this.selectedEntry_ ? 'selected' : '';
}
protected iconName_(index: number): string {
return index === this.selectedEntry_ ? 'icon-expand-less' :
'icon-expand-more';
}
/**
* Determine if the cr-collapse should be opened (expanded).
*/
protected isOpened_(index: number): boolean {
return index === this.selectedEntry_;
}
/**
* @return The aria-expanded value as a string.
*/
protected isAriaExpanded_(index: number): string {
return this.isOpened_(index).toString();
}
protected onErrorItemAction_(e: KeyboardEvent) {
if (e.type === 'keydown' && !((e.code === 'Space' || e.code === 'Enter'))) {
return;
}
// Call preventDefault() to avoid the browser scrolling when the space key
// is pressed.
e.preventDefault();
const index =
Number((e.currentTarget as HTMLElement).dataset['errorIndex']);
this.selectedEntry_ = this.selectedEntry_ === index ? -1 : index;
this.onSelectedErrorChanged_();
}
protected showReloadButton_(): boolean {
return this.canReloadItem();
}
protected onReloadClick_() {
this.reloadItem().catch((loadError) => this.fire('load-error', loadError));
}
}
declare global {
interface HTMLElementTagNameMap {
'extensions-error-page': ExtensionsErrorPageElement;
}
}
customElements.define(
ExtensionsErrorPageElement.is, ExtensionsErrorPageElement);