| // Copyright 2016 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. |
| |
| /** @typedef {chrome.developerPrivate.ManifestError} */ |
| let ManifestError; |
| /** @typedef {chrome.developerPrivate.RuntimeError} */ |
| let RuntimeError; |
| |
| cr.define('extensions', function() { |
| 'use strict'; |
| |
| /** @interface */ |
| class ErrorPageDelegate { |
| /** |
| * @param {string} extensionId |
| * @param {!Array<number>=} errorIds |
| * @param {chrome.developerPrivate.ErrorType=} type |
| */ |
| deleteErrors(extensionId, errorIds, type) {} |
| |
| /** |
| * @param {chrome.developerPrivate.RequestFileSourceProperties} args |
| * @return {!Promise<!chrome.developerPrivate.RequestFileSourceResponse>} |
| */ |
| requestFileSource(args) {} |
| } |
| |
| /** |
| * Get the URL relative to the main extension url. If the url is |
| * unassociated with the extension, this will be the full url. |
| * @param {string} url |
| * @param {?(ManifestError|RuntimeError)} error |
| * @return {string} |
| */ |
| function getRelativeUrl(url, error) { |
| const fullUrl = 'chrome-extension://' + error.extensionId + '/'; |
| return 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. |
| * @param {!ManifestError|!RuntimeError} item |
| * @param {string} log |
| * @param {string} warn |
| * @param {string} error |
| * @return {string} |
| * @private |
| */ |
| function getErrorSeverityText_(item, log, warn, error) { |
| if (item.type == chrome.developerPrivate.ErrorType.RUNTIME) { |
| switch (item.severity) { |
| case chrome.developerPrivate.ErrorLevel.LOG: |
| return log; |
| case chrome.developerPrivate.ErrorLevel.WARN: |
| return warn; |
| case chrome.developerPrivate.ErrorLevel.ERROR: |
| return error; |
| } |
| assertNotReached(); |
| } |
| assert(item.type == chrome.developerPrivate.ErrorType.MANIFEST); |
| return warn; |
| } |
| |
| const ErrorPage = Polymer({ |
| is: 'extensions-error-page', |
| |
| behaviors: [CrContainerShadowBehavior], |
| |
| properties: { |
| /** @type {!chrome.developerPrivate.ExtensionInfo|undefined} */ |
| data: Object, |
| |
| /** @type {!extensions.ErrorPageDelegate|undefined} */ |
| delegate: Object, |
| |
| // Whether or not dev mode is enabled. |
| inDevMode: { |
| type: Boolean, |
| value: false, |
| observer: 'onInDevModeChanged_', |
| }, |
| |
| /** @private {!Array<!(ManifestError|RuntimeError)>} */ |
| entries_: Array, |
| |
| /** @private {?chrome.developerPrivate.RequestFileSourceResponse} */ |
| code_: Object, |
| |
| /** |
| * Index into |entries_|. |
| * @private |
| */ |
| selectedEntry_: { |
| type: Number, |
| observer: 'onSelectedErrorChanged_', |
| }, |
| |
| /** @private {?chrome.developerPrivate.StackFrame}*/ |
| selectedStackFrame_: { |
| type: Object, |
| value: function() { |
| return null; |
| }, |
| }, |
| }, |
| |
| observers: [ |
| 'observeDataChanges_(data.*)', |
| ], |
| |
| listeners: { |
| 'view-enter-start': 'onViewEnterStart_', |
| }, |
| |
| /** @override */ |
| ready: function() { |
| cr.ui.FocusOutlineManager.forDocument(document); |
| }, |
| |
| /** @return {!ManifestError|!RuntimeError} */ |
| getSelectedError: function() { |
| return this.entries_[this.selectedEntry_]; |
| }, |
| |
| /** |
| * Focuses the back button when page is loaded. |
| * @private |
| */ |
| onViewEnterStart_: function() { |
| Polymer.RenderStatus.afterNextRender( |
| this, () => cr.ui.focusWithoutInk(this.$.closeButton)); |
| chrome.metricsPrivate.recordUserAction('Options_ViewExtensionErrors'); |
| }, |
| |
| /** |
| * @param {!ManifestError|!RuntimeError} error |
| * @param {string} unknown |
| * @return {string} |
| * @private |
| */ |
| getContextUrl_: function(error, unknown) { |
| return error.contextUrl ? getRelativeUrl(error.contextUrl, error) : |
| unknown; |
| }, |
| |
| /** |
| * Watches for changes to |data| in order to fetch the corresponding |
| * file source. |
| * @private |
| */ |
| observeDataChanges_: function() { |
| const errors = this.data.manifestErrors.concat(this.data.runtimeErrors); |
| this.entries_ = errors; |
| this.selectedEntry_ = -1; // This also help reset code-section content. |
| if (this.entries_.length) |
| this.selectedEntry_ = 0; |
| }, |
| |
| /** @private */ |
| onCloseButtonTap_: function() { |
| extensions.navigation.navigateTo({page: Page.LIST}); |
| }, |
| |
| /** @private */ |
| onClearAllTap_: function() { |
| const ids = this.entries_.map(entry => entry.id); |
| this.delegate.deleteErrors(this.data.id, ids); |
| }, |
| |
| /** |
| * @param {!ManifestError|!RuntimeError} error |
| * @return {string} |
| * @private |
| */ |
| computeErrorIcon_: function(error) { |
| // Do not i18n these strings, they're CSS classes. |
| return getErrorSeverityText_(error, 'info', 'warning', 'error'); |
| }, |
| |
| /** |
| * @param {!ManifestError|!RuntimeError} error |
| * @return {string} |
| * @private |
| */ |
| computeErrorTypeLabel_: function(error) { |
| return getErrorSeverityText_( |
| error, loadTimeData.getString('logLevel'), |
| loadTimeData.getString('warnLevel'), |
| loadTimeData.getString('errorLevel')); |
| }, |
| |
| /** |
| * @param {!Event} e |
| * @private |
| */ |
| onDeleteErrorAction_: function(e) { |
| if (e.type == 'keydown' && !((e.code == 'Space' || e.code == 'Enter'))) |
| return; |
| |
| this.delegate.deleteErrors( |
| this.data.id, [(/** @type {!{model:Object}} */ (e)).model.item.id]); |
| e.stopPropagation(); |
| }, |
| |
| /** private */ |
| onInDevModeChanged_: function() { |
| if (!this.inDevMode) { |
| // Wait until next render cycle in case error page is loading. |
| this.async(() => { |
| this.onCloseButtonTap_(); |
| }); |
| } |
| }, |
| |
| /** |
| * Fetches the source for the selected error and populates the code section. |
| * @private |
| */ |
| onSelectedErrorChanged_: function() { |
| this.code_ = null; |
| |
| if (this.selectedEntry_ < 0) |
| return; |
| |
| const error = this.getSelectedError(); |
| const args = { |
| extensionId: error.extensionId, |
| message: error.message, |
| }; |
| switch (error.type) { |
| case chrome.developerPrivate.ErrorType.MANIFEST: |
| args.pathSuffix = error.source; |
| args.manifestKey = error.manifestKey; |
| args.manifestSpecific = error.manifestSpecific; |
| break; |
| case chrome.developerPrivate.ErrorType.RUNTIME: |
| // slice(1) because pathname starts with a /. |
| args.pathSuffix = new URL(error.source).pathname.slice(1); |
| args.lineNumber = error.stackTrace && error.stackTrace[0] ? |
| error.stackTrace[0].lineNumber : |
| 0; |
| this.selectedStackFrame_ = error.stackTrace && error.stackTrace[0] ? |
| error.stackTrace[0] : |
| null; |
| break; |
| } |
| this.delegate.requestFileSource(args).then(code => this.code_ = code); |
| }, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| computeIsRuntimeError_: function(item) { |
| 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)". |
| * @param {!chrome.developerPrivate.StackFrame} frame |
| * @return {string} |
| * @private |
| */ |
| getStackTraceLabel_: function(frame) { |
| 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; |
| }, |
| |
| /** |
| * @param {chrome.developerPrivate.StackFrame} frame |
| * @return {string} |
| * @private |
| */ |
| getStackFrameClass_: function(frame) { |
| return frame == this.selectedStackFrame_ ? 'selected' : ''; |
| }, |
| |
| /** |
| * @param {!chrome.developerPrivate.StackFrame} frame |
| * @return {number} |
| * @private |
| */ |
| getStackFrameTabIndex_: function(frame) { |
| 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. |
| * @param {string} url |
| * @return {boolean} |
| * @private |
| */ |
| shouldDisplayFrame_: function(url) { |
| // All our internal scripts are in the 'extensions::' namespace. |
| return !/^extensions::/.test(url); |
| }, |
| |
| /** |
| * @param {!chrome.developerPrivate.StackFrame} frame |
| * @private |
| */ |
| updateSelected_: function(frame) { |
| this.selectedStackFrame_ = assert(frame); |
| |
| const selectedError = this.getSelectedError(); |
| this.delegate |
| .requestFileSource({ |
| extensionId: selectedError.extensionId, |
| message: selectedError.message, |
| pathSuffix: getRelativeUrl(frame.url, selectedError), |
| lineNumber: frame.lineNumber, |
| }) |
| .then(code => this.code_ = code); |
| }, |
| |
| /** |
| * @param {!Event} e |
| * @private |
| */ |
| onStackFrameTap_: function(e) { |
| const frame = /** @type {!{model:Object}} */ (e).model.item; |
| this.updateSelected_(frame); |
| }, |
| |
| /** |
| * @param {!Event} e |
| * @private |
| */ |
| onStackKeydown_: function(e) { |
| let direction = 0; |
| |
| if (e.key == 'ArrowDown') |
| direction = 1; |
| else if (e.key == 'ArrowUp') |
| direction = -1; |
| else |
| return; |
| |
| e.preventDefault(); |
| |
| const list = e.target.parentElement.querySelectorAll('li'); |
| |
| for (let i = 0; i < list.length; ++i) { |
| if (list[i].classList.contains('selected')) { |
| const polymerEvent = /** @type {!{model: !Object}} */ (e); |
| const frame = polymerEvent.model.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. |
| * @param {number} index |
| * @return {string} |
| * @private |
| */ |
| computeErrorClass_: function(index) { |
| return index == this.selectedEntry_ ? 'selected' : ''; |
| }, |
| |
| /** @private */ |
| iconName_: function(index) { |
| return index == this.selectedEntry_ ? 'icon-expand-less' : |
| 'icon-expand-more'; |
| }, |
| |
| /** |
| * Determine if the iron-collapse should be opened (expanded). |
| * @param {number} index |
| * @return {boolean} |
| * @private |
| */ |
| isOpened_: function(index) { |
| return index == this.selectedEntry_; |
| }, |
| |
| |
| /** |
| * @param {number} index |
| * @return {string} The aria-expanded value as a string. |
| * @private |
| */ |
| isAriaExpanded_: function(index) { |
| return this.isOpened_(index).toString(); |
| }, |
| |
| /** |
| * @param {!{type: string, code: string, model: !{index: number}}} e |
| * @private |
| */ |
| onErrorItemAction_: function(e) { |
| if (e.type == 'keydown' && !((e.code == 'Space' || e.code == 'Enter'))) |
| return; |
| |
| this.selectedEntry_ = |
| this.selectedEntry_ == e.model.index ? -1 : e.model.index; |
| }, |
| }); |
| |
| return { |
| ErrorPage: ErrorPage, |
| ErrorPageDelegate: ErrorPageDelegate, |
| }; |
| }); |