Bug 1643633 - Add a stack trace to the exception tooltip r=davidwalsh

Adds a stacktrace to the exception popup of the inline exceptions in the debugger.
Matches the styles of the console error messages.

The popup works in two modes:
- when the stacktrace is closed the exception popup gets closed when the mouse leaves the popup.
- when the stacktrace is opened the exception popup gets closed only by clicking outside the popup.

Differential Revision: https://phabricator.services.mozilla.com/D82690
This commit is contained in:
Stepan Stava 2020-07-17 15:15:55 +00:00
Родитель a1c623c01c
Коммит 5676d0c1ff
6 изменённых файлов: 275 добавлений и 11 удалений

Просмотреть файл

@ -15,6 +15,7 @@ export function addException({ resource }: Object) {
return;
}
const { columnNumber, lineNumber, sourceId, errorMessage } = pageError;
const stacktrace = pageError.stacktrace || [];
if (!hasException(getState(), lineNumber, columnNumber)) {
dispatch({
@ -24,6 +25,7 @@ export function addException({ resource }: Object) {
lineNumber,
sourceActorId: sourceId,
errorMessage,
stacktrace,
},
});
}

Просмотреть файл

@ -0,0 +1,178 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
// @flow
import React, { Component } from "react";
import { connect } from "../../../utils/connect";
import classnames from "classnames";
import Reps from "devtools-reps";
const {
REPS: { StringRep },
} = Reps;
import actions from "../../../actions";
import { getThreadContext } from "../../../selectors";
import AccessibleImage from "../../shared/AccessibleImage";
import { DevToolsUtils } from "devtools-modules";
import type { ThreadContext, StacktraceFrame, Exception } from "../../../types";
type Props = {
cx: ThreadContext,
clearPreview: typeof actions.clearPreview,
selectSourceURL: typeof actions.selectSourceURL,
exception: Exception,
mouseout: Function,
};
type OwnProps = {|
exception: Exception,
mouseout: Function,
|};
type State = {
isStacktraceExpanded: boolean,
};
const POPUP_SELECTOR = ".preview-popup.exception-popup";
const ANONYMOUS_FN_NAME = "<anonymous>";
// The exception popup works in two modes:
// a. when the stacktrace is closed the exception popup
// gets closed when the mouse leaves the popup.
// b. when the stacktrace is opened the exception popup
// gets closed only by clicking outside the popup.
class ExceptionPopup extends Component<Props, State> {
topWindow: Object;
constructor(props: Props) {
super(props);
this.state = {
isStacktraceExpanded: false,
};
}
updateTopWindow() {
// The ChromeWindow is used when the stacktrace is expanded to capture all clicks
// outside the popup so the popup can be closed only by clicking outside of it.
if (this.topWindow) {
this.topWindow.removeEventListener(
"mousedown",
this.onTopWindowClick,
true
);
this.topWindow = null;
}
this.topWindow = DevToolsUtils.getTopWindow(window.parent);
this.topWindow.addEventListener("mousedown", this.onTopWindowClick, true);
}
onTopWindowClick = (e: Object) => {
const { cx, clearPreview } = this.props;
// When the stactrace is expaned the exception popup gets closed
// only by clicking ouside the popup.
if (!e.target.closest(POPUP_SELECTOR)) {
clearPreview(cx);
}
};
onExceptionMessageClick() {
const isStacktraceExpanded = this.state.isStacktraceExpanded;
this.updateTopWindow();
this.setState({ isStacktraceExpanded: !isStacktraceExpanded });
}
buildStackFrame(frame: StacktraceFrame) {
const { cx, selectSourceURL } = this.props;
const { filename, lineNumber } = frame;
const functionName = frame.functionName || ANONYMOUS_FN_NAME;
return (
<div
className="frame"
onClick={() => selectSourceURL(cx, filename, { line: lineNumber })}
>
<span className="title">{functionName}</span>
<span className="location">
<span className="filename">{filename}</span>:
<span className="line">{lineNumber}</span>
</span>
</div>
);
}
renderStacktrace(stacktrace: StacktraceFrame[]) {
const isStacktraceExpanded = this.state.isStacktraceExpanded;
if (stacktrace.length && isStacktraceExpanded) {
return (
<div className="exception-stacktrace">
{stacktrace.map(frame => this.buildStackFrame(frame))}
</div>
);
}
return null;
}
renderArrowIcon(stacktrace: StacktraceFrame[]) {
if (stacktrace.length) {
return (
<AccessibleImage
className={classnames("arrow", {
expanded: this.state.isStacktraceExpanded,
})}
/>
);
}
return null;
}
render() {
const {
exception: { stacktrace, errorMessage },
mouseout,
} = this.props;
return (
<div
className="preview-popup exception-popup"
dir="ltr"
onMouseLeave={() => mouseout(true, this.state.isStacktraceExpanded)}
>
<div
className="exception-message"
onClick={() => this.onExceptionMessageClick()}
>
{this.renderArrowIcon(stacktrace)}
{StringRep.rep({
object: errorMessage,
useQuotes: false,
className: "exception-text",
})}
</div>
{this.renderStacktrace(stacktrace)}
</div>
);
}
}
const mapStateToProps = state => ({
cx: getThreadContext(state),
});
const mapDispatchToProps = {
selectSourceURL: actions.selectSourceURL,
clearPreview: actions.clearPreview,
};
export default connect<Props, OwnProps, _, _, _, _>(
mapStateToProps,
mapDispatchToProps
)(ExceptionPopup);

Просмотреть файл

@ -147,3 +147,58 @@
.theme-dark .exception-popup .exception-text {
color: var(--red-20);
}
.exception-popup .exception-message {
display: flex;
align-items: center;
}
.exception-message .arrow {
margin-inline-end: 4px;
}
.exception-popup .exception-stacktrace {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: 8px;
padding-inline: 2px 3px;
line-height: var(--theme-code-line-height);
}
.exception-stacktrace .frame {
display: contents;
cursor: pointer;
}
.exception-stacktrace .title {
grid-column: 1/2;
color: var(--grey-90);
}
.theme-dark .exception-stacktrace .title {
color: white;
}
.exception-stacktrace .location {
grid-column: -1/-2;
color: var(--theme-highlight-purple);
direction: rtl;
text-align: end;
white-space: nowrap;
/* Force the location to be on one line and crop at start if wider then max-width */
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
}
.theme-dark .exception-stacktrace .location {
color: var(--blue-40);
}
.exception-stacktrace .line {
color: var(--theme-highlight-blue);
}
.theme-dark .exception-stacktrace .line {
color: hsl(210, 40%, 60%);
}

Просмотреть файл

@ -9,7 +9,7 @@ import { connect } from "../../../utils/connect";
import Reps from "devtools-reps";
const {
REPS: { Rep, StringRep },
REPS: { Rep },
MODE,
objectInspector,
} = Reps;
@ -20,6 +20,8 @@ const {
node: { nodeIsPrimitive, nodeIsFunction, nodeIsObject },
} = utils;
import ExceptionPopup from "./ExceptionPopup";
import actions from "../../../actions";
import { getThreadContext } from "../../../selectors";
import Popover from "../../shared/Popover";
@ -53,6 +55,7 @@ export class Popup extends Component<Props> {
marker: any;
pos: any;
popover: ?React$ElementRef<typeof Popover>;
isExceptionStactraceOpen: ?boolean;
constructor(props: Props) {
super(props);
@ -179,16 +182,11 @@ export class Popup extends Component<Props> {
}
renderExceptionPreview(exception: Exception) {
const errorMessage = exception.errorMessage;
return (
<div className="preview-popup exception-popup">
{StringRep.rep({
object: errorMessage,
useQuotes: false,
className: "exception-text",
})}
</div>
<ExceptionPopup
exception={exception}
mouseout={this.onMouseOutException}
/>
);
}
@ -239,6 +237,27 @@ export class Popup extends Component<Props> {
clearPreview(cx);
};
onMouseOutException = (
shouldClearOnMouseout: ?boolean,
isExceptionStactraceOpen: ?boolean
) => {
// onMouseOutException can be called:
// a. when the mouse leaves Popover element
// b. when the mouse leaves ExceptionPopup element
// We want to prevent closing the popup when the stacktrace
// is expanded and the mouse leaves either the Popover element
// or the ExceptionPopup element.
const { clearPreview, cx } = this.props;
if (shouldClearOnMouseout) {
this.isExceptionStactraceOpen = isExceptionStactraceOpen;
}
if (!this.isExceptionStactraceOpen) {
clearPreview(cx);
}
};
render() {
const {
preview: { cursorPos, resultGrip, exception },
@ -259,7 +278,7 @@ export class Popup extends Component<Props> {
type={type}
editorRef={editorRef}
target={this.props.preview.target}
mouseout={this.onMouseOut}
mouseout={exception ? this.onMouseOutException : this.onMouseOut}
>
{this.renderPreview()}
</Popover>

Просмотреть файл

@ -8,6 +8,7 @@ DIRS += [
]
CompiledModules(
'ExceptionPopup.js',
'index.js',
'Popup.js',
)

Просмотреть файл

@ -529,4 +529,13 @@ export type Exception = {
fileName: URL,
lineNumber: number,
sourceActorId: SourceActorId,
stacktrace: Array<StacktraceFrame>,
};
export type StacktraceFrame = {
columnNumber: number,
filename: URL,
functionName: string,
lineNumber: number,
sourceId: SourceActorId,
};