summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/SecondaryPanes
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes')
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js235
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js84
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css235
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js40
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js172
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build13
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js22
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap18
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css33
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js502
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css76
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js189
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css155
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js339
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.css171
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.js486
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js232
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js18
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css185
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css38
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js191
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js220
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build13
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js110
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js270
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js93
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap1172
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap1001
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap2286
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.css100
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.js413
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css98
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Thread.js81
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.css64
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.js40
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css53
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js218
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css108
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js383
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/index.js548
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/moz.build22
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js78
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js136
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js77
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js341
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap408
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap203
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap619
48 files changed, 12589 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
new file mode 100644
index 0000000000..c55088e411
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
@@ -0,0 +1,235 @@
+/* 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/>. */
+
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { createSelector } from "devtools/client/shared/vendor/reselect";
+import actions from "../../../actions/index";
+
+import { CloseButton } from "../../shared/Button/index";
+
+import {
+ getSelectedText,
+ makeBreakpointId,
+} from "../../../utils/breakpoint/index";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { isLineBlackboxed } from "../../../utils/source";
+
+import {
+ getSelectedFrame,
+ getSelectedSource,
+ getCurrentThread,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+ getBlackBoxRanges,
+} from "../../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Breakpoint extends PureComponent {
+ static get propTypes() {
+ return {
+ breakpoint: PropTypes.object.isRequired,
+ disableBreakpoint: PropTypes.func.isRequired,
+ editor: PropTypes.object.isRequired,
+ enableBreakpoint: PropTypes.func.isRequired,
+ frame: PropTypes.object,
+ openConditionalPanel: PropTypes.func.isRequired,
+ removeBreakpoint: PropTypes.func.isRequired,
+ selectSpecificLocation: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object,
+ source: PropTypes.object.isRequired,
+ blackboxedRangesForSource: PropTypes.array.isRequired,
+ checkSourceOnIgnoreList: PropTypes.func.isRequired,
+ isBreakpointLineBlackboxed: PropTypes.bool,
+ showBreakpointContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ onContextMenu = event => {
+ event.preventDefault();
+
+ this.props.showBreakpointContextMenu(
+ event,
+ this.props.breakpoint,
+ this.props.source
+ );
+ };
+
+ get selectedLocation() {
+ const { breakpoint, selectedSource } = this.props;
+ return getSelectedLocation(breakpoint, selectedSource);
+ }
+
+ stopClicks = event => event.stopPropagation();
+
+ onDoubleClick = () => {
+ const { breakpoint, openConditionalPanel } = this.props;
+ if (breakpoint.options.condition) {
+ openConditionalPanel(this.selectedLocation);
+ } else if (breakpoint.options.logValue) {
+ openConditionalPanel(this.selectedLocation, true);
+ }
+ };
+
+ selectBreakpoint = event => {
+ event.preventDefault();
+ const { selectSpecificLocation } = this.props;
+ selectSpecificLocation(this.selectedLocation);
+ };
+
+ removeBreakpoint = event => {
+ const { removeBreakpoint, breakpoint } = this.props;
+ event.stopPropagation();
+ removeBreakpoint(breakpoint);
+ };
+
+ handleBreakpointCheckbox = () => {
+ const { breakpoint, enableBreakpoint, disableBreakpoint } = this.props;
+ if (breakpoint.disabled) {
+ enableBreakpoint(breakpoint);
+ } else {
+ disableBreakpoint(breakpoint);
+ }
+ };
+
+ isCurrentlyPausedAtBreakpoint() {
+ const { frame } = this.props;
+ if (!frame) {
+ return false;
+ }
+
+ const bpId = makeBreakpointId(this.selectedLocation);
+ const frameId = makeBreakpointId(frame.selectedLocation);
+ return bpId == frameId;
+ }
+
+ getBreakpointLocation() {
+ const { source } = this.props;
+ const { column, line } = this.selectedLocation;
+
+ const isWasm = source?.isWasm;
+ // column is 0-based everywhere, but we want to display 1-based to the user.
+ const columnVal = column ? `:${column + 1}` : "";
+ const bpLocation = isWasm
+ ? `0x${line.toString(16).toUpperCase()}`
+ : `${line}${columnVal}`;
+
+ return bpLocation;
+ }
+
+ getBreakpointText() {
+ const { breakpoint, selectedSource } = this.props;
+ const { condition, logValue } = breakpoint.options;
+ return logValue || condition || getSelectedText(breakpoint, selectedSource);
+ }
+
+ highlightText(text = "", editor) {
+ const node = document.createElement("div");
+ editor.CodeMirror.runMode(text, "application/javascript", node);
+ return { __html: node.innerHTML };
+ }
+
+ render() {
+ const { breakpoint, editor, isBreakpointLineBlackboxed } = this.props;
+ const text = this.getBreakpointText();
+ const labelId = `${breakpoint.id}-label`;
+ return div(
+ {
+ className: classnames({
+ breakpoint,
+ paused: this.isCurrentlyPausedAtBreakpoint(),
+ disabled: breakpoint.disabled,
+ "is-conditional": !!breakpoint.options.condition,
+ "is-log": !!breakpoint.options.logValue,
+ }),
+ onClick: this.selectBreakpoint,
+ onDoubleClick: this.onDoubleClick,
+ onContextMenu: this.onContextMenu,
+ },
+ input({
+ id: breakpoint.id,
+ type: "checkbox",
+ className: "breakpoint-checkbox",
+ checked: !breakpoint.disabled,
+ disabled: isBreakpointLineBlackboxed,
+ onChange: this.handleBreakpointCheckbox,
+ onClick: this.stopClicks,
+ "aria-labelledby": labelId,
+ }),
+ span(
+ {
+ id: labelId,
+ className: "breakpoint-label cm-s-mozilla devtools-monospace",
+ onClick: this.selectBreakpoint,
+ title: text,
+ },
+ span({
+ dangerouslySetInnerHTML: this.highlightText(text, editor),
+ })
+ ),
+ div(
+ {
+ className: "breakpoint-line-close",
+ },
+ div(
+ {
+ className: "breakpoint-line devtools-monospace",
+ },
+ this.getBreakpointLocation()
+ ),
+ React.createElement(CloseButton, {
+ handleClick: this.removeBreakpoint,
+ tooltip: L10N.getStr("breakpoints.removeBreakpointTooltip"),
+ })
+ )
+ );
+ }
+}
+
+const getFormattedFrame = createSelector(
+ getSelectedSource,
+ getSelectedFrame,
+ (selectedSource, frame) => {
+ if (!frame) {
+ return null;
+ }
+
+ return {
+ ...frame,
+ selectedLocation: getSelectedLocation(frame, selectedSource),
+ };
+ }
+);
+
+const mapStateToProps = (state, props) => {
+ const blackboxedRangesForSource = getBlackBoxRanges(state)[props.source.url];
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, props.source);
+ return {
+ selectedSource: getSelectedSource(state),
+ isBreakpointLineBlackboxed: isLineBlackboxed(
+ blackboxedRangesForSource,
+ props.breakpoint.location.line,
+ isSourceOnIgnoreList
+ ),
+ frame: getFormattedFrame(state, getCurrentThread(state)),
+ };
+};
+
+export default connect(mapStateToProps, {
+ enableBreakpoint: actions.enableBreakpoint,
+ removeBreakpoint: actions.removeBreakpoint,
+ disableBreakpoint: actions.disableBreakpoint,
+ selectSpecificLocation: actions.selectSpecificLocation,
+ openConditionalPanel: actions.openConditionalPanel,
+ showBreakpointContextMenu: actions.showBreakpointContextMenu,
+})(Breakpoint);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
new file mode 100644
index 0000000000..78cc530cff
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
@@ -0,0 +1,84 @@
+/* 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/>. */
+
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../../actions/index";
+
+import {
+ getTruncatedFileName,
+ getDisplayPath,
+ getSourceQueryString,
+ getFileURL,
+} from "../../../utils/source";
+import { createLocation } from "../../../utils/location";
+import { getFirstSourceActorForGeneratedSource } from "../../../selectors/index";
+
+import SourceIcon from "../../shared/SourceIcon";
+
+class BreakpointHeading extends PureComponent {
+ static get propTypes() {
+ return {
+ sources: PropTypes.array.isRequired,
+ source: PropTypes.object.isRequired,
+ firstSourceActor: PropTypes.object,
+ selectSource: PropTypes.func.isRequired,
+ showBreakpointHeadingContextMenu: PropTypes.func.isRequired,
+ };
+ }
+ onContextMenu = event => {
+ event.preventDefault();
+
+ this.props.showBreakpointHeadingContextMenu(event, this.props.source);
+ };
+
+ render() {
+ const { sources, source, selectSource } = this.props;
+
+ const path = getDisplayPath(source, sources);
+ const query = getSourceQueryString(source);
+ return div(
+ {
+ className: "breakpoint-heading",
+ title: getFileURL(source, false),
+ onClick: () => selectSource(source),
+ onContextMenu: this.onContextMenu,
+ },
+ React.createElement(
+ SourceIcon,
+ // Breakpoints are displayed per source and may relate to many source actors.
+ // Arbitrarily pick the first source actor to compute the matching source icon
+ // The source actor is used to pick one specific source text content and guess
+ // the related framework icon.
+ {
+ location: createLocation({
+ source,
+ sourceActor: this.props.firstSourceActor,
+ }),
+ modifier: icon =>
+ ["file", "javascript"].includes(icon) ? null : icon,
+ }
+ ),
+ div(
+ {
+ className: "filename",
+ },
+ getTruncatedFileName(source, query),
+ path && span(null, `../${path}/..`)
+ )
+ );
+ }
+}
+
+const mapStateToProps = (state, { source }) => ({
+ firstSourceActor: getFirstSourceActorForGeneratedSource(state, source.id),
+});
+
+export default connect(mapStateToProps, {
+ selectSource: actions.selectSource,
+ showBreakpointHeadingContextMenu: actions.showBreakpointHeadingContextMenu,
+})(BreakpointHeading);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css
new file mode 100644
index 0000000000..c05dd0b53f
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css
@@ -0,0 +1,235 @@
+/* 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/>. */
+
+.breakpoints-pane > ._content {
+ overflow-x: auto;
+}
+
+.breakpoints-options *,
+.breakpoints-list * {
+ user-select: none;
+}
+
+.breakpoints-list {
+ padding: 4px 0;
+}
+
+.breakpoints-list .breakpoint-heading {
+ text-overflow: ellipsis;
+ width: 100%;
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.breakpoint-heading:not(:first-child) {
+ margin-top: 2px;
+}
+
+.breakpoints-list .breakpoint-heading .filename {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.breakpoints-list .breakpoint-heading .filename span {
+ opacity: 0.7;
+ padding-left: 4px;
+}
+
+.breakpoints-list .breakpoint-heading,
+.breakpoints-list .breakpoint {
+ color: var(--theme-text-color-strong);
+ position: relative;
+ cursor: pointer;
+}
+
+.breakpoints-list .breakpoint-heading,
+.breakpoints-list .breakpoint,
+.breakpoints-options > * {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-inline-start: 16px;
+ padding-inline-end: 12px;
+}
+
+.breakpoints-exceptions-caught {
+ padding-bottom: 3px;
+ padding-top: 3px;
+ padding-inline-start: 36px;
+}
+
+.breakpoints-options {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.xhr-breakpoints-pane .breakpoints-options {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-options:not(.empty) {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-options input {
+ padding-inline-start: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-inline-start: 0;
+ margin-inline-end: 2px;
+ vertical-align: text-bottom;
+}
+
+.breakpoint-exceptions-label {
+ line-height: 14px;
+ padding-inline-end: 8px;
+ cursor: default;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.breakpoints-list .breakpoint,
+.breakpoints-list .breakpoint-heading,
+.breakpoints-options {
+ border-inline-start: 4px solid transparent
+}
+
+html .breakpoints-list .breakpoint.is-conditional {
+ border-inline-start-color: var(--theme-graphs-yellow);
+}
+
+html .breakpoints-list .breakpoint.is-log {
+ border-inline-start-color: var(--theme-graphs-purple);
+}
+
+html .breakpoints-list .breakpoint.paused {
+ background-color: var(--theme-toolbar-background-alt);
+ border-color: var(--breakpoint-active-color);
+}
+
+.breakpoints-list .breakpoint:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.breakpoint-line-close {
+ margin-inline-start: 4px;
+}
+
+.breakpoints-list .breakpoint .breakpoint-line {
+ font-size: 11px;
+ color: var(--theme-comment);
+ min-width: 16px;
+ text-align: end;
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+.breakpoints-list .breakpoint:hover .breakpoint-line,
+.breakpoints-list .breakpoint-line-close:focus-within .breakpoint-line {
+ color: transparent;
+}
+
+.breakpoints-list .breakpoint.paused:hover {
+ border-color: var(--breakpoint-active-color-hover);
+}
+
+.breakpoints-list .breakpoint-label {
+ display: inline-block;
+ cursor: pointer;
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: 11px;
+}
+
+.breakpoints-list .breakpoint-label span,
+.breakpoint-line-close {
+ display: inline;
+ line-height: 14px;
+}
+
+.breakpoint-checkbox {
+ margin-inline-start: 0px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ vertical-align: text-bottom;
+}
+
+.breakpoint-label .location {
+ width: 100%;
+ display: inline-block;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ padding: 1px 0;
+ vertical-align: bottom;
+}
+
+.breakpoints-list .pause-indicator {
+ flex: 0 1 content;
+ order: 3;
+}
+
+.breakpoint .close-btn {
+ position: absolute;
+ /* hide button outside of row until hovered or focused */
+ top: -100px;
+}
+
+[dir="ltr"] .breakpoint .close-btn {
+ right: 12px;
+}
+
+[dir="rtl"] .breakpoint .close-btn {
+ left: 12px;
+}
+
+/* Reveal the remove button on hover/focus */
+.breakpoint:hover .close-btn,
+.breakpoint .close-btn:focus {
+ top: calc(50% - 8px);
+}
+
+/* Hide the line number when revealing the remove button (since they're overlayed) */
+.breakpoint-line-close:focus-within .breakpoint-line,
+.breakpoint:hover .breakpoint-line {
+ visibility: hidden;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint {
+ cursor: pointer;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-lines {
+ padding: 0;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-sizer {
+ min-width: initial !important;
+}
+
+.breakpoints-list .breakpoint .CodeMirror.cm-s-mozilla-breakpoint {
+ transition: opacity 0.15s linear;
+}
+
+.breakpoints-list .breakpoint.disabled .CodeMirror.cm-s-mozilla-breakpoint {
+ opacity: 0.5;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-line span[role="presentation"] {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-code,
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-scroll {
+ pointer-events: none;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint {
+ padding-top: 1px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js
new file mode 100644
index 0000000000..31ff3f44a3
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js
@@ -0,0 +1,40 @@
+/* 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/>. */
+import {
+ div,
+ input,
+ label,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+export default function ExceptionOption({
+ className,
+ isChecked = false,
+ label: inputLabel,
+ onChange,
+}) {
+ return label(
+ {
+ className,
+ },
+ input({
+ type: "checkbox",
+ checked: isChecked,
+ onChange: onChange,
+ }),
+ div(
+ {
+ className: "breakpoint-exceptions-label",
+ },
+ inputLabel
+ )
+ );
+}
+
+ExceptionOption.propTypes = {
+ className: PropTypes.string.isRequired,
+ isChecked: PropTypes.bool.isRequired,
+ label: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js
new file mode 100644
index 0000000000..0f5d6f7ae3
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js
@@ -0,0 +1,172 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import ExceptionOption from "./ExceptionOption";
+
+import Breakpoint from "./Breakpoint";
+import BreakpointHeading from "./BreakpointHeading";
+
+import actions from "../../../actions/index";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { createHeadlessEditor } from "../../../utils/editor/create-editor";
+
+import { makeBreakpointId } from "../../../utils/breakpoint/index";
+
+import {
+ getSelectedSource,
+ getBreakpointSources,
+ getShouldPauseOnDebuggerStatement,
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+} from "../../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Breakpoints extends Component {
+ static get propTypes() {
+ return {
+ breakpointSources: PropTypes.array.isRequired,
+ pauseOnExceptions: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object,
+ shouldPauseOnDebuggerStatement: PropTypes.bool.isRequired,
+ shouldPauseOnCaughtExceptions: PropTypes.bool.isRequired,
+ shouldPauseOnExceptions: PropTypes.bool.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ this.removeEditor();
+ }
+
+ getEditor() {
+ if (!this.headlessEditor) {
+ this.headlessEditor = createHeadlessEditor();
+ }
+ return this.headlessEditor;
+ }
+
+ removeEditor() {
+ if (!this.headlessEditor) {
+ return;
+ }
+ this.headlessEditor.destroy();
+ this.headlessEditor = null;
+ }
+
+ togglePauseOnDebuggerStatement = () => {
+ this.props.pauseOnDebuggerStatement(
+ !this.props.shouldPauseOnDebuggerStatement
+ );
+ };
+
+ togglePauseOnException = () => {
+ this.props.pauseOnExceptions(!this.props.shouldPauseOnExceptions, false);
+ };
+
+ togglePauseOnCaughtException = () => {
+ this.props.pauseOnExceptions(
+ true,
+ !this.props.shouldPauseOnCaughtExceptions
+ );
+ };
+
+ renderExceptionsOptions() {
+ const {
+ breakpointSources,
+ shouldPauseOnDebuggerStatement,
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ } = this.props;
+
+ const isEmpty = !breakpointSources.length;
+ return div(
+ {
+ className: classnames("breakpoints-options", {
+ empty: isEmpty,
+ }),
+ },
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-debugger-statement",
+ label: L10N.getStr("pauseOnDebuggerStatement"),
+ isChecked: shouldPauseOnDebuggerStatement,
+ onChange: this.togglePauseOnDebuggerStatement,
+ }),
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions",
+ label: L10N.getStr("pauseOnExceptionsItem2"),
+ isChecked: shouldPauseOnExceptions,
+ onChange: this.togglePauseOnException,
+ }),
+ shouldPauseOnExceptions &&
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions-caught",
+ label: L10N.getStr("pauseOnCaughtExceptionsItem"),
+ isChecked: shouldPauseOnCaughtExceptions,
+ onChange: this.togglePauseOnCaughtException,
+ })
+ );
+ }
+
+ renderBreakpoints() {
+ const { breakpointSources, selectedSource } = this.props;
+ if (!breakpointSources.length) {
+ return null;
+ }
+
+ const editor = this.getEditor();
+ const sources = breakpointSources.map(({ source }) => source);
+ return div(
+ {
+ className: "pane breakpoints-list",
+ },
+ breakpointSources.map(({ source, breakpoints }) => {
+ return [
+ React.createElement(BreakpointHeading, {
+ key: source.id,
+ source,
+ sources,
+ }),
+ breakpoints.map(breakpoint =>
+ React.createElement(Breakpoint, {
+ breakpoint,
+ source,
+ editor,
+ key: makeBreakpointId(
+ getSelectedLocation(breakpoint, selectedSource)
+ ),
+ })
+ ),
+ ];
+ })
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: "pane",
+ },
+ this.renderExceptionsOptions(),
+ this.renderBreakpoints()
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpointSources: getBreakpointSources(state),
+ selectedSource: getSelectedSource(state),
+ shouldPauseOnDebuggerStatement: getShouldPauseOnDebuggerStatement(state),
+ shouldPauseOnExceptions: getShouldPauseOnExceptions(state),
+ shouldPauseOnCaughtExceptions: getShouldPauseOnCaughtExceptions(state),
+});
+
+export default connect(mapStateToProps, {
+ pauseOnDebuggerStatement: actions.pauseOnDebuggerStatement,
+ pauseOnExceptions: actions.pauseOnExceptions,
+})(Breakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build
new file mode 100644
index 0000000000..85716d122c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += []
+
+CompiledModules(
+ "Breakpoint.js",
+ "BreakpointHeading.js",
+ "ExceptionOption.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js
new file mode 100644
index 0000000000..51f0b1e948
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js
@@ -0,0 +1,22 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import ExceptionOption from "../ExceptionOption";
+
+describe("ExceptionOption renders", () => {
+ it("with values", () => {
+ const component = shallow(
+ React.createElement(ExceptionOption, {
+ label: "testLabel",
+ isChecked: true,
+ onChange: () => null,
+ className: "testClassName",
+ })
+ );
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap
new file mode 100644
index 0000000000..3ed80783b6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExceptionOption renders with values 1`] = `
+<label
+ className="testClassName"
+>
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ testLabel
+ </div>
+</label>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css
new file mode 100644
index 0000000000..68bd0bfcdd
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css
@@ -0,0 +1,33 @@
+/* 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/>. */
+
+.command-bar {
+ flex: 0 0 29px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+ overflow: hidden;
+ z-index: 1;
+ background-color: var(--theme-toolbar-background);
+}
+
+html[dir="rtl"] .command-bar {
+ border-right: 1px solid var(--theme-splitter-color);
+}
+
+.command-bar .filler {
+ flex-grow: 1;
+}
+
+.command-bar .step-position {
+ color: var(--theme-text-color-inactive);
+ padding-top: 8px;
+ margin-inline-end: 4px;
+}
+
+.command-bar .divider {
+ width: 1px;
+ background: var(--theme-splitter-color);
+ height: 10px;
+ margin: 11px 6px 0 6px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
new file mode 100644
index 0000000000..deae156a40
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
@@ -0,0 +1,502 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div, button } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { features, prefs } from "../../utils/prefs";
+import {
+ getIsWaitingOnBreak,
+ getSkipPausing,
+ getCurrentThread,
+ isTopFrameSelected,
+ getIsCurrentThreadPaused,
+ getIsJavascriptTracingEnabled,
+ getIsThreadCurrentlyTracing,
+ getJavascriptTracingLogMethod,
+ getJavascriptTracingValues,
+ getJavascriptTracingOnNextInteraction,
+ getJavascriptTracingOnNextLoad,
+ getJavascriptTracingFunctionReturn,
+} from "../../selectors/index";
+import { formatKeyShortcut } from "../../utils/text";
+import actions from "../../actions/index";
+import { debugBtn } from "../shared/Button/CommandBarButton";
+import AccessibleImage from "../shared/AccessibleImage";
+import { showMenu } from "../../context-menu/menu";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js");
+const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
+const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+// NOTE: the "resume" command will call either the resume or breakOnNext action
+// depending on whether or not the debugger is paused or running
+const COMMANDS = ["resume", "stepOver", "stepIn", "stepOut"];
+
+const KEYS = {
+ WINNT: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ trace: "Ctrl+Shift+5",
+ },
+ Darwin: {
+ resume: "Cmd+\\",
+ stepOver: "Cmd+'",
+ stepIn: "Cmd+;",
+ stepOut: "Cmd+Shift+:",
+ stepOutDisplay: "Cmd+Shift+;",
+ trace: "Ctrl+Shift+5",
+ },
+ Linux: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ trace: "Ctrl+Shift+5",
+ },
+};
+
+const LOG_METHODS = {
+ CONSOLE: "console",
+ STDOUT: "stdout",
+};
+
+function getKey(action) {
+ return getKeyForOS(Services.appinfo.OS, action);
+}
+
+function getKeyForOS(os, action) {
+ const osActions = KEYS[os] || KEYS.Linux;
+ return osActions[action];
+}
+
+function formatKey(action) {
+ const key = getKey(`${action}Display`) || getKey(action);
+
+ // On MacOS, we bind both Windows and MacOS/Darwin key shortcuts
+ // Display them both, but only when they are different
+ if (isMacOS) {
+ const winKey =
+ getKeyForOS("WINNT", `${action}Display`) || getKeyForOS("WINNT", action);
+ if (key != winKey) {
+ return formatKeyShortcut([key, winKey].join(" "));
+ }
+ }
+ return formatKeyShortcut(key);
+}
+
+class CommandBar extends Component {
+ constructor() {
+ super();
+
+ this.state = {};
+ }
+ static get propTypes() {
+ return {
+ breakOnNext: PropTypes.func.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ isTracingEnabled: PropTypes.bool.isRequired,
+ isWaitingOnBreak: PropTypes.bool.isRequired,
+ javascriptEnabled: PropTypes.bool.isRequired,
+ trace: PropTypes.func.isRequired,
+ resume: PropTypes.func.isRequired,
+ skipPausing: PropTypes.bool.isRequired,
+ stepIn: PropTypes.func.isRequired,
+ stepOut: PropTypes.func.isRequired,
+ stepOver: PropTypes.func.isRequired,
+ toggleEditorWrapping: PropTypes.func.isRequired,
+ toggleInlinePreview: PropTypes.func.isRequired,
+ toggleJavaScriptEnabled: PropTypes.func.isRequired,
+ toggleSkipPausing: PropTypes.any.isRequired,
+ toggleSourceMapsEnabled: PropTypes.func.isRequired,
+ topFrameSelected: PropTypes.bool.isRequired,
+ toggleTracing: PropTypes.func.isRequired,
+ logMethod: PropTypes.string.isRequired,
+ logValues: PropTypes.bool.isRequired,
+ traceOnNextInteraction: PropTypes.bool.isRequired,
+ setJavascriptTracingLogMethod: PropTypes.func.isRequired,
+ setHideOrShowIgnoredSources: PropTypes.func.isRequired,
+ toggleSourceMapIgnoreList: PropTypes.func.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ const { shortcuts } = this.context;
+
+ COMMANDS.forEach(action => shortcuts.off(getKey(action)));
+
+ if (isMacOS) {
+ COMMANDS.forEach(action => shortcuts.off(getKeyForOS("WINNT", action)));
+ }
+ }
+
+ componentDidMount() {
+ const { shortcuts } = this.context;
+
+ COMMANDS.forEach(action =>
+ shortcuts.on(getKey(action), e => this.handleEvent(e, action))
+ );
+
+ if (isMacOS) {
+ // The Mac supports both the Windows Function keys
+ // as well as the Mac non-Function keys
+ COMMANDS.forEach(action =>
+ shortcuts.on(getKeyForOS("WINNT", action), e =>
+ this.handleEvent(e, action)
+ )
+ );
+ }
+ }
+
+ handleEvent(e, action) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (action === "resume") {
+ this.props.isPaused ? this.props.resume() : this.props.breakOnNext();
+ } else {
+ this.props[action]();
+ }
+ }
+
+ renderStepButtons() {
+ const { isPaused, topFrameSelected } = this.props;
+ const className = isPaused ? "active" : "disabled";
+ const isDisabled = !isPaused;
+
+ return [
+ this.renderPauseButton(),
+ debugBtn(
+ () => this.props.stepOver(),
+ "stepOver",
+ className,
+ L10N.getFormatStr("stepOverTooltip", formatKey("stepOver")),
+ isDisabled
+ ),
+ debugBtn(
+ () => this.props.stepIn(),
+ "stepIn",
+ className,
+ L10N.getFormatStr("stepInTooltip", formatKey("stepIn")),
+ isDisabled || !topFrameSelected
+ ),
+ debugBtn(
+ () => this.props.stepOut(),
+ "stepOut",
+ className,
+ L10N.getFormatStr("stepOutTooltip", formatKey("stepOut")),
+ isDisabled
+ ),
+ ];
+ }
+
+ resume() {
+ this.props.resume();
+ }
+
+ renderTraceButton() {
+ if (!features.javascriptTracing) {
+ return null;
+ }
+
+ // The button is highlighted in blue as soon as the user requested to start the trace
+ const isActive = this.props.isTracingEnabled;
+ // But it will only be active once the tracer actually started.
+ // This may come later when using "on next user interaction" feature.
+ const isPending = isActive && !this.props.isTracingActive;
+
+ let className = "";
+ if (isPending) {
+ className = "pending";
+ } else if (isActive) {
+ className = "active";
+ }
+ // Display a button which:
+ // - on left click, would toggle on/off javascript tracing
+ // - on right click, would display a context menu to configure the tracer settings
+ return button({
+ className: `devtools-button command-bar-button debugger-trace-menu-button ${className}`,
+ title: this.props.isTracingEnabled
+ ? L10N.getFormatStr("stopTraceButtonTooltip2", formatKey("trace"))
+ : L10N.getFormatStr(
+ "startTraceButtonTooltip2",
+ formatKey("trace"),
+ this.props.logMethod
+ ),
+ onClick: event => {
+ this.props.toggleTracing();
+ },
+ onContextMenu: event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Avoid showing the menu to avoid having to support changing tracing config "live"
+ if (this.props.isTracingEnabled) {
+ return;
+ }
+ const items = [
+ {
+ id: "debugger-trace-menu-item-console",
+ label: L10N.getStr("traceInWebConsole"),
+ checked: this.props.logMethod == LOG_METHODS.CONSOLE,
+ type: "radio",
+ click: () => {
+ this.props.setJavascriptTracingLogMethod(LOG_METHODS.CONSOLE);
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-stdout",
+ label: L10N.getStr("traceInStdout"),
+ type: "radio",
+ checked: this.props.logMethod == LOG_METHODS.STDOUT,
+ click: () => {
+ this.props.setJavascriptTracingLogMethod(LOG_METHODS.STDOUT);
+ },
+ },
+ { type: "separator" },
+ {
+ id: "debugger-trace-menu-item-next-interaction",
+ label: L10N.getStr("traceOnNextInteraction"),
+ type: "checkbox",
+ checked: this.props.traceOnNextInteraction,
+ click: () => {
+ this.props.toggleJavascriptTracingOnNextInteraction();
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-next-load",
+ label: L10N.getStr("traceOnNextLoad"),
+ type: "checkbox",
+ checked: this.props.traceOnNextLoad,
+ click: () => {
+ this.props.toggleJavascriptTracingOnNextLoad();
+ },
+ },
+ { type: "separator" },
+ {
+ id: "debugger-trace-menu-item-log-values",
+ label: L10N.getStr("traceValues"),
+ type: "checkbox",
+ checked: this.props.logValues,
+ click: () => {
+ this.props.toggleJavascriptTracingValues();
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-function-return",
+ label: L10N.getStr("traceFunctionReturn"),
+ type: "checkbox",
+ checked: this.props.traceFunctionReturn,
+ click: () => {
+ this.props.toggleJavascriptTracingFunctionReturn();
+ },
+ },
+ ];
+ showMenu(event, items);
+ },
+ });
+ }
+ renderPauseButton() {
+ const { breakOnNext, isWaitingOnBreak } = this.props;
+
+ if (this.props.isPaused) {
+ return debugBtn(
+ () => this.resume(),
+ "resume",
+ "active",
+ L10N.getFormatStr("resumeButtonTooltip", formatKey("resume"))
+ );
+ }
+
+ if (isWaitingOnBreak) {
+ return debugBtn(
+ null,
+ "pause",
+ "disabled",
+ L10N.getStr("pausePendingButtonTooltip"),
+ true
+ );
+ }
+
+ return debugBtn(
+ () => breakOnNext(),
+ "pause",
+ "active",
+ L10N.getFormatStr("pauseButtonTooltip", formatKey("resume"))
+ );
+ }
+
+ renderSkipPausingButton() {
+ const { skipPausing, toggleSkipPausing } = this.props;
+ return button(
+ {
+ className: classnames(
+ "command-bar-button",
+ "command-bar-skip-pausing",
+ {
+ active: skipPausing,
+ }
+ ),
+ title: skipPausing
+ ? L10N.getStr("undoSkipPausingTooltip.label")
+ : L10N.getStr("skipPausingTooltip.label"),
+ onClick: toggleSkipPausing,
+ },
+ React.createElement(AccessibleImage, {
+ className: skipPausing ? "enable-pausing" : "disable-pausing",
+ })
+ );
+ }
+
+ renderSettingsButton() {
+ const { toolboxDoc } = this.context;
+ return React.createElement(
+ MenuButton,
+ {
+ menuId: "debugger-settings-menu-button",
+ toolboxDoc: toolboxDoc,
+ className:
+ "devtools-button command-bar-button debugger-settings-menu-button",
+ title: L10N.getStr("settings.button.label"),
+ },
+ () => this.renderSettingsMenuItems()
+ );
+ }
+
+ renderSettingsMenuItems() {
+ return React.createElement(
+ MenuList,
+ {
+ id: "debugger-settings-menu-list",
+ },
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-javascript",
+ className: "menu-item debugger-settings-menu-item-disable-javascript",
+ checked: !this.props.javascriptEnabled,
+ label: L10N.getStr("settings.disableJavaScript.label"),
+ tooltip: L10N.getStr("settings.disableJavaScript.tooltip"),
+ onClick: () => {
+ this.props.toggleJavaScriptEnabled(!this.props.javascriptEnabled);
+ },
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-inline-previews",
+ checked: features.inlinePreview,
+ label: L10N.getStr("inlinePreview.toggle.label"),
+ tooltip: L10N.getStr("inlinePreview.toggle.tooltip"),
+ onClick: () => this.props.toggleInlinePreview(!features.inlinePreview),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-wrap-lines",
+ checked: prefs.editorWrapping,
+ label: L10N.getStr("editorWrapping.toggle.label"),
+ tooltip: L10N.getStr("editorWrapping.toggle.tooltip"),
+ onClick: () => this.props.toggleEditorWrapping(!prefs.editorWrapping),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-sourcemaps",
+ checked: prefs.clientSourceMapsEnabled,
+ label: L10N.getStr("settings.toggleSourceMaps.label"),
+ tooltip: L10N.getStr("settings.toggleSourceMaps.tooltip"),
+ onClick: () =>
+ this.props.toggleSourceMapsEnabled(!prefs.clientSourceMapsEnabled),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-hide-ignored-sources",
+ className: "menu-item debugger-settings-menu-item-hide-ignored-sources",
+ checked: prefs.hideIgnoredSources,
+ label: L10N.getStr("settings.hideIgnoredSources.label"),
+ tooltip: L10N.getStr("settings.hideIgnoredSources.tooltip"),
+ onClick: () =>
+ this.props.setHideOrShowIgnoredSources(!prefs.hideIgnoredSources),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-enable-sourcemap-ignore-list",
+ className:
+ "menu-item debugger-settings-menu-item-enable-sourcemap-ignore-list",
+ checked: prefs.sourceMapIgnoreListEnabled,
+ label: L10N.getStr("settings.enableSourceMapIgnoreList.label"),
+ tooltip: L10N.getStr("settings.enableSourceMapIgnoreList.tooltip"),
+ onClick: () =>
+ this.props.toggleSourceMapIgnoreList(
+ !prefs.sourceMapIgnoreListEnabled
+ ),
+ })
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: classnames("command-bar", {
+ vertical: !this.props.horizontal,
+ }),
+ },
+ this.renderStepButtons(),
+ div({
+ className: "filler",
+ }),
+ this.renderTraceButton(),
+ this.renderSkipPausingButton(),
+ div({
+ className: "devtools-separator",
+ }),
+ this.renderSettingsButton()
+ );
+ }
+}
+
+CommandBar.contextTypes = {
+ shortcuts: PropTypes.object,
+ toolboxDoc: PropTypes.object,
+};
+
+const mapStateToProps = state => ({
+ isWaitingOnBreak: getIsWaitingOnBreak(state, getCurrentThread(state)),
+ skipPausing: getSkipPausing(state),
+ topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
+ javascriptEnabled: state.ui.javascriptEnabled,
+ isPaused: getIsCurrentThreadPaused(state),
+ isTracingEnabled: getIsJavascriptTracingEnabled(
+ state,
+ getCurrentThread(state)
+ ),
+ isTracingActive: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
+ logMethod: getJavascriptTracingLogMethod(state),
+ logValues: getJavascriptTracingValues(state),
+ traceOnNextInteraction: getJavascriptTracingOnNextInteraction(state),
+ traceOnNextLoad: getJavascriptTracingOnNextLoad(state),
+ traceFunctionReturn: getJavascriptTracingFunctionReturn(state),
+});
+
+export default connect(mapStateToProps, {
+ toggleTracing: actions.toggleTracing,
+ setJavascriptTracingLogMethod: actions.setJavascriptTracingLogMethod,
+ toggleJavascriptTracingValues: actions.toggleJavascriptTracingValues,
+ toggleJavascriptTracingOnNextInteraction:
+ actions.toggleJavascriptTracingOnNextInteraction,
+ toggleJavascriptTracingOnNextLoad: actions.toggleJavascriptTracingOnNextLoad,
+ toggleJavascriptTracingFunctionReturn:
+ actions.toggleJavascriptTracingFunctionReturn,
+ resume: actions.resume,
+ stepIn: actions.stepIn,
+ stepOut: actions.stepOut,
+ stepOver: actions.stepOver,
+ breakOnNext: actions.breakOnNext,
+ pauseOnExceptions: actions.pauseOnExceptions,
+ toggleSkipPausing: actions.toggleSkipPausing,
+ toggleInlinePreview: actions.toggleInlinePreview,
+ toggleEditorWrapping: actions.toggleEditorWrapping,
+ toggleSourceMapsEnabled: actions.toggleSourceMapsEnabled,
+ toggleJavaScriptEnabled: actions.toggleJavaScriptEnabled,
+ setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources,
+ toggleSourceMapIgnoreList: actions.toggleSourceMapIgnoreList,
+})(CommandBar);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css
new file mode 100644
index 0000000000..b525783984
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css
@@ -0,0 +1,76 @@
+/* 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/>. */
+
+ .dom-mutation-empty {
+ padding: 6px 20px;
+ text-align: center;
+ font-style: italic;
+ color: var(--theme-body-color);
+ white-space: normal;
+ }
+
+ .dom-mutation-empty a {
+ text-decoration: underline;
+ color: var(--theme-toolbar-selected-color);
+ cursor: pointer;
+ }
+
+.dom-mutation-list * {
+ user-select: none;
+}
+
+.dom-mutation-list {
+ padding: 4px 0;
+ list-style-type: none;
+}
+
+.dom-mutation-list li {
+ position: relative;
+
+ display: flex;
+ align-items: start;
+ overflow: hidden;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+}
+
+.dom-mutation-list input {
+ margin: 2px 3px;
+
+ padding-inline-start: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-inline-start: 0;
+ margin-inline-end: 2px;
+ vertical-align: text-bottom;
+}
+
+.dom-mutation-info {
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-inline-end: 20px;
+}
+
+.dom-mutation-list .close-btn {
+ position: absolute;
+ /* hide button outside of row until hovered or focused */
+ top: -100px;
+}
+
+/* Reveal the remove button on hover/focus */
+.dom-mutation-list li:hover .close-btn,
+.dom-mutation-list li .close-btn:focus {
+ top: calc(50% - 8px);
+}
+
+[dir="ltr"] .dom-mutation-list .close-btn {
+ right: 12px;
+}
+
+[dir="rtl"] .dom-mutation-list .close-btn {
+ left: 12px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js
new file mode 100644
index 0000000000..642ba21505
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js
@@ -0,0 +1,189 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+import { translateNodeFrontToGrip } from "devtools/client/inspector/shared/utils";
+
+import {
+ deleteDOMMutationBreakpoint,
+ toggleDOMMutationBreakpointState,
+} from "devtools/client/framework/actions/index";
+
+import actions from "../../actions/index";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { CloseButton } from "../shared/Button/index";
+
+const localizationTerms = {
+ subtree: L10N.getStr("domMutationTypes.subtree"),
+ attribute: L10N.getStr("domMutationTypes.attribute"),
+ removal: L10N.getStr("domMutationTypes.removal"),
+};
+
+class DOMMutationBreakpointsContents extends Component {
+ static get propTypes() {
+ return {
+ breakpoints: PropTypes.array.isRequired,
+ deleteBreakpoint: PropTypes.func.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openInspector: PropTypes.func.isRequired,
+ setSkipPausing: PropTypes.func.isRequired,
+ toggleBreakpoint: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ handleBreakpoint(breakpointId, shouldEnable) {
+ const { toggleBreakpoint, setSkipPausing } = this.props;
+
+ // The user has enabled a mutation breakpoint so we should no
+ // longer skip pausing
+ if (shouldEnable) {
+ setSkipPausing(false);
+ }
+ toggleBreakpoint(breakpointId, shouldEnable);
+ }
+
+ renderItem(breakpoint) {
+ const {
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ deleteBreakpoint,
+ } = this.props;
+ const { enabled, id: breakpointId, nodeFront, mutationType } = breakpoint;
+
+ return li(
+ {
+ key: breakpoint.id,
+ },
+ input({
+ type: "checkbox",
+ checked: enabled,
+ onChange: () => this.handleBreakpoint(breakpointId, !enabled),
+ }),
+ div(
+ {
+ className: "dom-mutation-info",
+ },
+ div(
+ {
+ className: "dom-mutation-label",
+ },
+ Rep({
+ object: translateNodeFrontToGrip(nodeFront),
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(nodeFront),
+ onInspectIconClick: () => openElementInInspector(nodeFront),
+ onDOMNodeMouseOver: () => highlightDomElement(nodeFront),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ })
+ ),
+ div(
+ {
+ className: "dom-mutation-type",
+ },
+ localizationTerms[mutationType] || mutationType
+ )
+ ),
+ React.createElement(CloseButton, {
+ handleClick: () => deleteBreakpoint(nodeFront, mutationType),
+ })
+ );
+ }
+
+ /* eslint-disable react/no-danger */
+ renderEmpty() {
+ const { openInspector } = this.props;
+ const text = L10N.getFormatStr(
+ "noDomMutationBreakpoints",
+ `<a>${L10N.getStr("inspectorTool")}</a>`
+ );
+ return div(
+ {
+ className: "dom-mutation-empty",
+ },
+ div({
+ onClick: () => openInspector(),
+ dangerouslySetInnerHTML: {
+ __html: text,
+ },
+ })
+ );
+ }
+
+ render() {
+ const { breakpoints } = this.props;
+
+ if (breakpoints.length === 0) {
+ return this.renderEmpty();
+ }
+ return ul(
+ {
+ className: "dom-mutation-list",
+ },
+ breakpoints.map(breakpoint => this.renderItem(breakpoint))
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpoints: state.domMutationBreakpoints.breakpoints,
+});
+
+const DOMMutationBreakpointsPanel = connect(
+ mapStateToProps,
+ {
+ deleteBreakpoint: deleteDOMMutationBreakpoint,
+ toggleBreakpoint: toggleDOMMutationBreakpointState,
+ },
+ undefined,
+ { storeKey: "toolbox-store" }
+)(DOMMutationBreakpointsContents);
+
+class DomMutationBreakpoints extends Component {
+ static get propTypes() {
+ return {
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openInspector: PropTypes.func.isRequired,
+ setSkipPausing: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ return React.createElement(DOMMutationBreakpointsPanel, {
+ openElementInInspector: this.props.openElementInInspector,
+ highlightDomElement: this.props.highlightDomElement,
+ unHighlightDomElement: this.props.unHighlightDomElement,
+ setSkipPausing: this.props.setSkipPausing,
+ openInspector: this.props.openInspector,
+ });
+ }
+}
+
+export default connect(undefined, {
+ // the debugger-specific action bound to the debugger store
+ // since there is no `storeKey`
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+ setSkipPausing: actions.setSkipPausing,
+ openInspector: actions.openInspector,
+})(DomMutationBreakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css
new file mode 100644
index 0000000000..eadc3a917b
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css
@@ -0,0 +1,155 @@
+/* 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/>. */
+
+.event-listeners-content {
+ padding-block: 4px;
+}
+
+.event-listeners-content ul {
+ padding: 0;
+ list-style-type: none;
+}
+
+.event-listeners-content button:hover,
+.event-listeners-content button:focus {
+ background: none;
+}
+
+.event-listener-group {
+ user-select: none;
+}
+
+.event-listener-header {
+ display: flex;
+ align-items: center;
+}
+
+.event-listener-expand {
+ border: none;
+ background: none;
+ padding: 4px 5px;
+ line-height: 12px;
+ outline-offset: -2px;
+}
+
+.event-listener-expand:hover {
+ background: transparent;
+}
+
+.event-listener-group input[type="checkbox"] {
+ margin: 0;
+ margin-inline-end: 4px;
+}
+
+.event-listener-label {
+ display: flex;
+ align-items: center;
+ padding-inline-end: 10px;
+}
+
+.event-listener-category {
+ padding: 3px 0;
+ line-height: 14px;
+}
+
+.event-listeners-content .arrow {
+ margin-inline-end: 0;
+}
+
+.event-listeners-content .arrow.expanded {
+ transform: rotate(0deg);
+}
+
+.event-listeners-content .arrow.expanded:dir(rtl) {
+ transform: rotate(90deg);
+}
+
+.event-listeners-list {
+ border-block-start: 1px;
+ padding-inline: 18px 20px;
+}
+
+.event-listener-event {
+ display: flex;
+ align-items: center;
+}
+
+.event-listeners-list .event-listener-event {
+ margin-inline-start: 40px;
+}
+
+.event-search-results-list .event-listener-event {
+ padding-inline: 20px;
+}
+
+.event-listener-name {
+ line-height: 14px;
+ padding: 3px 0;
+}
+
+.event-listener-event input {
+ margin-inline: 0 4px;
+ margin-block: 0;
+}
+
+.event-search-container {
+ display: flex;
+ border: 1px solid transparent;
+ border-block-end: 1px solid var(--theme-splitter-color);
+}
+
+.event-search-form {
+ display: flex;
+ flex-grow: 1;
+}
+
+.event-search-input {
+ flex-grow: 1;
+ margin: 0;
+ font-size: inherit;
+ background-color: var(--theme-sidebar-background);
+ border: 0;
+ height: 24px;
+ color: var(--theme-body-color);
+ background-image: url("chrome://devtools/skin/images/filter-small.svg");
+ background-position-x: 4px;
+ background-position-y: 50%;
+ background-repeat: no-repeat;
+ background-size: 12px;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+ text-align: match-parent;
+ outline-offset: -1px;
+}
+
+:root:dir(ltr) .event-search-input {
+ /* Be explicit about left/right direction to prevent the text/placeholder
+ * from overlapping the background image when the user changes the text
+ * direction manually (e.g. via Ctrl+Shift). */
+ padding-left: 19px;
+ padding-right: 12px;
+}
+
+:root:dir(rtl) .event-search-input {
+ background-position-x: right 4px;
+ padding-right: 19px;
+ padding-left: 12px;
+}
+
+.category-label {
+ color: var(--theme-comment);
+}
+
+.event-search-input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.event-search-container:focus-within {
+ border: 1px solid var(--theme-highlight-blue);
+}
+
+.devtools-searchinput-clear {
+ margin-inline-end: 8px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
new file mode 100644
index 0000000000..ce7eabf89d
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
@@ -0,0 +1,339 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+ span,
+ button,
+ form,
+ label,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+import {
+ getActiveEventListeners,
+ getEventListenerBreakpointTypes,
+ getEventListenerExpanded,
+} from "../../selectors/index";
+
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class EventListeners extends Component {
+ state = {
+ searchText: "",
+ focused: false,
+ };
+
+ static get propTypes() {
+ return {
+ activeEventListeners: PropTypes.array.isRequired,
+ addEventListenerExpanded: PropTypes.func.isRequired,
+ addEventListeners: PropTypes.func.isRequired,
+ categories: PropTypes.array.isRequired,
+ expandedCategories: PropTypes.array.isRequired,
+ removeEventListenerExpanded: PropTypes.func.isRequired,
+ removeEventListeners: PropTypes.func.isRequired,
+ };
+ }
+
+ hasMatch(eventOrCategoryName, searchText) {
+ const lowercaseEventOrCategoryName = eventOrCategoryName.toLowerCase();
+ const lowercaseSearchText = searchText.toLowerCase();
+
+ return lowercaseEventOrCategoryName.includes(lowercaseSearchText);
+ }
+
+ getSearchResults() {
+ const { searchText } = this.state;
+ const { categories } = this.props;
+ const searchResults = categories.reduce((results, cat, index) => {
+ const category = categories[index];
+
+ if (this.hasMatch(category.name, searchText)) {
+ results[category.name] = category.events;
+ } else {
+ results[category.name] = category.events.filter(event =>
+ this.hasMatch(event.name, searchText)
+ );
+ }
+
+ return results;
+ }, {});
+
+ return searchResults;
+ }
+
+ onCategoryToggle(category) {
+ const {
+ expandedCategories,
+ removeEventListenerExpanded,
+ addEventListenerExpanded,
+ } = this.props;
+
+ if (expandedCategories.includes(category)) {
+ removeEventListenerExpanded(category);
+ } else {
+ addEventListenerExpanded(category);
+ }
+ }
+
+ onCategoryClick(category, isChecked) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ const eventsIds = category.events.map(event => event.id);
+
+ if (isChecked) {
+ addEventListeners(eventsIds);
+ } else {
+ removeEventListeners(eventsIds);
+ }
+ }
+
+ onEventTypeClick(eventId, isChecked) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ if (isChecked) {
+ addEventListeners([eventId]);
+ } else {
+ removeEventListeners([eventId]);
+ }
+ }
+
+ onInputChange = event => {
+ this.setState({ searchText: event.currentTarget.value });
+ };
+
+ onKeyDown = event => {
+ if (event.key === "Escape") {
+ this.setState({ searchText: "" });
+ }
+ };
+
+ onFocus = event => {
+ this.setState({ focused: true });
+ };
+
+ onBlur = event => {
+ this.setState({ focused: false });
+ };
+
+ renderSearchInput() {
+ const { focused, searchText } = this.state;
+ const placeholder = L10N.getStr("eventListenersHeader1.placeholder");
+ return form(
+ {
+ className: "event-search-form",
+ onSubmit: e => e.preventDefault(),
+ },
+ input({
+ className: classnames("event-search-input", {
+ focused,
+ }),
+ placeholder: placeholder,
+ value: searchText,
+ onChange: this.onInputChange,
+ onKeyDown: this.onKeyDown,
+ onFocus: this.onFocus,
+ onBlur: this.onBlur,
+ })
+ );
+ }
+
+ renderClearSearchButton() {
+ const { searchText } = this.state;
+
+ if (!searchText) {
+ return null;
+ }
+ return button({
+ onClick: () =>
+ this.setState({
+ searchText: "",
+ }),
+ className: "devtools-searchinput-clear",
+ });
+ }
+
+ renderCategoriesList() {
+ const { categories } = this.props;
+ return ul(
+ {
+ className: "event-listeners-list",
+ },
+ categories.map((category, index) => {
+ return li(
+ {
+ className: "event-listener-group",
+ key: index,
+ },
+ this.renderCategoryHeading(category),
+ this.renderCategoryListing(category)
+ );
+ })
+ );
+ }
+
+ renderSearchResultsList() {
+ const searchResults = this.getSearchResults();
+ return ul(
+ {
+ className: "event-search-results-list",
+ },
+ Object.keys(searchResults).map(category => {
+ return searchResults[category].map(event => {
+ return this.renderListenerEvent(event, category);
+ });
+ })
+ );
+ }
+
+ renderCategoryHeading(category) {
+ const { activeEventListeners, expandedCategories } = this.props;
+ const { events } = category;
+
+ const expanded = expandedCategories.includes(category.name);
+ const checked = events.every(({ id }) => activeEventListeners.includes(id));
+ const indeterminate =
+ !checked && events.some(({ id }) => activeEventListeners.includes(id));
+
+ return div(
+ {
+ className: "event-listener-header",
+ },
+ button(
+ {
+ className: "event-listener-expand",
+ onClick: () => this.onCategoryToggle(category.name),
+ },
+ React.createElement(AccessibleImage, {
+ className: classnames("arrow", {
+ expanded,
+ }),
+ })
+ ),
+ label(
+ {
+ className: "event-listener-label",
+ },
+ input({
+ type: "checkbox",
+ value: category.name,
+ onChange: e => {
+ this.onCategoryClick(
+ category,
+ // Clicking an indeterminate checkbox should always have the
+ // effect of disabling any selected items.
+ indeterminate ? false : e.target.checked
+ );
+ },
+ checked: checked,
+ ref: el => el && (el.indeterminate = indeterminate),
+ }),
+ span(
+ {
+ className: "event-listener-category",
+ },
+ category.name
+ )
+ )
+ );
+ }
+
+ renderCategoryListing(category) {
+ const { expandedCategories } = this.props;
+
+ const expanded = expandedCategories.includes(category.name);
+ if (!expanded) {
+ return null;
+ }
+ return ul(
+ null,
+ category.events.map(event => {
+ return this.renderListenerEvent(event, category.name);
+ })
+ );
+ }
+
+ renderCategory(category) {
+ return span(
+ {
+ className: "category-label",
+ },
+ category,
+ " \u25B8 "
+ );
+ }
+
+ renderListenerEvent(event, category) {
+ const { activeEventListeners } = this.props;
+ const { searchText } = this.state;
+ return li(
+ {
+ className: "event-listener-event",
+ key: event.id,
+ },
+ label(
+ {
+ className: "event-listener-label",
+ },
+ input({
+ type: "checkbox",
+ value: event.id,
+ onChange: e => this.onEventTypeClick(event.id, e.target.checked),
+ checked: activeEventListeners.includes(event.id),
+ }),
+ span(
+ {
+ className: "event-listener-name",
+ },
+ searchText ? this.renderCategory(category) : null,
+ event.name
+ )
+ )
+ );
+ }
+
+ render() {
+ const { searchText } = this.state;
+ return div(
+ {
+ className: "event-listeners",
+ },
+ div(
+ {
+ className: "event-search-container",
+ },
+ this.renderSearchInput(),
+ this.renderClearSearchButton()
+ ),
+ div(
+ {
+ className: "event-listeners-content",
+ },
+ searchText
+ ? this.renderSearchResultsList()
+ : this.renderCategoriesList()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ activeEventListeners: getActiveEventListeners(state),
+ categories: getEventListenerBreakpointTypes(state),
+ expandedCategories: getEventListenerExpanded(state),
+});
+
+export default connect(mapStateToProps, {
+ addEventListeners: actions.addEventListenerBreakpoints,
+ removeEventListeners: actions.removeEventListenerBreakpoints,
+ addEventListenerExpanded: actions.addEventListenerExpanded,
+ removeEventListenerExpanded: actions.removeEventListenerExpanded,
+})(EventListeners);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css
new file mode 100644
index 0000000000..5dcf2622b5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css
@@ -0,0 +1,171 @@
+/* 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/>. */
+
+.expression-input-form {
+ width: 100%;
+}
+
+.input-expression {
+ width: 100%;
+ margin: 0;
+ font-size: inherit;
+ border: 1px;
+ background-color: var(--theme-sidebar-background);
+ height: 24px;
+ padding-inline-start: 19px;
+ padding-inline-end: 12px;
+ color: var(--theme-body-color);
+ outline: 0;
+}
+
+@keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+ 20%,
+ 60% {
+ transform: translateX(-10px);
+ }
+ 40%,
+ 80% {
+ transform: translateX(10px);
+ }
+}
+
+.input-expression::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.input-expression:focus {
+ cursor: text;
+}
+
+.expressions-list .expression-input-container {
+ height: var(--expression-item-height);
+}
+
+.expressions-list .input-expression {
+ /* Prevent vertical bounce when editing an existing Watch Expression */
+ height: 100%;
+}
+
+.expressions-list {
+ /* TODO: add normalize */
+ margin: 0;
+ padding: 4px 0px;
+ overflow-x: auto;
+}
+
+.expression-input-container {
+ display: flex;
+ border: 1px solid transparent;
+}
+
+.expression-input-container.focused {
+ border: 1px solid var(--theme-highlight-blue);
+}
+
+:root.theme-dark .expression-input-container.focused {
+ border: 1px solid var(--blue-50);
+}
+
+.expression-container {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+ width: 100%;
+ color: var(--theme-body-color);
+ background-color: var(--theme-body-background);
+ display: block;
+ position: relative;
+ overflow: hidden;
+}
+
+.expression-container > .tree {
+ width: 100%;
+ overflow: hidden;
+}
+
+.expression-container .tree .tree-node[aria-level="1"] {
+ padding-top: 0px;
+ /* keep line-height at 14px to prevent row from shifting upon expansion */
+ line-height: 14px;
+}
+
+.expression-container .tree-node[aria-level="1"] .object-label {
+ font-family: var(--monospace-font-family);
+}
+
+:root.theme-light .expression-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+:root.theme-dark .expression-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.tree .tree-node:not(.focused):hover {
+ background-color: transparent;
+}
+
+.expression-container__close-btn {
+ position: absolute;
+ /* hiding button outside of row until hovered or focused */
+ top: -100px;
+}
+
+.expression-container:hover .expression-container__close-btn,
+.expression-container:focus-within .expression-container__close-btn,
+.expression-container__close-btn:focus-within {
+ top: 0;
+}
+
+.expression-content .object-node {
+ padding-inline-start: 0px;
+ cursor: default;
+}
+
+.expressions-list .tree.object-inspector .node.object-node {
+ max-width: calc(100% - 20px);
+ min-width: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.expression-container__close-btn {
+ max-height: 16px;
+ padding-inline-start: 4px;
+}
+
+[dir="ltr"] .expression-container__close-btn {
+ right: 0;
+}
+
+[dir="rtl"] .expression-container__close-btn {
+ left: 0;
+}
+
+.expression-content {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+ position: relative;
+}
+
+.expression-content .tree {
+ overflow: hidden;
+ flex-grow: 1;
+ line-height: 15px;
+}
+
+.expression-content .tree-node[data-expandable="false"][aria-level="1"] {
+ padding-inline-start: 0px;
+}
+
+.input-expression:not(:placeholder-shown) {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
new file mode 100644
index 0000000000..be05c7327c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
@@ -0,0 +1,486 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+ form,
+ datalist,
+ option,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { features } from "../../utils/prefs";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+import actions from "../../actions/index";
+import {
+ getExpressions,
+ getAutocompleteMatchset,
+ getSelectedSource,
+ isMapScopesEnabled,
+ getIsCurrentThreadPaused,
+ getSelectedFrame,
+ getOriginalFrameScope,
+ getCurrentThread,
+} from "../../selectors/index";
+import { getExpressionResultGripAndFront } from "../../utils/expressions";
+
+import { CloseButton } from "../shared/Button/index";
+
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const { ObjectInspector } = objectInspector;
+
+class Expressions extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ focused: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ addExpression: PropTypes.func.isRequired,
+ autocomplete: PropTypes.func.isRequired,
+ autocompleteMatches: PropTypes.array,
+ clearAutocomplete: PropTypes.func.isRequired,
+ deleteExpression: PropTypes.func.isRequired,
+ expressions: PropTypes.array.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ onExpressionAdded: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openLink: PropTypes.any.isRequired,
+ showInput: PropTypes.bool.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ updateExpression: PropTypes.func.isRequired,
+ isOriginalVariableMappingDisabled: PropTypes.bool,
+ isLoadingOriginalVariables: PropTypes.bool,
+ };
+ }
+
+ componentDidMount() {
+ const { showInput } = this.props;
+
+ // Ensures that the input is focused when the "+"
+ // is clicked while the panel is collapsed
+ if (showInput && this._input) {
+ this._input.focus();
+ }
+ }
+
+ clear = () => {
+ this.setState(() => ({
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ focused: false,
+ }));
+ };
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (this.state.editing) {
+ this.clear();
+ }
+
+ // Ensures that the add watch expression input
+ // is no longer visible when the new watch expression is rendered
+ if (this.props.expressions.length < nextProps.expressions.length) {
+ this.hideInput();
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const { editing, inputValue, focused } = this.state;
+ const {
+ expressions,
+ showInput,
+ autocompleteMatches,
+ isLoadingOriginalVariables,
+ isOriginalVariableMappingDisabled,
+ } = this.props;
+
+ return (
+ autocompleteMatches !== nextProps.autocompleteMatches ||
+ expressions !== nextProps.expressions ||
+ isLoadingOriginalVariables !== nextProps.isLoadingOriginalVariables ||
+ isOriginalVariableMappingDisabled !==
+ nextProps.isOriginalVariableMappingDisabled ||
+ editing !== nextState.editing ||
+ inputValue !== nextState.inputValue ||
+ nextProps.showInput !== showInput ||
+ focused !== nextState.focused
+ );
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const _input = this._input;
+
+ if (!_input) {
+ return;
+ }
+
+ if (!prevState.editing && this.state.editing) {
+ _input.setSelectionRange(0, _input.value.length);
+ _input.focus();
+ } else if (this.props.showInput && !this.state.focused) {
+ _input.focus();
+ }
+ }
+
+ editExpression(expression, index) {
+ this.setState({
+ inputValue: expression.input,
+ editing: true,
+ editIndex: index,
+ });
+ }
+
+ deleteExpression(e, expression) {
+ e.stopPropagation();
+ const { deleteExpression } = this.props;
+ deleteExpression(expression);
+ }
+
+ handleChange = e => {
+ const { target } = e;
+ if (features.autocompleteExpression) {
+ this.findAutocompleteMatches(target.value, target.selectionStart);
+ }
+ this.setState({ inputValue: target.value });
+ };
+
+ findAutocompleteMatches = debounce((value, selectionStart) => {
+ const { autocomplete } = this.props;
+ autocomplete(value, selectionStart);
+ }, 250);
+
+ handleKeyDown = e => {
+ if (e.key === "Escape") {
+ this.clear();
+ }
+ };
+
+ hideInput = () => {
+ this.setState({ focused: false });
+ this.props.onExpressionAdded();
+ };
+
+ createElement = element => {
+ return document.createElement(element);
+ };
+
+ onFocus = () => {
+ this.setState({ focused: true });
+ };
+
+ onBlur() {
+ this.clear();
+ this.hideInput();
+ }
+
+ handleExistingSubmit = async (e, expression) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.props.updateExpression(this.state.inputValue, expression);
+ };
+
+ handleNewSubmit = async e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ await this.props.addExpression(this.state.inputValue);
+ this.setState({
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ });
+
+ this.props.clearAutocomplete();
+ };
+
+ renderExpressionsNotification() {
+ const { isOriginalVariableMappingDisabled, isLoadingOriginalVariables } =
+ this.props;
+
+ if (isOriginalVariableMappingDisabled) {
+ return div(
+ {
+ className: "pane-info no-original-scopes-info",
+ "aria-role": "status",
+ },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ span(
+ { className: "message" },
+ L10N.getStr("expressions.noOriginalScopes")
+ )
+ );
+ }
+
+ if (isLoadingOriginalVariables) {
+ return div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ span(
+ { className: "message" },
+ L10N.getStr("scopes.loadingOriginalScopes")
+ )
+ );
+ }
+ return null;
+ }
+
+ renderExpression = (expression, index) => {
+ const {
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const { editing, editIndex } = this.state;
+ const { input: _input, updating } = expression;
+ const isEditingExpr = editing && editIndex === index;
+ if (isEditingExpr) {
+ return this.renderExpressionEditInput(expression);
+ }
+
+ if (updating) {
+ return null;
+ }
+
+ const { expressionResultGrip, expressionResultFront } =
+ getExpressionResultGripAndFront(expression);
+
+ const root = {
+ name: expression.input,
+ path: _input,
+ contents: {
+ value: expressionResultGrip,
+ front: expressionResultFront,
+ },
+ };
+
+ return li(
+ {
+ className: "expression-container",
+ key: _input,
+ title: expression.input,
+ },
+ div(
+ {
+ className: "expression-content",
+ },
+ React.createElement(ObjectInspector, {
+ roots: [root],
+ autoExpandDepth: 0,
+ disableWrap: true,
+ openLink: openLink,
+ createElement: this.createElement,
+ onDoubleClick: (items, { depth }) => {
+ if (depth === 0) {
+ this.editExpression(expression, index);
+ }
+ },
+ onDOMNodeClick: grip => openElementInInspector(grip),
+ onInspectIconClick: grip => openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
+ shouldRenderTooltip: true,
+ mayUseCustomFormatter: true,
+ }),
+ div(
+ {
+ className: "expression-container__close-btn",
+ },
+ React.createElement(CloseButton, {
+ handleClick: e => this.deleteExpression(e, expression),
+ tooltip: L10N.getStr("expressions.remove.tooltip"),
+ })
+ )
+ )
+ );
+ };
+
+ renderExpressions() {
+ const { expressions, showInput } = this.props;
+ return React.createElement(
+ React.Fragment,
+ null,
+ ul(
+ {
+ className: "pane expressions-list",
+ },
+ expressions.map(this.renderExpression)
+ ),
+ showInput && this.renderNewExpressionInput()
+ );
+ }
+
+ renderAutoCompleteMatches() {
+ if (!features.autocompleteExpression) {
+ return null;
+ }
+ const { autocompleteMatches } = this.props;
+ if (autocompleteMatches) {
+ return datalist(
+ {
+ id: "autocomplete-matches",
+ },
+ autocompleteMatches.map((match, index) => {
+ return option({
+ key: index,
+ value: match,
+ });
+ })
+ );
+ }
+ return datalist({
+ id: "autocomplete-matches",
+ });
+ }
+
+ renderNewExpressionInput() {
+ const { editing, inputValue, focused } = this.state;
+ return form(
+ {
+ className: classnames(
+ "expression-input-container expression-input-form",
+ { focused }
+ ),
+ onSubmit: this.handleNewSubmit,
+ },
+ input({
+ className: "input-expression",
+ type: "text",
+ placeholder: L10N.getStr("expressions.placeholder"),
+ onChange: this.handleChange,
+ onBlur: this.hideInput,
+ onKeyDown: this.handleKeyDown,
+ onFocus: this.onFocus,
+ value: !editing ? inputValue : "",
+ ref: c => (this._input = c),
+ ...(features.autocompleteExpression && {
+ list: "autocomplete-matches",
+ }),
+ }),
+ this.renderAutoCompleteMatches(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ renderExpressionEditInput(expression) {
+ const { inputValue, editing, focused } = this.state;
+ return form(
+ {
+ key: expression.input,
+ className: classnames(
+ "expression-input-container expression-input-form",
+ {
+ focused,
+ }
+ ),
+ onSubmit: e => this.handleExistingSubmit(e, expression),
+ },
+ input({
+ className: "input-expression",
+ type: "text",
+ onChange: this.handleChange,
+ onBlur: this.clear,
+ onKeyDown: this.handleKeyDown,
+ onFocus: this.onFocus,
+ value: editing ? inputValue : expression.input,
+ ref: c => (this._input = c),
+ ...(features.autocompleteExpression && {
+ list: "autocomplete-matches",
+ }),
+ }),
+ this.renderAutoCompleteMatches(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ render() {
+ const { expressions } = this.props;
+
+ return div(
+ { className: "pane" },
+ this.renderExpressionsNotification(),
+ expressions.length === 0
+ ? this.renderNewExpressionInput()
+ : this.renderExpressions()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedFrame = getSelectedFrame(state, getCurrentThread(state));
+ const selectedSource = getSelectedSource(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const mapScopesEnabled = isMapScopesEnabled(state);
+ const expressions = getExpressions(state);
+
+ const selectedSourceIsNonPrettyPrintedOriginal =
+ selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted;
+
+ let isOriginalVariableMappingDisabled, isLoadingOriginalVariables;
+
+ if (selectedSourceIsNonPrettyPrintedOriginal) {
+ isOriginalVariableMappingDisabled = isPaused && !mapScopesEnabled;
+ isLoadingOriginalVariables =
+ isPaused &&
+ mapScopesEnabled &&
+ !expressions.length &&
+ !getOriginalFrameScope(state, selectedFrame)?.scope;
+ }
+
+ return {
+ isOriginalVariableMappingDisabled,
+ isLoadingOriginalVariables,
+ autocompleteMatches: getAutocompleteMatchset(state),
+ expressions,
+ };
+};
+
+export default connect(mapStateToProps, {
+ autocomplete: actions.autocomplete,
+ clearAutocomplete: actions.clearAutocomplete,
+ addExpression: actions.addExpression,
+ updateExpression: actions.updateExpression,
+ deleteExpression: actions.deleteExpression,
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+})(Expressions);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js
new file mode 100644
index 0000000000..0c81d8afb4
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js
@@ -0,0 +1,232 @@
+/* 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/>. */
+
+import React, { Component, memo } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+import { formatDisplayName } from "../../../utils/pause/frames/index";
+import { getFilename, getFileURL } from "../../../utils/source";
+import FrameIndent from "./FrameIndent";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function FrameTitle({ frame, options = {}, l10n }) {
+ const displayName = formatDisplayName(frame, options, l10n);
+ return React.createElement(
+ "span",
+ {
+ className: "title",
+ },
+ displayName
+ );
+}
+
+FrameTitle.propTypes = {
+ frame: PropTypes.object.isRequired,
+ options: PropTypes.object.isRequired,
+ l10n: PropTypes.object.isRequired,
+ showFrameContextMenu: PropTypes.func.isRequired,
+};
+
+function getFrameLocation(frame, shouldDisplayOriginalLocation) {
+ if (shouldDisplayOriginalLocation) {
+ return frame.location;
+ }
+ return frame.generatedLocation || frame.location;
+}
+const FrameLocation = memo(
+ ({ frame, displayFullUrl = false, shouldDisplayOriginalLocation }) => {
+ if (frame.library) {
+ return React.createElement(
+ "span",
+ {
+ className: "location",
+ },
+ frame.library,
+ React.createElement(AccessibleImage, {
+ className: `annotation-logo ${frame.library.toLowerCase()}`,
+ })
+ );
+ }
+ const location = getFrameLocation(frame, shouldDisplayOriginalLocation);
+ const filename = displayFullUrl
+ ? getFileURL(location.source, false)
+ : getFilename(location.source);
+ return React.createElement(
+ "span",
+ {
+ className: "location",
+ title: location.source.url,
+ },
+ React.createElement(
+ "span",
+ {
+ className: "filename",
+ },
+ filename
+ ),
+ ":",
+ React.createElement(
+ "span",
+ {
+ className: "line",
+ },
+ location.line
+ )
+ );
+ }
+);
+FrameLocation.displayName = "FrameLocation";
+
+FrameLocation.propTypes = {
+ frame: PropTypes.object.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+};
+
+export default class FrameComponent extends Component {
+ static defaultProps = {
+ hideLocation: false,
+ shouldMapDisplayName: true,
+ disableContextMenu: false,
+ };
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ frame: PropTypes.object.isRequired,
+ getFrameTitle: PropTypes.func,
+ hideLocation: PropTypes.bool.isRequired,
+ isInGroup: PropTypes.bool,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectedFrame: PropTypes.object,
+ shouldMapDisplayName: PropTypes.bool.isRequired,
+ shouldDisplayOriginalLocation: PropTypes.bool.isRequired,
+ showFrameContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ get isDebugger() {
+ return this.props.panel == "debugger";
+ }
+
+ onContextMenu(event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const { frame } = this.props;
+ this.props.showFrameContextMenu(event, frame);
+ }
+
+ onMouseDown(e, frame, selectedFrame) {
+ if (e.button !== 0) {
+ return;
+ }
+
+ this.props.selectFrame(frame);
+ }
+
+ onKeyUp(event, frame, selectedFrame) {
+ if (event.key != "Enter") {
+ return;
+ }
+
+ this.props.selectFrame(frame);
+ }
+
+ render() {
+ const {
+ frame,
+ selectedFrame,
+ hideLocation,
+ shouldMapDisplayName,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ shouldDisplayOriginalLocation,
+ isInGroup,
+ } = this.props;
+ const { l10n } = this.context;
+
+ const className = classnames("frame", {
+ selected: selectedFrame && selectedFrame.id === frame.id,
+ });
+
+ const location = getFrameLocation(frame, shouldDisplayOriginalLocation);
+ const title = getFrameTitle
+ ? getFrameTitle(`${getFileURL(location.source, false)}:${location.line}`)
+ : undefined;
+ return React.createElement(
+ "div",
+ {
+ role: "listitem",
+ key: frame.id,
+ className: className,
+ onMouseDown: e => this.onMouseDown(e, frame, selectedFrame),
+ onKeyUp: e => this.onKeyUp(e, frame, selectedFrame),
+ onContextMenu: disableContextMenu ? null : e => this.onContextMenu(e),
+ tabIndex: 0,
+ title: title,
+ },
+ frame.asyncCause &&
+ React.createElement(
+ "span",
+ {
+ className: "location-async-cause",
+ },
+ this.isSelectable && React.createElement(FrameIndent, null),
+ this.isDebugger
+ ? React.createElement(
+ "span",
+ {
+ className: "async-label",
+ },
+ frame.asyncCause
+ )
+ : l10n.getFormatStr("stacktrace.asyncStack", frame.asyncCause),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ ),
+ this.isSelectable &&
+ React.createElement(FrameIndent, {
+ indentLevel: isInGroup ? 2 : 1,
+ }),
+ React.createElement(FrameTitle, {
+ frame,
+ options: {
+ shouldMapDisplayName,
+ },
+ l10n,
+ }),
+ !hideLocation &&
+ React.createElement(
+ "span",
+ {
+ className: "clipboard-only",
+ },
+ " "
+ ),
+ !hideLocation &&
+ React.createElement(FrameLocation, {
+ frame,
+ displayFullUrl,
+ shouldDisplayOriginalLocation,
+ }),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ );
+ }
+}
+
+FrameComponent.displayName = "Frame";
+FrameComponent.contextTypes = { l10n: PropTypes.object };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js
new file mode 100644
index 0000000000..7eee92ffd1
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js
@@ -0,0 +1,18 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+
+export default function FrameIndent({ indentLevel = 1 } = {}) {
+ // \xA0 represents the non breakable space &nbsp;
+ const indentWidth = 4 * indentLevel;
+ const nonBreakableSpaces = "\xA0".repeat(indentWidth);
+ return React.createElement(
+ "span",
+ {
+ className: "frame-indent clipboard-only",
+ },
+ nonBreakableSpaces
+ );
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css
new file mode 100644
index 0000000000..5f57f97e51
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css
@@ -0,0 +1,185 @@
+/* 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/>. */
+
+.frames [role="list"] {
+ list-style: none;
+ margin: 0;
+ padding: 4px 0;
+}
+
+.frames [role="list"] [role="listitem"] {
+ padding-bottom: 2px;
+ overflow: hidden;
+ display: flex;
+ justify-content: space-between;
+ column-gap: 0.5em;
+ flex-direction: row;
+ align-items: center;
+ margin: 0;
+ max-width: 100%;
+ flex-wrap: wrap;
+}
+
+.frames [role="list"] [role="listitem"] * {
+ user-select: none;
+}
+
+.frames .badge {
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.frames .location {
+ font-weight: normal;
+ margin: 0;
+ flex-grow: 1;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ /* Trick to get the ellipsis at the start of the string */
+ text-overflow: ellipsis;
+ direction: rtl;
+}
+
+.call-stack-pane:dir(ltr) .frames .location {
+ padding-right: 10px;
+ text-align: right;
+}
+
+.call-stack-pane:dir(rtl) .frames .location {
+ padding-left: 10px;
+ text-align: left;
+}
+
+.call-stack-pane .location-async-cause {
+ color: var(--theme-comment);
+}
+
+.theme-light .frames .location {
+ color: var(--theme-comment);
+}
+
+:root.theme-dark .frames .location {
+ color: var(--theme-body-color);
+ opacity: 0.6;
+}
+
+.frames .title {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-inline-start: 10px;
+}
+
+.frames-group .title {
+ padding-inline-start: 40px;
+}
+
+.frames [role="list"] [role="listitem"]:hover,
+.frames [role="list"] [role="listitem"]:focus {
+ background-color: var(--theme-toolbar-background-alt);
+}
+
+.frames [role="list"] [role="listitem"]:hover .location-async-cause,
+.frames [role="list"] [role="listitem"]:focus .location-async-cause,
+.frames [role="list"] [role="listitem"]:hover .async-label,
+.frames [role="list"] [role="listitem"]:focus .async-label {
+ background-color: var(--theme-body-background);
+}
+
+.theme-dark .frames [role="list"] [role="listitem"]:focus,
+.theme-dark .frames [role="list"] [role="listitem"]:focus .async-label,
+.theme-dark .frames [role="list"] [role="listitem"]:focus .async-label {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.frames [role="list"] [role="listitem"].selected,
+.frames [role="list"] [role="listitem"].selected .async-label {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.frames [role="list"] [role="listitem"].selected i.annotation-logo svg path {
+ fill: white;
+}
+
+:root.theme-light .frames [role="list"] [role="listitem"].selected .location,
+:root.theme-dark .frames [role="list"] [role="listitem"].selected .location {
+ color: white;
+}
+
+.frames .show-more-container {
+ display: flex;
+ min-height: 24px;
+ padding: 4px 0;
+}
+
+.frames .show-more {
+ text-align: center;
+ padding: 8px 0px;
+ margin: 7px 10px 7px 7px;
+ border: 1px solid var(--theme-splitter-color);
+ background-color: var(--theme-tab-toolbar-background);
+ width: 100%;
+ font-size: inherit;
+ color: inherit;
+}
+
+.frames .show-more:hover {
+ background-color: var(--theme-toolbar-background-hover);
+}
+
+.frames .img.annotation-logo {
+ margin-inline-end: 4px;
+ background-color: currentColor;
+}
+
+/*
+ * We also show the library icon in locations, which are forced to RTL.
+ */
+.frames .location .img.annotation-logo {
+ margin-inline-start: 4px;
+}
+
+/* Some elements are added to the DOM only to be printed into the clipboard
+ when the user copy some elements. We don't want those elements to mess with
+ the layout so we put them outside of the screen
+*/
+.frames .clipboard-only {
+ position: absolute;
+ left: -9999px;
+}
+
+.call-stack-pane [role="listitem"] .location-async-cause {
+ height: 20px;
+ line-height: 20px;
+ color: var(--theme-icon-dimmed-color);
+ display: block;
+ z-index: 4;
+ position: relative;
+ padding-inline-start: 17px;
+ width: 100%;
+ pointer-events: none;
+}
+
+.frames-group .location-async-cause {
+ padding-inline-start: 47px;
+}
+
+.call-stack-pane [role="listitem"] .location-async-cause::after {
+ content: " ";
+ position: absolute;
+ left: 0;
+ z-index: -1;
+ height: 30px;
+ top: 50%;
+ width: 100%;
+ border-top: 1px solid var(--theme-tab-toolbar-background);;
+}
+
+.call-stack-pane .async-label {
+ z-index: 1;
+ background-color: var(--theme-sidebar-background);
+ padding: 0 3px;
+ display: inline-block;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css
new file mode 100644
index 0000000000..14dbea9954
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css
@@ -0,0 +1,38 @@
+/* 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/>. */
+
+.frames-group .group,
+.frames-group .group .location {
+ font-weight: 500;
+ cursor: default;
+ /*
+ * direction:rtl is set in Frames.css to overflow the location text from the
+ * start. Here we need to reset it in order to display the framework icon
+ * after the framework name.
+ */
+ direction: ltr;
+}
+
+.frames-group.expanded .group,
+.frames-group.expanded .group .location {
+ color: var(--theme-highlight-blue);
+}
+
+.frames-group .frames-list {
+ border-top: 1px solid var(--theme-splitter-color);
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.frames-group.expanded .badge {
+ color: var(--theme-highlight-blue);
+}
+
+.frames-group .img.arrow {
+ margin-inline-start: -1px;
+ margin-inline-end: 4px;
+}
+
+.frames-group .group-description {
+ padding-inline-start: 6px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js
new file mode 100644
index 0000000000..ab9f7073a7
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js
@@ -0,0 +1,191 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { getLibraryFromUrl } from "../../../utils/pause/frames/index";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+import FrameComponent from "./Frame";
+import Badge from "../../shared/Badge";
+import FrameIndent from "./FrameIndent";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function FrameLocation({ frame, expanded }) {
+ const library = frame.library || getLibraryFromUrl(frame);
+ if (!library) {
+ return null;
+ }
+ const arrowClassName = classnames("arrow", {
+ expanded,
+ });
+ return React.createElement(
+ "span",
+ {
+ className: "group-description",
+ },
+ React.createElement(AccessibleImage, {
+ className: arrowClassName,
+ }),
+ React.createElement(AccessibleImage, {
+ className: `annotation-logo ${library.toLowerCase()}`,
+ }),
+ React.createElement(
+ "span",
+ {
+ className: "group-description-name",
+ },
+ library
+ )
+ );
+}
+
+FrameLocation.propTypes = {
+ expanded: PropTypes.any.isRequired,
+ frame: PropTypes.object.isRequired,
+};
+
+FrameLocation.displayName = "FrameLocation";
+
+export default class Group extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { expanded: false };
+ }
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ getFrameTitle: PropTypes.func,
+ group: PropTypes.array.isRequired,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func,
+ selectedFrame: PropTypes.object,
+ showFrameContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ onContextMenu(event) {
+ const { group } = this.props;
+ const frame = group[0];
+ this.props.showFrameContextMenu(event, frame, true);
+ }
+
+ toggleFrames = event => {
+ event.stopPropagation();
+ this.setState(prevState => ({ expanded: !prevState.expanded }));
+ };
+
+ renderFrames() {
+ const {
+ group,
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ showFrameContextMenu,
+ } = this.props;
+
+ const { expanded } = this.state;
+ if (!expanded) {
+ return null;
+ }
+
+ return React.createElement(
+ "div",
+ {
+ className: "frames-list",
+ },
+ group.map(frame =>
+ React.createElement(FrameComponent, {
+ frame: frame,
+ showFrameContextMenu: showFrameContextMenu,
+ hideLocation: true,
+ key: frame.id,
+ selectedFrame: selectedFrame,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ shouldMapDisplayName: false,
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ isInGroup: true,
+ })
+ )
+ );
+ }
+
+ renderDescription() {
+ const { l10n } = this.context;
+ const { group } = this.props;
+ const { expanded } = this.state;
+
+ const frame = group[0];
+ const l10NEntry = expanded
+ ? "callStack.group.collapseTooltip"
+ : "callStack.group.expandTooltip";
+ const title = l10n.getFormatStr(l10NEntry, frame.library);
+
+ return React.createElement(
+ "div",
+ {
+ role: "listitem",
+ key: frame.id,
+ className: "group",
+ onClick: this.toggleFrames,
+ tabIndex: 0,
+ title,
+ },
+ this.isSelectable && React.createElement(FrameIndent, null),
+ React.createElement(FrameLocation, {
+ frame,
+ expanded,
+ }),
+ this.isSelectable &&
+ React.createElement(
+ "span",
+ {
+ className: "clipboard-only",
+ },
+ " "
+ ),
+ React.createElement(Badge, { badgeText: this.props.group.length }),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ );
+ }
+
+ render() {
+ const { expanded } = this.state;
+ const { disableContextMenu } = this.props;
+ return React.createElement(
+ "div",
+ {
+ className: classnames("frames-group", {
+ expanded,
+ }),
+ onContextMenu: disableContextMenu ? null : e => this.onContextMenu(e),
+ },
+ this.renderDescription(),
+ this.renderFrames()
+ );
+ }
+}
+
+Group.displayName = "Group";
+Group.contextTypes = { l10n: PropTypes.object };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js
new file mode 100644
index 0000000000..d83b413a01
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js
@@ -0,0 +1,220 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import FrameComponent from "./Frame";
+import Group from "./Group";
+
+import actions from "../../../actions/index";
+import { collapseFrames } from "../../../utils/pause/frames/index";
+
+import {
+ getFrameworkGroupingState,
+ getSelectedFrame,
+ getCurrentThreadFrames,
+ getCurrentThread,
+ getShouldSelectOriginalLocation,
+} from "../../../selectors/index";
+
+const NUM_FRAMES_SHOWN = 7;
+
+class Frames extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showAllFrames: !!props.disableFrameTruncate,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ disableFrameTruncate: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ frames: PropTypes.array.isRequired,
+ frameworkGroupingOn: PropTypes.bool.isRequired,
+ getFrameTitle: PropTypes.func,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func,
+ selectedFrame: PropTypes.object,
+ showFrameContextMenu: PropTypes.func,
+ shouldDisplayOriginalLocation: PropTypes.bool,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const {
+ frames,
+ selectedFrame,
+ frameworkGroupingOn,
+ shouldDisplayOriginalLocation,
+ } = this.props;
+ const { showAllFrames } = this.state;
+ return (
+ frames !== nextProps.frames ||
+ selectedFrame !== nextProps.selectedFrame ||
+ showAllFrames !== nextState.showAllFrames ||
+ frameworkGroupingOn !== nextProps.frameworkGroupingOn ||
+ shouldDisplayOriginalLocation !== nextProps.shouldDisplayOriginalLocation
+ );
+ }
+
+ toggleFramesDisplay = () => {
+ this.setState(prevState => ({
+ showAllFrames: !prevState.showAllFrames,
+ }));
+ };
+
+ collapseFrames(frames) {
+ const { frameworkGroupingOn } = this.props;
+ if (!frameworkGroupingOn) {
+ return frames;
+ }
+
+ return collapseFrames(frames);
+ }
+
+ truncateFrames(frames) {
+ const numFramesToShow = this.state.showAllFrames
+ ? frames.length
+ : NUM_FRAMES_SHOWN;
+
+ return frames.slice(0, numFramesToShow);
+ }
+
+ renderFrames(frames) {
+ const {
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ shouldDisplayOriginalLocation,
+ showFrameContextMenu,
+ } = this.props;
+
+ const framesOrGroups = this.truncateFrames(this.collapseFrames(frames));
+
+ // We're not using a <ul> because it adds new lines before and after when
+ // the user copies the trace. Needed for the console which has several
+ // places where we don't want to have those new lines.
+ return React.createElement(
+ "div",
+ {
+ role: "list",
+ },
+ framesOrGroups.map(frameOrGroup =>
+ frameOrGroup.id
+ ? React.createElement(FrameComponent, {
+ frame: frameOrGroup,
+ showFrameContextMenu: showFrameContextMenu,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ selectedFrame: selectedFrame,
+ shouldDisplayOriginalLocation: shouldDisplayOriginalLocation,
+ key: String(frameOrGroup.id),
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ })
+ : React.createElement(Group, {
+ group: frameOrGroup,
+ showFrameContextMenu: showFrameContextMenu,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ selectedFrame: selectedFrame,
+ key: frameOrGroup[0].id,
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ })
+ )
+ );
+ }
+
+ renderToggleButton(frames) {
+ const { l10n } = this.context;
+ const buttonMessage = this.state.showAllFrames
+ ? l10n.getStr("callStack.collapse")
+ : l10n.getStr("callStack.expand");
+
+ frames = this.collapseFrames(frames);
+ if (frames.length <= NUM_FRAMES_SHOWN) {
+ return null;
+ }
+ return React.createElement(
+ "div",
+ {
+ className: "show-more-container",
+ },
+ React.createElement(
+ "button",
+ {
+ className: "show-more",
+ onClick: this.toggleFramesDisplay,
+ },
+ buttonMessage
+ )
+ );
+ }
+
+ render() {
+ const { frames, disableFrameTruncate } = this.props;
+
+ if (!frames) {
+ return React.createElement(
+ "div",
+ {
+ className: "pane frames",
+ },
+ React.createElement(
+ "div",
+ {
+ className: "pane-info empty",
+ },
+ L10N.getStr("callStack.notPaused")
+ )
+ );
+ }
+ return React.createElement(
+ "div",
+ {
+ className: "pane frames",
+ },
+ this.renderFrames(frames),
+ disableFrameTruncate ? null : this.renderToggleButton(frames)
+ );
+ }
+}
+
+Frames.contextTypes = { l10n: PropTypes.object };
+
+const mapStateToProps = state => ({
+ frames: getCurrentThreadFrames(state),
+ frameworkGroupingOn: getFrameworkGroupingState(state),
+ selectedFrame: getSelectedFrame(state, getCurrentThread(state)),
+ shouldDisplayOriginalLocation: getShouldSelectOriginalLocation(state),
+ disableFrameTruncate: false,
+ disableContextMenu: false,
+ displayFullUrl: false,
+});
+
+export default connect(mapStateToProps, {
+ selectFrame: actions.selectFrame,
+ selectLocation: actions.selectLocation,
+ showFrameContextMenu: actions.showFrameContextMenu,
+})(Frames);
+
+// Export the non-connected component in order to use it outside of the debugger
+// panel (e.g. console, netmonitor, …).
+export { Frames };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build
new file mode 100644
index 0000000000..54c188ed98
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += []
+
+CompiledModules(
+ "Frame.js",
+ "FrameIndent.js",
+ "Group.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
new file mode 100644
index 0000000000..e0dfc58a98
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
@@ -0,0 +1,110 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow, mount } from "enzyme";
+import Frame from "../Frame.js";
+import { makeMockFrame, makeMockSource } from "../../../../utils/test-mockup";
+
+function frameProperties(frame, selectedFrame, overrides = {}) {
+ return {
+ frame,
+ selectedFrame,
+ copyStackTrace: jest.fn(),
+ contextTypes: {},
+ selectFrame: jest.fn(),
+ selectLocation: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ displayFullUrl: false,
+ frameworkGroupingOn: false,
+ panel: "webconsole",
+ toggleFrameworkGrouping: null,
+ restart: jest.fn(),
+ ...overrides,
+ };
+}
+
+function render(frameToSelect = {}, overrides = {}, propsOverrides = {}) {
+ const source = makeMockSource("foo-view.js");
+ const defaultFrame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const frame = { ...defaultFrame, ...overrides };
+ const selectedFrame = { ...frame, ...frameToSelect };
+
+ const props = frameProperties(frame, selectedFrame, propsOverrides);
+ const component = shallow(React.createElement(Frame, props));
+ return { component, props };
+}
+
+describe("Frame", () => {
+ it("user frame", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("user frame (not selected)", () => {
+ const { component } = render({ id: "2" });
+ expect(component).toMatchSnapshot();
+ });
+
+ it("library frame", () => {
+ const source = makeMockSource("backbone.js");
+ const backboneFrame = {
+ ...makeMockFrame("3", source, undefined, 12, "updateEvents"),
+ library: "backbone",
+ };
+
+ const { component } = render({ id: "3" }, backboneFrame);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("filename only", () => {
+ const source = makeMockSource(
+ "https://firefox.com/assets/src/js/foo-view.js"
+ );
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null);
+ const component = mount(React.createElement(Frame, props));
+ expect(component.text()).toBe("    renderFoo foo-view.js:10");
+ });
+
+ it("full URL", () => {
+ const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null, { displayFullUrl: true });
+ const component = mount(React.createElement(Frame, props));
+ expect(component.text()).toBe(`    renderFoo ${url}:10`);
+ });
+
+ it("renders asyncCause", () => {
+ const url = `https://example.com/async.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "timeoutFn");
+ frame.asyncCause = "setTimeout handler";
+
+ const props = frameProperties(frame);
+ const component = mount(React.createElement(Frame, props), {
+ context: { l10n: L10N },
+ });
+ expect(component.find(".location-async-cause").text()).toBe(
+ `    (Async: setTimeout handler)`
+ );
+ });
+
+ it("getFrameTitle", () => {
+ const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null, {
+ getFrameTitle: x => `Jump to ${x}`,
+ });
+ const component = shallow(React.createElement(Frame, props));
+ expect(component.prop("title")).toBe(`Jump to ${url}:10`);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
new file mode 100644
index 0000000000..240e455f75
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
@@ -0,0 +1,270 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { mount, shallow } from "enzyme";
+import Frames from "../index.js";
+
+function render(overrides = {}) {
+ const defaultProps = {
+ frames: null,
+ selectedFrame: null,
+ frameworkGroupingOn: false,
+ toggleFrameworkGrouping: jest.fn(),
+ contextTypes: {},
+ selectFrame: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ };
+
+ const props = { ...defaultProps, ...overrides };
+ const component = shallow(
+ React.createElement(Frames.WrappedComponent, props),
+ {
+ context: {
+ l10n: L10N,
+ },
+ }
+ );
+
+ return component;
+}
+
+describe("Frames", () => {
+ describe("Supports different number of frames", () => {
+ it("empty frames", () => {
+ const component = render();
+ expect(component).toMatchSnapshot();
+ expect(component.find(".show-more").exists()).toBeFalsy();
+ });
+
+ it("one frame", () => {
+ const frames = [{ id: 1 }];
+ const selectedFrame = frames[0];
+ const component = render({ frames, selectedFrame });
+
+ expect(component.find(".show-more").exists()).toBeFalsy();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("toggling the show more button", () => {
+ const frames = [
+ { id: 1 },
+ { id: 2 },
+ { id: 3 },
+ { id: 4 },
+ { id: 5 },
+ { id: 6 },
+ { id: 7 },
+ { id: 8 },
+ { id: 9 },
+ { id: 10 },
+ ];
+
+ const selectedFrame = frames[0];
+ const component = render({ selectedFrame, frames });
+
+ const getToggleBtn = () => component.find(".show-more");
+ const getFrames = () => component.find("Frame");
+
+ expect(getToggleBtn().text()).toEqual("Expand rows");
+ expect(getFrames()).toHaveLength(7);
+
+ getToggleBtn().simulate("click");
+ expect(getToggleBtn().text()).toEqual("Collapse rows");
+ expect(getFrames()).toHaveLength(10);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("disable frame truncation", () => {
+ const framesNumber = 20;
+ const frames = Array.from({ length: framesNumber }, (_, i) => ({
+ id: i + 1,
+ }));
+
+ const component = render({
+ frames,
+ disableFrameTruncate: true,
+ });
+
+ const getToggleBtn = () => component.find(".show-more");
+ const getFrames = () => component.find("Frame");
+
+ expect(getToggleBtn().exists()).toBeFalsy();
+ expect(getFrames()).toHaveLength(framesNumber);
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it("shows the full URL", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+
+ const component = mount(
+ <Frames.WrappedComponent
+ frames={frames}
+ disableFrameTruncate={true}
+ displayFullUrl={true}
+ />
+ );
+ expect(component.text()).toBe(
+ "renderFoo http://myfile.com/mahscripts.js:55"
+ );
+ });
+
+ it("passes the getFrameTitle prop to the Frame component", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+ const getFrameTitle = () => {};
+ const component = render({ frames, getFrameTitle });
+
+ expect(component.find("Frame").prop("getFrameTitle")).toBe(getFrameTitle);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("passes the getFrameTitle prop to the Group component", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ {
+ id: 2,
+ library: "back",
+ displayName: "a",
+ location: {
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ line: 55,
+ },
+ },
+ {
+ id: 3,
+ library: "back",
+ displayName: "b",
+ location: {
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+ const getFrameTitle = () => {};
+ const component = render({
+ frames,
+ getFrameTitle,
+ frameworkGroupingOn: true,
+ });
+
+ expect(component.find("Group").prop("getFrameTitle")).toBe(getFrameTitle);
+ });
+ });
+
+ describe("Library Frames", () => {
+ it("toggling framework frames", () => {
+ const frames = [
+ { id: 1, location: { source: {} } },
+ { id: 2, library: "back", location: { source: {} } },
+ { id: 3, library: "back", location: { source: {} } },
+ { id: 8, location: { source: {} } },
+ ];
+
+ const selectedFrame = frames[0];
+ const frameworkGroupingOn = false;
+ const component = render({ frames, frameworkGroupingOn, selectedFrame });
+
+ expect(component.find("Frame")).toHaveLength(4);
+ expect(component).toMatchSnapshot();
+
+ component.setProps({ frameworkGroupingOn: true });
+
+ expect(component.find("Frame")).toHaveLength(2);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("groups all the Webpack-related frames", () => {
+ const frames = [
+ { id: "1-appFrame", location: { source: {} } },
+ {
+ id: "2-webpackBootstrapFrame",
+ location: {
+ source: {
+ url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ {
+ id: "3-webpackBundleFrame",
+ location: { source: { url: "https://foo.com/bundle.js" } },
+ },
+ {
+ id: "4-webpackBootstrapFrame",
+ location: {
+ source: {
+ url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ {
+ id: "5-webpackBundleFrame",
+ location: { source: { url: "https://foo.com/bundle.js" } },
+ },
+ ];
+ const selectedFrame = frames[0];
+ const frameworkGroupingOn = true;
+ const component = render({ frames, frameworkGroupingOn, selectedFrame });
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it("selectable framework frames", () => {
+ const frames = [
+ { id: 1, location: { source: {} } },
+ { id: 2, library: "back", location: { source: {} } },
+ { id: 3, library: "back", location: { source: {} } },
+ { id: 8, location: { source: {} } },
+ ];
+
+ const selectedFrame = frames[0];
+
+ const component = render({
+ frames,
+ frameworkGroupingOn: false,
+ selectedFrame,
+ selectable: true,
+ });
+ expect(component).toMatchSnapshot();
+
+ component.setProps({ frameworkGroupingOn: true });
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js
new file mode 100644
index 0000000000..8d08fa0aed
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js
@@ -0,0 +1,93 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import Group from "../Group.js";
+import { makeMockFrame, makeMockSource } from "../../../../utils/test-mockup";
+
+function render(overrides = {}) {
+ const frame = { ...makeMockFrame(), displayName: "foo", library: "Back" };
+ const defaultProps = {
+ group: [frame],
+ selectedFrame: frame,
+ frameworkGroupingOn: true,
+ toggleFrameworkGrouping: jest.fn(),
+ selectFrame: jest.fn(),
+ selectLocation: jest.fn(),
+ copyStackTrace: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ disableContextMenu: false,
+ displayFullUrl: false,
+ panel: "webconsole",
+ restart: jest.fn(),
+ };
+
+ const props = { ...defaultProps, ...overrides };
+ const component = shallow(React.createElement(Group, props), {
+ context: { l10n: L10N },
+ });
+ return { component, props };
+}
+
+describe("Group", () => {
+ it("displays a group", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("passes the getFrameTitle prop to the Frame components", () => {
+ const mahscripts = makeMockSource("http://myfile.com/mahscripts.js");
+ const back = makeMockSource("http://myfile.com/back.js");
+ const group = [
+ {
+ ...makeMockFrame("1", mahscripts, undefined, 55, "renderFoo"),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("2", back, undefined, 55, "a"),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("3", back, undefined, 55, "b"),
+ library: "Back",
+ },
+ ];
+ const getFrameTitle = () => {};
+ const { component } = render({ group, getFrameTitle });
+
+ component.setState({ expanded: true });
+
+ const frameComponents = component.find("Frame");
+ expect(frameComponents).toHaveLength(3);
+ frameComponents.forEach(node => {
+ expect(node.prop("getFrameTitle")).toBe(getFrameTitle);
+ });
+ expect(component).toMatchSnapshot();
+ });
+
+ it("renders group with anonymous functions", () => {
+ const mahscripts = makeMockSource("http://myfile.com/mahscripts.js");
+ const back = makeMockSource("http://myfile.com/back.js");
+ const group = [
+ {
+ ...makeMockFrame("1", mahscripts, undefined, 55),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("2", back, undefined, 55),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("3", back, undefined, 55),
+ library: "Back",
+ },
+ ];
+
+ const { component } = render({ group });
+ expect(component).toMatchSnapshot();
+ component.setState({ expanded: true });
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap
new file mode 100644
index 0000000000..90a5b1f906
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap
@@ -0,0 +1,1172 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Frame getFrameTitle 1`] = `
+<div
+ className="frame"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Jump to https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js:10"
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame library frame 1`] = `
+<div
+ className="frame selected"
+ key="3"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "updateEvents",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "updateEvents",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame user frame (not selected) 1`] = `
+<div
+ className="frame"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame user frame 1`] = `
+<div
+ className="frame selected"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap
new file mode 100644
index 0000000000..d1068b1aa0
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap
@@ -0,0 +1,1001 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Frames Library Frames groups all the Webpack-related frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1-appFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": "2-webpackBootstrapFrame",
+ "location": Object {
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ Object {
+ "id": "3-webpackBundleFrame",
+ "location": Object {
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ },
+ Object {
+ "id": "4-webpackBootstrapFrame",
+ "location": Object {
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ Object {
+ "id": "5-webpackBundleFrame",
+ "location": Object {
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ },
+ ]
+ }
+ key="2-webpackBootstrapFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames disable frame truncation 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 11,
+ }
+ }
+ hideLocation={false}
+ key="11"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 12,
+ }
+ }
+ hideLocation={false}
+ key="12"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 13,
+ }
+ }
+ hideLocation={false}
+ key="13"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 14,
+ }
+ }
+ hideLocation={false}
+ key="14"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 15,
+ }
+ }
+ hideLocation={false}
+ key="15"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 16,
+ }
+ }
+ hideLocation={false}
+ key="16"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 17,
+ }
+ }
+ hideLocation={false}
+ key="17"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 18,
+ }
+ }
+ hideLocation={false}
+ key="18"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 19,
+ }
+ }
+ hideLocation={false}
+ key="19"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 20,
+ }
+ }
+ hideLocation={false}
+ key="20"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames empty frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ className="pane-info empty"
+ >
+ Not paused
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames one frame 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames passes the getFrameTitle prop to the Frame component 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "displayName": "renderFoo",
+ "id": 1,
+ "location": Object {
+ "line": 55,
+ "source": Object {
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames toggling the show more button 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+ <div
+ className="show-more-container"
+ >
+ <button
+ className="show-more"
+ onClick={[Function]}
+ >
+ Collapse rows
+ </button>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap
new file mode 100644
index 0000000000..97c4c2ad1a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap
@@ -0,0 +1,2286 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Group displays a group 1`] = `
+<div
+ className="frames-group"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="frame"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Show Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={1}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+</div>
+`;
+
+exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
+<div
+ className="frames-group expanded"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Collapse Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={true}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="1"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "a",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="2"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "b",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="3"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ </div>
+</div>
+`;
+
+exports[`Group renders group with anonymous functions 1`] = `
+<div
+ className="frames-group"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Show Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+</div>
+`;
+
+exports[`Group renders group with anonymous functions 2`] = `
+<div
+ className="frames-group expanded"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Collapse Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={true}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="1"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-2",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="2"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-3",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="3"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css
new file mode 100644
index 0000000000..07ebd44048
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css
@@ -0,0 +1,100 @@
+/* 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/>. */
+
+.secondary-panes .map-scopes-header {
+ padding-inline-end: 3px;
+}
+
+.secondary-panes .header-buttons .img.shortcuts {
+ width: 14px;
+ height: 14px;
+ /* Better vertical centering of the icon */
+ margin-top: -2px;
+}
+
+.scopes-content .node.object-node {
+ padding-inline-start: 7px;
+}
+
+.scopes-content .toggle-map-scopes a.mdn {
+ padding-inline-start: 3px;
+}
+
+.scopes-content .toggle-map-scopes .img.shortcuts {
+ background: var(--theme-comment);
+}
+
+.scopes-content .object-node.default-property {
+ opacity: 0.6;
+}
+
+.scopes-content .object-node {
+ padding-inline-start: 20px;
+}
+
+html[dir="rtl"] .scopes-content .object-node {
+ padding-right: 4px;
+}
+
+.scopes-content .object-label {
+ color: var(--theme-highlight-blue);
+}
+
+.objectBox-object,
+.objectBox-text,
+.objectBox-table,
+.objectLink-textNode,
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date,
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ white-space: nowrap;
+}
+
+.scopes-pane ._content {
+ overflow: auto;
+}
+
+.scopes-list {
+ padding: 4px 0px;
+}
+
+.scopes-list .function-signature {
+ display: inline-block;
+}
+
+.scopes-list .scope-type-toggle {
+ text-align: center;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.scopes-list .scope-type-toggle button {
+ /* Override color so that the link doesn't turn purple */
+ color: var(--theme-body-color);
+ font-size: inherit;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.scopes-list .scope-type-toggle button:hover {
+ background: transparent;
+}
+
+.scopes-list .tree.object-inspector .node.object-node {
+ display: flex;
+ align-items: center;
+}
+
+.scopes-list .tree.object-inspector .tree-node button.arrow,
+.scopes-list button.invoke-getter {
+ margin-top: 2px;
+}
+
+.scopes-list .tree {
+ line-height: 15px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js
new file mode 100644
index 0000000000..135decd254
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js
@@ -0,0 +1,413 @@
+/* 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/>. */
+
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ button,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import AccessibleImage from "../shared/AccessibleImage";
+import { showMenu } from "../../context-menu/menu";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+import {
+ getSelectedFrame,
+ getCurrentThread,
+ getSelectedSource,
+ getGeneratedFrameScope,
+ getOriginalFrameScope,
+ getPauseReason,
+ isMapScopesEnabled,
+ getLastExpandedScopes,
+ getIsCurrentThreadPaused,
+} from "../../selectors/index";
+import {
+ getScopesItemsForSelectedFrame,
+ getScopeItemPath,
+} from "../../utils/pause/scopes";
+import { clientCommands } from "../../client/firefox";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+const { ObjectInspector } = objectInspector;
+
+class Scopes extends PureComponent {
+ constructor(props) {
+ const { why, selectedFrame, originalFrameScopes, generatedFrameScopes } =
+ props;
+
+ super(props);
+
+ this.state = {
+ originalScopes: getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ originalFrameScopes
+ ),
+ generatedScopes: getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ generatedFrameScopes
+ ),
+ };
+ }
+
+ static get propTypes() {
+ return {
+ addWatchpoint: PropTypes.func.isRequired,
+ expandedScopes: PropTypes.array.isRequired,
+ generatedFrameScopes: PropTypes.object,
+ highlightDomElement: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ mapScopesEnabled: PropTypes.bool.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openLink: PropTypes.func.isRequired,
+ originalFrameScopes: PropTypes.object,
+ removeWatchpoint: PropTypes.func.isRequired,
+ setExpandedScope: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ why: PropTypes.object.isRequired,
+ selectedFrame: PropTypes.object,
+ toggleMapScopes: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const {
+ selectedFrame,
+ originalFrameScopes,
+ generatedFrameScopes,
+ isPaused,
+ selectedSource,
+ } = this.props;
+ const isPausedChanged = isPaused !== nextProps.isPaused;
+ const selectedFrameChanged = selectedFrame !== nextProps.selectedFrame;
+ const originalFrameScopesChanged =
+ originalFrameScopes !== nextProps.originalFrameScopes;
+ const generatedFrameScopesChanged =
+ generatedFrameScopes !== nextProps.generatedFrameScopes;
+ const selectedSourceChanged = selectedSource !== nextProps.selectedSource;
+
+ if (
+ isPausedChanged ||
+ selectedFrameChanged ||
+ originalFrameScopesChanged ||
+ generatedFrameScopesChanged ||
+ selectedSourceChanged
+ ) {
+ this.setState({
+ originalScopes: getScopesItemsForSelectedFrame(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.originalFrameScopes
+ ),
+ generatedScopes: getScopesItemsForSelectedFrame(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.generatedFrameScopes
+ ),
+ });
+ }
+ }
+
+ onContextMenu = (event, item) => {
+ const { addWatchpoint, removeWatchpoint } = this.props;
+
+ if (!item.parent || !item.contents.configurable) {
+ return;
+ }
+
+ if (!item.contents || item.contents.watchpoint) {
+ const removeWatchpointLabel = L10N.getStr("watchpoints.removeWatchpoint");
+
+ const removeWatchpointItem = {
+ id: "node-menu-remove-watchpoint",
+ label: removeWatchpointLabel,
+ disabled: false,
+ click: () => removeWatchpoint(item),
+ };
+
+ const menuItems = [removeWatchpointItem];
+ showMenu(event, menuItems);
+ return;
+ }
+
+ const addSetWatchpointLabel = L10N.getStr("watchpoints.setWatchpoint");
+ const addGetWatchpointLabel = L10N.getStr("watchpoints.getWatchpoint");
+ const addGetOrSetWatchpointLabel = L10N.getStr(
+ "watchpoints.getOrSetWatchpoint"
+ );
+ const watchpointsSubmenuLabel = L10N.getStr("watchpoints.submenu");
+
+ const addSetWatchpointItem = {
+ id: "node-menu-add-set-watchpoint",
+ label: addSetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "set"),
+ };
+
+ const addGetWatchpointItem = {
+ id: "node-menu-add-get-watchpoint",
+ label: addGetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "get"),
+ };
+
+ const addGetOrSetWatchpointItem = {
+ id: "node-menu-add-get-watchpoint",
+ label: addGetOrSetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "getorset"),
+ };
+
+ const watchpointsSubmenuItem = {
+ id: "node-menu-watchpoints",
+ label: watchpointsSubmenuLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "set"),
+ submenu: [
+ addSetWatchpointItem,
+ addGetWatchpointItem,
+ addGetOrSetWatchpointItem,
+ ],
+ };
+
+ const menuItems = [watchpointsSubmenuItem];
+ showMenu(event, menuItems);
+ };
+
+ renderWatchpointButton = item => {
+ const { removeWatchpoint } = this.props;
+
+ if (
+ !item ||
+ !item.contents ||
+ !item.contents.watchpoint ||
+ typeof L10N === "undefined"
+ ) {
+ return null;
+ }
+
+ const { watchpoint } = item.contents;
+ return button({
+ className: `remove-watchpoint-${watchpoint}`,
+ title: L10N.getStr("watchpoints.removeWatchpointTooltip"),
+ onClick: e => {
+ e.stopPropagation();
+ removeWatchpoint(item);
+ },
+ });
+ };
+
+ renderScopesList() {
+ const {
+ isLoading,
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ mapScopesEnabled,
+ selectedFrame,
+ setExpandedScope,
+ expandedScopes,
+ selectedSource,
+ } = this.props;
+
+ if (!selectedSource) {
+ return div(
+ { className: "pane scopes-list" },
+ div({ className: "pane-info" }, L10N.getStr("scopes.notAvailable"))
+ );
+ }
+
+ const { originalScopes, generatedScopes } = this.state;
+ let scopes = null;
+
+ if (
+ selectedSource.isOriginal &&
+ !selectedSource.isPrettyPrinted &&
+ !selectedFrame.generatedLocation?.source.isWasm
+ ) {
+ if (!mapScopesEnabled) {
+ return div(
+ { className: "pane scopes-list" },
+ div(
+ {
+ className: "pane-info no-original-scopes-info",
+ "aria-role": "status",
+ },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ L10N.getFormatStr(
+ "scopes.noOriginalScopes",
+ L10N.getStr("scopes.showOriginalScopes")
+ )
+ )
+ );
+ }
+ if (isLoading) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ L10N.getStr("scopes.loadingOriginalScopes")
+ )
+ );
+ }
+ scopes = originalScopes;
+ } else {
+ if (isLoading) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ L10N.getStr("loadingText")
+ )
+ );
+ }
+ scopes = generatedScopes;
+ }
+
+ function initiallyExpanded(item) {
+ return expandedScopes.some(path => path == getScopeItemPath(item));
+ }
+
+ if (scopes && !!scopes.length) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ React.createElement(ObjectInspector, {
+ roots: scopes,
+ autoExpandAll: false,
+ autoExpandDepth: 1,
+ client: clientCommands,
+ createElement: tagName => document.createElement(tagName),
+ disableWrap: true,
+ dimTopLevelWindow: true,
+ frame: selectedFrame,
+ mayUseCustomFormatter: true,
+ openLink: openLink,
+ onDOMNodeClick: grip => openElementInInspector(grip),
+ onInspectIconClick: grip => openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
+ onContextMenu: this.onContextMenu,
+ setExpanded: (path, expand) =>
+ setExpandedScope(selectedFrame, path, expand),
+ initiallyExpanded: initiallyExpanded,
+ renderItemActions: this.renderWatchpointButton,
+ shouldRenderTooltip: true,
+ })
+ );
+ }
+
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ {
+ className: "pane-info",
+ },
+ L10N.getStr("scopes.notAvailable")
+ )
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: "scopes-content",
+ },
+ this.renderScopesList()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ // This component doesn't need any prop when we are not paused
+ const selectedFrame = getSelectedFrame(state, getCurrentThread(state));
+ if (!selectedFrame) {
+ return {};
+ }
+ const why = getPauseReason(state, selectedFrame.thread);
+ const expandedScopes = getLastExpandedScopes(state, selectedFrame.thread);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const selectedSource = getSelectedSource(state);
+
+ let originalFrameScopes;
+ let generatedFrameScopes;
+ let isLoading;
+ let mapScopesEnabled;
+
+ if (
+ selectedSource?.isOriginal &&
+ !selectedSource?.isPrettyPrinted &&
+ !selectedFrame.generatedLocation?.source.isWasm
+ ) {
+ const { scope, pending: originalPending } = getOriginalFrameScope(
+ state,
+ selectedFrame
+ ) || {
+ scope: null,
+ pending: false,
+ };
+ originalFrameScopes = scope;
+ isLoading = originalPending;
+ mapScopesEnabled = isMapScopesEnabled(state);
+ } else {
+ const { scope, pending: generatedPending } = getGeneratedFrameScope(
+ state,
+ selectedFrame
+ ) || {
+ scope: null,
+ pending: false,
+ };
+ generatedFrameScopes = scope;
+ isLoading = generatedPending;
+ }
+
+ return {
+ originalFrameScopes,
+ generatedFrameScopes,
+ mapScopesEnabled,
+ selectedFrame,
+ isLoading,
+ why,
+ expandedScopes,
+ isPaused,
+ selectedSource,
+ };
+};
+
+export default connect(mapStateToProps, {
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+ setExpandedScope: actions.setExpandedScope,
+ addWatchpoint: actions.addWatchpoint,
+ removeWatchpoint: actions.removeWatchpoint,
+ toggleMapScopes: actions.toggleMapScopes,
+})(Scopes);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css b/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css
new file mode 100644
index 0000000000..6432e9fe75
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css
@@ -0,0 +1,98 @@
+/* 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/>. */
+
+.secondary-panes {
+ overflow-x: hidden;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ white-space: nowrap;
+ background-color: var(--theme-sidebar-background);
+ --breakpoint-expression-right-clear-space: 36px;
+ --paused-background-color: hsl(54, 100%, 92%);
+ --paused-color: var(--theme-body-color);
+
+ .theme-dark & {
+ --paused-background-color: hsl(42, 37%, 19%);
+ --paused-color: hsl(43, 94%, 81%);
+ }
+}
+
+.secondary-panes .controlled > div {
+ max-width: 100%;
+}
+
+/*
+ We apply overflow to the container with the commandbar.
+ This allows the commandbar to remain fixed when scrolling
+ until the content completely ends. Not just the height of
+ the wrapper.
+ Ref: https://github.com/firefox-devtools/debugger/issues/3426
+*/
+
+.secondary-panes-wrapper {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.secondary-panes .accordion {
+ flex: 1 0 auto;
+ margin-bottom: 0;
+}
+
+.secondary-panes-wrapper .accordion li:last-child ._content {
+ border-bottom: 0;
+}
+
+.pane {
+ color: var(--theme-body-color);
+}
+
+.pane .pane-info {
+ text-align: start;
+ padding: 0.5em;
+ gap: 8px;
+ display: flex;
+ white-space: normal;
+}
+
+.pane .pane-info.no-original-scopes-info {
+ background-color: var(--theme-warning-background);
+ color: var(--theme-warning-color);
+}
+
+.secondary-panes .breakpoints-buttons {
+ display: flex;
+}
+
+.dropdown {
+ width: 20em;
+ overflow: auto;
+}
+
+.secondary-panes input[type="checkbox"] {
+ margin: 0;
+ margin-inline-end: 4px;
+ vertical-align: text-top;
+}
+
+.secondary-panes-wrapper .command-bar.bottom {
+ background-color: var(--theme-body-background);
+}
+
+/**
+ * Skip Pausing style
+ * Add a gray background and lower content opacity
+ */
+.skip-pausing .xhr-breakpoints-pane ._content,
+.skip-pausing .breakpoints-pane ._content,
+.skip-pausing .event-listeners-pane ._content,
+.skip-pausing .dom-mutations-pane ._content {
+ background-color: var(--skip-pausing-background-color);
+ opacity: var(--skip-pausing-opacity);
+ color: var(--skip-pausing-color);
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Thread.js b/devtools/client/debugger/src/components/SecondaryPanes/Thread.js
new file mode 100644
index 0000000000..bfa73f1346
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Thread.js
@@ -0,0 +1,81 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import actions from "../../actions/index";
+import { getCurrentThread, getIsPaused } from "../../selectors/index";
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+export class Thread extends Component {
+ static get propTypes() {
+ return {
+ currentThread: PropTypes.string.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ selectThread: PropTypes.func.isRequired,
+ thread: PropTypes.object.isRequired,
+ };
+ }
+
+ onSelectThread = () => {
+ this.props.selectThread(this.props.thread.actor);
+ };
+
+ render() {
+ const { currentThread, isPaused, thread } = this.props;
+
+ const isWorker = thread.targetType.includes("worker");
+ let label = thread.name;
+ if (thread.serviceWorkerStatus) {
+ label += ` (${thread.serviceWorkerStatus})`;
+ }
+ return div(
+ {
+ className: classnames("thread", {
+ selected: thread.actor == currentThread,
+ paused: isPaused,
+ }),
+ key: thread.actor,
+ onClick: this.onSelectThread,
+ },
+ div(
+ {
+ className: "icon",
+ },
+ React.createElement(AccessibleImage, {
+ className: isWorker ? "worker" : "window",
+ })
+ ),
+ div(
+ {
+ className: "label",
+ },
+ label
+ ),
+ isPaused
+ ? span(
+ {
+ className: "pause-badge",
+ role: "status",
+ },
+ L10N.getStr("pausedThread")
+ )
+ : null
+ );
+ }
+}
+
+const mapStateToProps = (state, props) => ({
+ currentThread: getCurrentThread(state),
+ isPaused: getIsPaused(state, props.thread.actor),
+});
+
+export default connect(mapStateToProps, {
+ selectThread: actions.selectThread,
+})(Thread);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Threads.css b/devtools/client/debugger/src/components/SecondaryPanes/Threads.css
new file mode 100644
index 0000000000..603aa3ad8a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Threads.css
@@ -0,0 +1,64 @@
+/* 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/>. */
+
+.threads-list {
+ padding: 4px 0;
+}
+
+.threads-list * {
+ user-select: none;
+}
+
+.threads-list > .thread {
+ font-size: inherit;
+ color: var(--theme-text-color-strong);
+ padding: 2px 6px;
+ padding-inline-start: 20px;
+ line-height: 16px;
+ position: relative;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &:hover {
+ background-color: var(--search-overlays-semitransparent);
+ }
+
+ &.selected {
+ background: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+
+ & .img {
+ background-color: currentColor;
+ }
+ }
+
+ &.paused:not(.selected) {
+ background-color: var(--paused-background-color);
+ color: var(--paused-color);
+ }
+}
+
+.threads-list .icon {
+ flex: none;
+}
+
+.threads-list .img {
+ display: block;
+}
+
+.threads-list .label {
+ display: inline-block;
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.threads-list .pause-badge {
+ flex: none;
+ font-weight: bold;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Threads.js b/devtools/client/debugger/src/components/SecondaryPanes/Threads.js
new file mode 100644
index 0000000000..2d21a1dcc5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Threads.js
@@ -0,0 +1,40 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { getAllThreads } from "../../selectors/index";
+import Thread from "./Thread";
+
+export class Threads extends Component {
+ static get propTypes() {
+ return {
+ threads: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { threads } = this.props;
+ return div(
+ {
+ className: "pane threads-list",
+ },
+ threads.map(thread =>
+ React.createElement(Thread, {
+ thread: thread,
+ key: thread.actor,
+ })
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ threads: getAllThreads(state),
+});
+
+export default connect(mapStateToProps)(Threads);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css
new file mode 100644
index 0000000000..6fa5bc42d7
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css
@@ -0,0 +1,53 @@
+/* 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/>. */
+
+.why-paused {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background-color: var(--paused-background-color);
+ color: var(--paused-color);
+ font-size: 12px;
+ cursor: default;
+ min-height: 44px;
+ padding: 6px;
+ white-space: normal;
+ font-weight: bold;
+}
+
+.why-paused > div {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.why-paused .info.icon {
+ align-self: center;
+ padding-right: 4px;
+ margin-inline-start: 14px;
+ margin-inline-end: 3px;
+}
+
+.why-paused .pause.reason {
+ display: flex;
+ flex-direction: column;
+ padding-right: 4px;
+}
+
+.why-paused .message {
+ font-style: italic;
+ font-weight: 100;
+}
+
+.why-paused .mutationNode {
+ font-weight: normal;
+}
+
+.why-paused .message.warning {
+ color: var(--theme-graphs-full-red);
+ font-family: var(--monospace-font-family);
+ font-size: 10px;
+ font-style: normal;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js
new file mode 100644
index 0000000000..fabd75a7ce
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js
@@ -0,0 +1,218 @@
+/* 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/>. */
+
+const {
+ LocalizationProvider,
+ Localized,
+} = require("resource://devtools/client/shared/vendor/fluent-react.js");
+
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import AccessibleImage from "../shared/AccessibleImage";
+import actions from "../../actions/index";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+
+import { getPauseReason } from "../../utils/pause/index";
+import {
+ getCurrentThread,
+ getPaneCollapse,
+ getPauseReason as getWhy,
+} from "../../selectors/index";
+
+class WhyPaused extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = { hideWhyPaused: "" };
+ }
+
+ static get propTypes() {
+ return {
+ delay: PropTypes.number.isRequired,
+ endPanelCollapsed: PropTypes.bool.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ why: PropTypes.object,
+ };
+ }
+
+ componentDidUpdate() {
+ const { delay } = this.props;
+
+ if (delay) {
+ setTimeout(() => {
+ this.setState({ hideWhyPaused: "" });
+ }, delay);
+ } else {
+ this.setState({ hideWhyPaused: "pane why-paused" });
+ }
+ }
+
+ renderExceptionSummary(exception) {
+ if (typeof exception === "string") {
+ return exception;
+ }
+
+ const { preview } = exception;
+ if (!preview || !preview.name || !preview.message) {
+ return null;
+ }
+
+ return `${preview.name}: ${preview.message}`;
+ }
+
+ renderMessage(why) {
+ const { type, exception, message } = why;
+
+ if (type == "exception" && exception) {
+ // Our types for 'Why' are too general because 'type' can be 'string'.
+ // $FlowFixMe - We should have a proper discriminating union of reasons.
+ const summary = this.renderExceptionSummary(exception);
+ return div(
+ {
+ className: "message warning",
+ },
+ summary
+ );
+ }
+
+ if (type === "mutationBreakpoint" && why.nodeGrip) {
+ const { nodeGrip, ancestorGrip, action } = why;
+ const {
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const targetRep = Rep({
+ object: nodeGrip,
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(nodeGrip),
+ onInspectIconClick: () => openElementInInspector(nodeGrip),
+ onDOMNodeMouseOver: () => highlightDomElement(nodeGrip),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ });
+
+ const ancestorRep = ancestorGrip
+ ? Rep({
+ object: ancestorGrip,
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(ancestorGrip),
+ onInspectIconClick: () => openElementInInspector(ancestorGrip),
+ onDOMNodeMouseOver: () => highlightDomElement(ancestorGrip),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ })
+ : null;
+ return div(
+ null,
+ div(
+ {
+ className: "message",
+ },
+ why.message
+ ),
+ div(
+ {
+ className: "mutationNode",
+ },
+ ancestorRep,
+ ancestorGrip
+ ? span(
+ {
+ className: "why-paused-ancestor",
+ },
+ React.createElement(Localized, {
+ id:
+ action === "remove"
+ ? "whypaused-mutation-breakpoint-removed"
+ : "whypaused-mutation-breakpoint-added",
+ }),
+ targetRep
+ )
+ : targetRep
+ )
+ );
+ }
+
+ if (typeof message == "string") {
+ return div(
+ {
+ className: "message",
+ },
+ message
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ const { endPanelCollapsed, why } = this.props;
+ const { fluentBundles } = this.context;
+ const reason = getPauseReason(why);
+
+ if (!why || !reason || endPanelCollapsed) {
+ return div({
+ className: this.state.hideWhyPaused,
+ });
+ }
+ return (
+ // We're rendering the LocalizationProvider component from here and not in an upper
+ // component because it does set a new context, overriding the context that we set
+ // in the first place in <App>, which breaks some components.
+ // This should be fixed in Bug 1743155.
+ React.createElement(
+ LocalizationProvider,
+ {
+ bundles: fluentBundles || [],
+ },
+ div(
+ {
+ className: "pane why-paused",
+ },
+ div(
+ null,
+ div(
+ {
+ className: "info icon",
+ },
+ React.createElement(AccessibleImage, {
+ className: "info",
+ })
+ ),
+ div(
+ {
+ className: "pause reason",
+ },
+ React.createElement(Localized, {
+ id: reason,
+ }),
+ this.renderMessage(why)
+ )
+ )
+ )
+ )
+ );
+ }
+}
+
+WhyPaused.contextTypes = { fluentBundles: PropTypes.array };
+
+const mapStateToProps = state => ({
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+ why: getWhy(state, getCurrentThread(state)),
+});
+
+export default connect(mapStateToProps, {
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+})(WhyPaused);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css
new file mode 100644
index 0000000000..50b6240337
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css
@@ -0,0 +1,108 @@
+/* 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/>. */
+
+.xhr-breakpoints-pane ._content {
+ overflow-x: auto;
+}
+
+.xhr-input-container {
+ display: flex;
+}
+
+.xhr-input-form {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ width: 100%;
+ /* helps to display a nice outline on focused elements */
+ padding-block: 2px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+ /* Stop select height from increasing as input height increases */
+ align-items: center;
+}
+
+.xhr-checkbox {
+ margin-inline-start: 0;
+ margin-inline-end: 4px;
+}
+
+.xhr-input-url {
+ border: 1px;
+ flex: 1 1 100px;
+ min-width: min(100%, 100px);
+ height: 24px;
+ background-color: var(--theme-sidebar-background);
+ font-size: inherit;
+ color: var(--theme-body-color);
+}
+
+.xhr-input-url::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.expressions-list .xhr-input-url {
+ /* Prevent vertical bounce when editing an existing XHR Breakpoint */
+ height: 100%;
+}
+
+.xhr-input-method {
+ flex: 0 1 100px;
+ min-width: min(100%, 100px);
+}
+
+.xhr-container {
+ border-left: 4px solid transparent;
+ width: 100%;
+ color: var(--theme-body-color);
+ padding-inline-start: 16px;
+ padding-inline-end: 6px;
+ display: flex;
+ align-items: center;
+ position: relative;
+ height: var(--expression-item-height);
+}
+
+:root.theme-light .xhr-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+:root.theme-dark .xhr-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.xhr-label-method {
+ line-height: 14px;
+ display: inline-block;
+ margin-inline-end: 2px;
+}
+
+.xhr-input-container:not(.focused) .xhr-input-method {
+ display: none;
+}
+
+.xhr-label-url {
+ max-width: calc(100% - var(--breakpoint-expression-right-clear-space));
+ color: var(--theme-comment);
+ display: inline-block;
+ cursor: text;
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding: 0px 2px 0px 2px;
+ line-height: 14px;
+}
+
+.xhr-container label {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+}
+
+.xhr-container__close-btn {
+ display: flex;
+ padding: 2px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
new file mode 100644
index 0000000000..9774255dcd
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
@@ -0,0 +1,383 @@
+/* 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/>. */
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ form,
+ input,
+ li,
+ label,
+ ul,
+ option,
+ select,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+import { CloseButton } from "../shared/Button/index";
+
+import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors/index";
+import ExceptionOption from "./Breakpoints/ExceptionOption";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+// At present, the "Pause on any URL" checkbox creates an xhrBreakpoint
+// of "ANY" with no path, so we can remove that before creating the list
+function getExplicitXHRBreakpoints(xhrBreakpoints) {
+ return xhrBreakpoints.filter(bp => bp.path !== "");
+}
+
+const xhrMethods = [
+ "ANY",
+ "GET",
+ "POST",
+ "PUT",
+ "HEAD",
+ "DELETE",
+ "PATCH",
+ "OPTIONS",
+];
+
+class XHRBreakpoints extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ inputValue: "",
+ inputMethod: "ANY",
+ focused: false,
+ editIndex: -1,
+ clickedOnFormElement: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ disableXHRBreakpoint: PropTypes.func.isRequired,
+ enableXHRBreakpoint: PropTypes.func.isRequired,
+ onXHRAdded: PropTypes.func.isRequired,
+ removeXHRBreakpoint: PropTypes.func.isRequired,
+ setXHRBreakpoint: PropTypes.func.isRequired,
+ shouldPauseOnAny: PropTypes.bool.isRequired,
+ showInput: PropTypes.bool.isRequired,
+ togglePauseOnAny: PropTypes.func.isRequired,
+ updateXHRBreakpoint: PropTypes.func.isRequired,
+ xhrBreakpoints: PropTypes.array.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { showInput } = this.props;
+
+ // Ensures that the input is focused when the "+"
+ // is clicked while the panel is collapsed
+ if (this._input && showInput) {
+ this._input.focus();
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const _input = this._input;
+
+ if (!_input) {
+ return;
+ }
+
+ if (!prevState.editing && this.state.editing) {
+ _input.setSelectionRange(0, _input.value.length);
+ _input.focus();
+ } else if (this.props.showInput && !this.state.focused) {
+ _input.focus();
+ }
+ }
+
+ handleNewSubmit = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const setXHRBreakpoint = function () {
+ this.props.setXHRBreakpoint(
+ this.state.inputValue,
+ this.state.inputMethod
+ );
+ this.hideInput();
+ };
+
+ // force update inputMethod in state for mochitest purposes
+ // before setting XHR breakpoint
+ this.setState(
+ { inputMethod: e.target.children[1].value },
+ setXHRBreakpoint
+ );
+ };
+
+ handleExistingSubmit = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const { editIndex, inputValue, inputMethod } = this.state;
+ const { xhrBreakpoints } = this.props;
+ const { path, method } = xhrBreakpoints[editIndex];
+
+ if (path !== inputValue || method != inputMethod) {
+ this.props.updateXHRBreakpoint(editIndex, inputValue, inputMethod);
+ }
+
+ this.hideInput();
+ };
+
+ handleChange = e => {
+ this.setState({ inputValue: e.target.value });
+ };
+
+ handleMethodChange = e => {
+ this.setState({
+ focused: true,
+ editing: true,
+ inputMethod: e.target.value,
+ });
+ };
+
+ hideInput = () => {
+ if (this.state.clickedOnFormElement) {
+ this.setState({
+ focused: true,
+ clickedOnFormElement: false,
+ });
+ } else {
+ this.setState({
+ focused: false,
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ inputMethod: "ANY",
+ });
+ this.props.onXHRAdded();
+ }
+ };
+
+ onFocus = () => {
+ this.setState({ focused: true, editing: true });
+ };
+
+ onMouseDown = e => {
+ this.setState({ editing: false, clickedOnFormElement: true });
+ };
+
+ handleTab = e => {
+ if (e.key !== "Tab") {
+ return;
+ }
+
+ if (e.currentTarget.nodeName === "INPUT") {
+ this.setState({
+ clickedOnFormElement: true,
+ editing: false,
+ });
+ } else if (e.currentTarget.nodeName === "SELECT" && !e.shiftKey) {
+ // The user has tabbed off the select and we should
+ // cancel the edit
+ this.hideInput();
+ }
+ };
+
+ editExpression = index => {
+ const { xhrBreakpoints } = this.props;
+ const { path, method } = xhrBreakpoints[index];
+ this.setState({
+ inputValue: path,
+ inputMethod: method,
+ editing: true,
+ editIndex: index,
+ });
+ };
+
+ renderXHRInput(onSubmit) {
+ const { focused, inputValue } = this.state;
+ const placeholder = L10N.getStr("xhrBreakpoints.placeholder");
+ return form(
+ {
+ key: "xhr-input-container",
+ className: classnames("xhr-input-container xhr-input-form", {
+ focused,
+ }),
+ onSubmit: onSubmit,
+ },
+ input({
+ className: "xhr-input-url",
+ type: "text",
+ placeholder: placeholder,
+ onChange: this.handleChange,
+ onBlur: this.hideInput,
+ onFocus: this.onFocus,
+ value: inputValue,
+ onKeyDown: this.handleTab,
+ ref: c => (this._input = c),
+ }),
+ this.renderMethodSelectElement(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ handleCheckbox = index => {
+ const { xhrBreakpoints, enableXHRBreakpoint, disableXHRBreakpoint } =
+ this.props;
+ const breakpoint = xhrBreakpoints[index];
+ if (breakpoint.disabled) {
+ enableXHRBreakpoint(index);
+ } else {
+ disableXHRBreakpoint(index);
+ }
+ };
+
+ renderBreakpoint = breakpoint => {
+ const { path, disabled, method } = breakpoint;
+ const { editIndex } = this.state;
+ const { removeXHRBreakpoint, xhrBreakpoints } = this.props;
+
+ // The "pause on any" checkbox
+ if (!path) {
+ return null;
+ }
+
+ // Finds the xhrbreakpoint so as to not make assumptions about position
+ const index = xhrBreakpoints.findIndex(
+ bp => bp.path === path && bp.method === method
+ );
+
+ if (index === editIndex) {
+ return this.renderXHRInput(this.handleExistingSubmit);
+ }
+ return li(
+ {
+ className: "xhr-container",
+ key: `${path}-${method}`,
+ title: path,
+ onDoubleClick: (items, options) => this.editExpression(index),
+ },
+ label(
+ null,
+ React.createElement("input", {
+ type: "checkbox",
+ className: "xhr-checkbox",
+ checked: !disabled,
+ onChange: () => this.handleCheckbox(index),
+ onClick: ev => ev.stopPropagation(),
+ }),
+ div(
+ {
+ className: "xhr-label-method",
+ },
+ method
+ ),
+ div(
+ {
+ className: "xhr-label-url",
+ },
+ path
+ ),
+ div(
+ {
+ className: "xhr-container__close-btn",
+ },
+ React.createElement(CloseButton, {
+ handleClick: e => removeXHRBreakpoint(index),
+ })
+ )
+ )
+ );
+ };
+
+ renderBreakpoints = explicitXhrBreakpoints => {
+ const { showInput } = this.props;
+ return React.createElement(
+ React.Fragment,
+ null,
+ ul(
+ {
+ className: "pane expressions-list",
+ },
+ explicitXhrBreakpoints.map(this.renderBreakpoint)
+ ),
+ showInput && this.renderXHRInput(this.handleNewSubmit)
+ );
+ };
+
+ renderCheckbox = explicitXhrBreakpoints => {
+ const { shouldPauseOnAny, togglePauseOnAny } = this.props;
+ return div(
+ {
+ className: classnames("breakpoints-options", {
+ empty: explicitXhrBreakpoints.length === 0,
+ }),
+ },
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions",
+ label: L10N.getStr("pauseOnAnyXHR"),
+ isChecked: shouldPauseOnAny,
+ onChange: () => togglePauseOnAny(),
+ })
+ );
+ };
+ renderMethodOption = method => {
+ return option(
+ {
+ key: method,
+ value: method,
+ // e.stopPropagation() required here since otherwise Firefox triggers 2x
+ // onMouseDown events on <select> upon clicking on an <option>
+ onMouseDown: e => e.stopPropagation(),
+ },
+ method
+ );
+ };
+
+ renderMethodSelectElement = () => {
+ return select(
+ {
+ value: this.state.inputMethod,
+ className: "xhr-input-method",
+ onChange: this.handleMethodChange,
+ onMouseDown: this.onMouseDown,
+ onKeyDown: this.handleTab,
+ },
+ xhrMethods.map(this.renderMethodOption)
+ );
+ };
+
+ render() {
+ const { xhrBreakpoints } = this.props;
+ const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints);
+ return React.createElement(
+ React.Fragment,
+ null,
+ this.renderCheckbox(explicitXhrBreakpoints),
+ explicitXhrBreakpoints.length === 0
+ ? this.renderXHRInput(this.handleNewSubmit)
+ : this.renderBreakpoints(explicitXhrBreakpoints)
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ xhrBreakpoints: getXHRBreakpoints(state),
+ shouldPauseOnAny: shouldPauseOnAnyXHR(state),
+});
+
+export default connect(mapStateToProps, {
+ setXHRBreakpoint: actions.setXHRBreakpoint,
+ removeXHRBreakpoint: actions.removeXHRBreakpoint,
+ enableXHRBreakpoint: actions.enableXHRBreakpoint,
+ disableXHRBreakpoint: actions.disableXHRBreakpoint,
+ updateXHRBreakpoint: actions.updateXHRBreakpoint,
+ togglePauseOnAny: actions.togglePauseOnAny,
+})(XHRBreakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/index.js b/devtools/client/debugger/src/components/SecondaryPanes/index.js
new file mode 100644
index 0000000000..20830afc12
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/index.js
@@ -0,0 +1,548 @@
+/* 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/>. */
+
+const SplitBox = require("resource://devtools/client/shared/components/splitter/SplitBox.js");
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ label,
+ button,
+ a,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import actions from "../../actions/index";
+import {
+ getTopFrame,
+ getExpressions,
+ getPauseCommand,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getSelectedSource,
+ getThreads,
+ getCurrentThread,
+ getPauseReason,
+ getShouldBreakpointsPaneOpenOnPause,
+ getSkipPausing,
+ shouldLogEventBreakpoints,
+} from "../../selectors/index";
+
+import AccessibleImage from "../shared/AccessibleImage";
+import { prefs } from "../../utils/prefs";
+
+import Breakpoints from "./Breakpoints/index";
+import Expressions from "./Expressions";
+import Frames from "./Frames/index";
+import Threads from "./Threads";
+import Accordion from "../shared/Accordion";
+import CommandBar from "./CommandBar";
+import XHRBreakpoints from "./XHRBreakpoints";
+import EventListeners from "./EventListeners";
+import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
+import WhyPaused from "./WhyPaused";
+
+import Scopes from "./Scopes";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function debugBtn(onClick, type, className, tooltip) {
+ return button(
+ {
+ onClick: onClick,
+ className: `${type} ${className}`,
+ key: type,
+ title: tooltip,
+ },
+ React.createElement(AccessibleImage, {
+ className: type,
+ title: tooltip,
+ "aria-label": tooltip,
+ })
+ );
+}
+
+const mdnLink =
+ "https://firefox-source-docs.mozilla.org/devtools-user/debugger/using_the_debugger_map_scopes_feature/";
+
+class SecondaryPanes extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showExpressionsInput: false,
+ showXHRInput: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ evaluateExpressionsForCurrentContext: PropTypes.func.isRequired,
+ expressions: PropTypes.array.isRequired,
+ hasFrames: PropTypes.bool.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ logEventBreakpoints: PropTypes.bool.isRequired,
+ mapScopesEnabled: PropTypes.bool.isRequired,
+ pauseReason: PropTypes.string.isRequired,
+ shouldBreakpointsPaneOpenOnPause: PropTypes.bool.isRequired,
+ thread: PropTypes.string.isRequired,
+ renderWhyPauseDelay: PropTypes.number.isRequired,
+ selectedFrame: PropTypes.object,
+ skipPausing: PropTypes.bool.isRequired,
+ source: PropTypes.object,
+ toggleEventLogging: PropTypes.func.isRequired,
+ resetBreakpointsPaneState: PropTypes.func.isRequired,
+ toggleMapScopes: PropTypes.func.isRequired,
+ threads: PropTypes.array.isRequired,
+ removeAllBreakpoints: PropTypes.func.isRequired,
+ removeAllXHRBreakpoints: PropTypes.func.isRequired,
+ };
+ }
+
+ onExpressionAdded = () => {
+ this.setState({ showExpressionsInput: false });
+ };
+
+ onXHRAdded = () => {
+ this.setState({ showXHRInput: false });
+ };
+
+ watchExpressionHeaderButtons() {
+ const { expressions } = this.props;
+ const buttons = [];
+
+ if (expressions.length) {
+ buttons.push(
+ debugBtn(
+ () => {
+ this.props.evaluateExpressionsForCurrentContext();
+ },
+ "refresh",
+ "active",
+ L10N.getStr("watchExpressions.refreshButton")
+ )
+ );
+ }
+ buttons.push(
+ debugBtn(
+ () => {
+ if (!prefs.expressionsVisible) {
+ this.onWatchExpressionPaneToggle(true);
+ }
+ this.setState({ showExpressionsInput: true });
+ },
+ "plus",
+ "active",
+ L10N.getStr("expressions.placeholder")
+ )
+ );
+ return buttons;
+ }
+
+ xhrBreakpointsHeaderButtons() {
+ return [
+ debugBtn(
+ () => {
+ if (!prefs.xhrBreakpointsVisible) {
+ this.onXHRPaneToggle(true);
+ }
+ this.setState({ showXHRInput: true });
+ },
+ "plus",
+ "active",
+ L10N.getStr("xhrBreakpoints.label")
+ ),
+
+ debugBtn(
+ () => {
+ this.props.removeAllXHRBreakpoints();
+ },
+ "removeAll",
+ "active",
+ L10N.getStr("xhrBreakpoints.removeAll.tooltip")
+ ),
+ ];
+ }
+
+ breakpointsHeaderButtons() {
+ return [
+ debugBtn(
+ () => {
+ this.props.removeAllBreakpoints();
+ },
+ "removeAll",
+ "active",
+ L10N.getStr("breakpointMenuItem.deleteAll")
+ ),
+ ];
+ }
+
+ getScopeItem() {
+ return {
+ header: L10N.getStr("scopes.header"),
+ className: "scopes-pane",
+ component: React.createElement(Scopes, null),
+ opened: prefs.scopesVisible,
+ buttons: this.getScopesButtons(),
+ onToggle: opened => {
+ prefs.scopesVisible = opened;
+ },
+ };
+ }
+
+ getScopesButtons() {
+ const { selectedFrame, mapScopesEnabled, source } = this.props;
+
+ if (!selectedFrame || !source?.isOriginal || source?.isPrettyPrinted) {
+ return null;
+ }
+
+ return [
+ div(
+ {
+ key: "scopes-buttons",
+ },
+ label(
+ {
+ className: "map-scopes-header",
+ title: L10N.getStr("scopes.showOriginalScopesTooltip"),
+ onClick: e => e.stopPropagation(),
+ },
+ input({
+ type: "checkbox",
+ checked: mapScopesEnabled ? "checked" : "",
+ onChange: e => this.props.toggleMapScopes(),
+ }),
+ L10N.getStr("scopes.showOriginalScopes")
+ ),
+ a(
+ {
+ className: "mdn",
+ target: "_blank",
+ href: mdnLink,
+ onClick: e => e.stopPropagation(),
+ title: L10N.getStr("scopes.showOriginalScopesHelpTooltip"),
+ },
+ React.createElement(AccessibleImage, {
+ className: "shortcuts",
+ })
+ )
+ ),
+ ];
+ }
+
+ getEventButtons() {
+ const { logEventBreakpoints } = this.props;
+ return [
+ div(
+ {
+ key: "events-buttons",
+ },
+ label(
+ {
+ className: "events-header",
+ title: L10N.getStr("eventlisteners.log.label"),
+ },
+ input({
+ type: "checkbox",
+ checked: logEventBreakpoints ? "checked" : "",
+ onChange: e => this.props.toggleEventLogging(),
+ }),
+ L10N.getStr("eventlisteners.log")
+ )
+ ),
+ ];
+ }
+
+ onWatchExpressionPaneToggle(opened) {
+ prefs.expressionsVisible = opened;
+ }
+
+ getWatchItem() {
+ return {
+ header: L10N.getStr("watchExpressions.header"),
+ id: "watch-expressions-pane",
+ className: "watch-expressions-pane",
+ buttons: this.watchExpressionHeaderButtons(),
+ component: React.createElement(Expressions, {
+ showInput: this.state.showExpressionsInput,
+ onExpressionAdded: this.onExpressionAdded,
+ }),
+ opened: prefs.expressionsVisible,
+ onToggle: this.onWatchExpressionPaneToggle,
+ };
+ }
+
+ onXHRPaneToggle(opened) {
+ prefs.xhrBreakpointsVisible = opened;
+ }
+
+ getXHRItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("xhrBreakpoints.header"),
+ id: "xhr-breakpoints-pane",
+ className: "xhr-breakpoints-pane",
+ buttons: this.xhrBreakpointsHeaderButtons(),
+ component: React.createElement(XHRBreakpoints, {
+ showInput: this.state.showXHRInput,
+ onXHRAdded: this.onXHRAdded,
+ }),
+ opened: prefs.xhrBreakpointsVisible || pauseReason === "XHR",
+ onToggle: this.onXHRPaneToggle,
+ };
+ }
+
+ getCallStackItem() {
+ return {
+ header: L10N.getStr("callStack.header"),
+ id: "call-stack-pane",
+ className: "call-stack-pane",
+ component: React.createElement(Frames, {
+ panel: "debugger",
+ }),
+ opened: prefs.callStackVisible,
+ onToggle: opened => {
+ prefs.callStackVisible = opened;
+ },
+ };
+ }
+
+ getThreadsItem() {
+ return {
+ header: L10N.getStr("threadsHeader"),
+ id: "threads-pane",
+ className: "threads-pane",
+ component: React.createElement(Threads, null),
+ opened: prefs.threadsVisible,
+ onToggle: opened => {
+ prefs.threadsVisible = opened;
+ },
+ };
+ }
+
+ getBreakpointsItem() {
+ const { pauseReason, shouldBreakpointsPaneOpenOnPause, thread } =
+ this.props;
+
+ return {
+ header: L10N.getStr("breakpoints.header"),
+ id: "breakpoints-pane",
+ className: "breakpoints-pane",
+ buttons: this.breakpointsHeaderButtons(),
+ component: React.createElement(Breakpoints),
+ opened:
+ prefs.breakpointsVisible ||
+ (pauseReason === "breakpoint" && shouldBreakpointsPaneOpenOnPause),
+ onToggle: opened => {
+ prefs.breakpointsVisible = opened;
+ // one-shot flag used to force open the Breakpoints Pane only
+ // when hitting a breakpoint, but not when selecting frames etc...
+ if (shouldBreakpointsPaneOpenOnPause) {
+ this.props.resetBreakpointsPaneState(thread);
+ }
+ },
+ };
+ }
+
+ getEventListenersItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("eventListenersHeader1"),
+ id: "event-listeners-pane",
+ className: "event-listeners-pane",
+ buttons: this.getEventButtons(),
+ component: React.createElement(EventListeners, null),
+ opened: prefs.eventListenersVisible || pauseReason === "eventBreakpoint",
+ onToggle: opened => {
+ prefs.eventListenersVisible = opened;
+ },
+ };
+ }
+
+ getDOMMutationsItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("domMutationHeader"),
+ id: "dom-mutations-pane",
+ className: "dom-mutations-pane",
+ buttons: [],
+ component: React.createElement(DOMMutationBreakpoints, null),
+ opened:
+ prefs.domMutationBreakpointsVisible ||
+ pauseReason === "mutationBreakpoint",
+ onToggle: opened => {
+ prefs.domMutationBreakpointsVisible = opened;
+ },
+ };
+ }
+
+ getStartItems() {
+ const items = [];
+ const { horizontal, hasFrames } = this.props;
+
+ if (horizontal) {
+ if (this.props.threads.length) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+ }
+
+ items.push(this.getBreakpointsItem());
+
+ if (hasFrames) {
+ items.push(this.getCallStackItem());
+ if (horizontal) {
+ items.push(this.getScopeItem());
+ }
+ }
+
+ items.push(this.getXHRItem());
+
+ items.push(this.getEventListenersItem());
+
+ items.push(this.getDOMMutationsItem());
+
+ return items;
+ }
+
+ getEndItems() {
+ if (this.props.horizontal) {
+ return [];
+ }
+
+ const items = [];
+ if (this.props.threads.length) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+
+ if (this.props.hasFrames) {
+ items.push(this.getScopeItem());
+ }
+
+ return items;
+ }
+
+ getItems() {
+ return [...this.getStartItems(), ...this.getEndItems()];
+ }
+
+ renderHorizontalLayout() {
+ const { renderWhyPauseDelay } = this.props;
+ return div(
+ null,
+ React.createElement(WhyPaused, {
+ delay: renderWhyPauseDelay,
+ }),
+ React.createElement(Accordion, {
+ items: this.getItems(),
+ })
+ );
+ }
+
+ renderVerticalLayout() {
+ return React.createElement(SplitBox, {
+ initialSize: "300px",
+ minSize: 10,
+ maxSize: "50%",
+ splitterSize: 1,
+ startPanel: div(
+ {
+ style: {
+ width: "inherit",
+ },
+ },
+ React.createElement(WhyPaused, {
+ delay: this.props.renderWhyPauseDelay,
+ }),
+ React.createElement(Accordion, {
+ items: this.getStartItems(),
+ })
+ ),
+ endPanel: React.createElement(Accordion, {
+ items: this.getEndItems(),
+ }),
+ });
+ }
+
+ render() {
+ const { skipPausing } = this.props;
+ return div(
+ {
+ className: "secondary-panes-wrapper",
+ },
+ React.createElement(CommandBar, {
+ horizontal: this.props.horizontal,
+ }),
+ React.createElement(
+ "div",
+ {
+ className: classnames(
+ "secondary-panes",
+ skipPausing && "skip-pausing"
+ ),
+ },
+ this.props.horizontal
+ ? this.renderHorizontalLayout()
+ : this.renderVerticalLayout()
+ )
+ );
+ }
+}
+
+// Checks if user is in debugging mode and adds a delay preventing
+// excessive vertical 'jumpiness'
+function getRenderWhyPauseDelay(state, thread) {
+ const inPauseCommand = !!getPauseCommand(state, thread);
+
+ if (!inPauseCommand) {
+ return 100;
+ }
+
+ return 0;
+}
+
+const mapStateToProps = state => {
+ const thread = getCurrentThread(state);
+ const selectedFrame = getSelectedFrame(state, thread);
+ const pauseReason = getPauseReason(state, thread);
+ const shouldBreakpointsPaneOpenOnPause = getShouldBreakpointsPaneOpenOnPause(
+ state,
+ thread
+ );
+
+ return {
+ expressions: getExpressions(state),
+ hasFrames: !!getTopFrame(state, thread),
+ renderWhyPauseDelay: getRenderWhyPauseDelay(state, thread),
+ selectedFrame,
+ mapScopesEnabled: isMapScopesEnabled(state),
+ threads: getThreads(state),
+ skipPausing: getSkipPausing(state),
+ logEventBreakpoints: shouldLogEventBreakpoints(state),
+ source: getSelectedSource(state),
+ pauseReason: pauseReason?.type ?? "",
+ shouldBreakpointsPaneOpenOnPause,
+ thread,
+ };
+};
+
+export default connect(mapStateToProps, {
+ evaluateExpressionsForCurrentContext:
+ actions.evaluateExpressionsForCurrentContext,
+ toggleMapScopes: actions.toggleMapScopes,
+ breakOnNext: actions.breakOnNext,
+ toggleEventLogging: actions.toggleEventLogging,
+ removeAllBreakpoints: actions.removeAllBreakpoints,
+ removeAllXHRBreakpoints: actions.removeAllXHRBreakpoints,
+ resetBreakpointsPaneState: actions.resetBreakpointsPaneState,
+})(SecondaryPanes);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/moz.build
new file mode 100644
index 0000000000..33cfa2e316
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/moz.build
@@ -0,0 +1,22 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "Breakpoints",
+ "Frames",
+]
+
+CompiledModules(
+ "CommandBar.js",
+ "DOMMutationBreakpoints.js",
+ "EventListeners.js",
+ "Expressions.js",
+ "index.js",
+ "Scopes.js",
+ "Thread.js",
+ "Threads.js",
+ "WhyPaused.js",
+ "XHRBreakpoints.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js
new file mode 100644
index 0000000000..b82997eb9a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js
@@ -0,0 +1,78 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import CommandBar from "../CommandBar";
+
+describe("CommandBar", () => {
+ it("f8 key command calls props.breakOnNext when not in paused state", () => {
+ const props = {
+ breakOnNext: jest.fn(),
+ resume: jest.fn(),
+ isPaused: false,
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+
+ // The "on" spy will see all the keyboard listeners being registered by
+ // the shortcuts.on function
+ const context = { shortcuts: { on: jest.fn() } };
+
+ shallow(React.createElement(CommandBar.WrappedComponent, props), {
+ context,
+ });
+
+ // get the keyboard event listeners recorded from the "on" spy.
+ // this will be an array where each item is itself a two item array
+ // containing the key code and the corresponding handler for that key code
+ const keyEventHandlers = context.shortcuts.on.mock.calls;
+
+ // simulate pressing the F8 key by calling the F8 handlers
+ keyEventHandlers
+ .filter(i => i[0] === "F8")
+ .forEach(([_, handler]) => {
+ handler(mockEvent);
+ });
+
+ expect(props.breakOnNext).toHaveBeenCalled();
+ expect(props.resume).not.toHaveBeenCalled();
+ });
+
+ it("f8 key command calls props.resume when in paused state", () => {
+ const props = {
+ breakOnNext: jest.fn(),
+ resume: jest.fn(),
+ isPaused: true,
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+
+ // The "on" spy will see all the keyboard listeners being registered by
+ // the shortcuts.on function
+ const context = { shortcuts: { on: jest.fn() } };
+
+ shallow(React.createElement(CommandBar.WrappedComponent, props), {
+ context,
+ });
+
+ // get the keyboard event listeners recorded from the "on" spy.
+ // this will be an array where each item is itself a two item array
+ // containing the key code and the corresponding handler for that key code
+ const keyEventHandlers = context.shortcuts.on.mock.calls;
+
+ // simulate pressing the F8 key by calling the F8 handlers
+ keyEventHandlers
+ .filter(i => i[0] === "F8")
+ .forEach(([_, handler]) => {
+ handler(mockEvent);
+ });
+ expect(props.resume).toHaveBeenCalled();
+ expect(props.breakOnNext).not.toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js
new file mode 100644
index 0000000000..58d7776e5a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js
@@ -0,0 +1,136 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import EventListeners from "../EventListeners";
+
+function getCategories() {
+ return [
+ {
+ name: "Category 1",
+ events: [
+ { name: "Subcategory 1", id: "category1.subcategory1" },
+ { name: "Subcategory 2", id: "category1.subcategory2" },
+ ],
+ },
+ {
+ name: "Category 2",
+ events: [
+ { name: "Subcategory 3", id: "category2.subcategory1" },
+ { name: "Subcategory 4", id: "category2.subcategory2" },
+ ],
+ },
+ ];
+}
+
+function generateDefaults(overrides = {}) {
+ const defaults = {
+ activeEventListeners: [],
+ expandedCategories: [],
+ categories: [],
+ };
+
+ return { ...defaults, ...overrides };
+}
+
+function render(overrides = {}) {
+ const props = generateDefaults(overrides);
+ const component = shallow(
+ React.createElement(EventListeners.WrappedComponent, props)
+ );
+ return { component, props };
+}
+
+describe("EventListeners", () => {
+ it("should render", async () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render categories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render expanded categories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ expandedCategories: ["Category 2"],
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render checked subcategories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ activeEventListeners: ["category1.subcategory2"],
+ expandedCategories: ["Category 1"],
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should filter the event listeners based on the event name", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Subcategory 3" to display just one event which
+ // will be the Subcategory 3 event
+ searchInput.simulate("change", {
+ currentTarget: { value: "Subcategory 3" },
+ });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(1);
+ });
+
+ it("should filter the event listeners based on the category name", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Category 1" to display two events which will be
+ // the Subcategory 1 event and the Subcategory 2 event
+ searchInput.simulate("change", { currentTarget: { value: "Category 1" } });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(2);
+ });
+
+ it("should be case insensitive when filtering events and categories", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Subcategory 3" to display just one event which
+ // will be the Subcategory 3 event
+ searchInput.simulate("change", {
+ currentTarget: { value: "sUbCaTeGoRy 3" },
+ });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js
new file mode 100644
index 0000000000..685c636f05
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js
@@ -0,0 +1,77 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import Expressions from "../Expressions";
+
+function generateDefaults(overrides) {
+ return {
+ evaluateExpressions: async () => {},
+ expressions: [
+ {
+ input: "expression1",
+ value: {
+ result: {
+ value: "foo",
+ class: "",
+ },
+ },
+ },
+ {
+ input: "expression2",
+ value: {
+ result: {
+ value: "bar",
+ class: "",
+ },
+ },
+ },
+ ],
+ ...overrides,
+ };
+}
+
+function render(overrides = {}) {
+ const props = generateDefaults(overrides);
+ const component = shallow(
+ React.createElement(Expressions.WrappedComponent, props)
+ );
+ return { component, props };
+}
+
+describe("Expressions", () => {
+ it("should render", async () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should always have unique keys", async () => {
+ const overrides = {
+ expressions: [
+ {
+ input: "expression1",
+ value: {
+ result: {
+ value: undefined,
+ class: "",
+ },
+ },
+ },
+ {
+ input: "expression2",
+ value: {
+ result: {
+ value: undefined,
+ class: "",
+ },
+ },
+ },
+ ],
+ };
+
+ const { component } = render(overrides);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
new file mode 100644
index 0000000000..532c95e4ad
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
@@ -0,0 +1,341 @@
+/* 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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { mount } from "enzyme";
+import XHRBreakpoints from "../XHRBreakpoints";
+
+const xhrMethods = [
+ "ANY",
+ "GET",
+ "POST",
+ "PUT",
+ "HEAD",
+ "DELETE",
+ "PATCH",
+ "OPTIONS",
+];
+
+// default state includes xhrBreakpoints[0] which is the checkbox that
+// enables breaking on any url during an XMLHTTPRequest
+function generateDefaultState(propsOverride) {
+ return {
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ ],
+ enableXHRBreakpoint: () => {},
+ disableXHRBreakpoint: () => {},
+ updateXHRBreakpoint: () => {},
+ removeXHRBreakpoint: () => {},
+ setXHRBreakpoint: () => {},
+ togglePauseOnAny: () => {},
+ showInput: false,
+ shouldPauseOnAny: false,
+ onXHRAdded: () => {},
+ ...propsOverride,
+ };
+}
+
+function renderXHRBreakpointsComponent(propsOverride) {
+ const props = generateDefaultState(propsOverride);
+ const xhrBreakpointsComponent = mount(
+ React.createElement(XHRBreakpoints.WrappedComponent, props)
+ );
+ return xhrBreakpointsComponent;
+}
+
+describe("XHR Breakpoints", function () {
+ it("should render with 0 expressions passed from props", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ expect(xhrBreakpointsComponent).toMatchSnapshot();
+ });
+
+ it("should render with 8 expressions passed from props", function () {
+ const allXHRBreakpointMethods = {
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ {
+ path: "this is any",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is any"',
+ },
+ {
+ path: "this is get",
+ method: "GET",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is get"',
+ },
+ {
+ path: "this is post",
+ method: "POST",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is post"',
+ },
+ {
+ path: "this is put",
+ method: "PUT",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is put"',
+ },
+ {
+ path: "this is head",
+ method: "HEAD",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is head"',
+ },
+ {
+ path: "this is delete",
+ method: "DELETE",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is delete"',
+ },
+ {
+ path: "this is patch",
+ method: "PATCH",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is patch"',
+ },
+ {
+ path: "this is options",
+ method: "OPTIONS",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is options"',
+ },
+ ],
+ };
+
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+ allXHRBreakpointMethods
+ );
+ expect(xhrBreakpointsComponent).toMatchSnapshot();
+ });
+
+ it("should display xhr-input-method on click", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ const xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+ });
+
+ it("should have focused and editing default to false", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ });
+
+ it("should have state {..focused: true, editing: true} on focus", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ });
+
+ // shifting focus from .xhr-input to any other element apart from
+ // .xhr-input-method should unrender .xhr-input-method
+ it("shifting focus should unrender XHR methods", function () {
+ const propsOverride = {
+ onXHRAdded: jest.fn,
+ togglePauseOnAny: jest.fn,
+ };
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ let xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+
+ xhrBreakpointsComponent.find(".breakpoints-options").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".breakpoints-options").simulate("click");
+
+ xhrInputContainer = xhrBreakpointsComponent.find(".xhr-input-container");
+ expect(xhrInputContainer.hasClass("focused")).not.toBeTruthy();
+ });
+
+ // shifting focus from .xhr-input to .xhr-input-method
+ // should not unrender .xhr-input-method
+ it("shifting focus to XHR methods should not unrender", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(true);
+
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("click");
+ const xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+ });
+
+ it("should have all 8 methods available as options", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ expect(xhrInputMethod.children()).toHaveLength(8);
+
+ const actualXHRMethods = [];
+ const expectedXHRMethods = xhrMethods;
+
+ // fill the actualXHRMethods array with actual methods displayed in DOM
+ for (let i = 0; i < xhrInputMethod.children().length; i++) {
+ actualXHRMethods.push(xhrInputMethod.childAt(i).key());
+ }
+
+ // check each expected XHR Method to see if they match the actual methods
+ expectedXHRMethods.forEach((expectedMethod, i) => {
+ function compareMethods(actualMethod) {
+ return expectedMethod === actualMethod;
+ }
+ expect(actualXHRMethods.find(compareMethods)).toBeTruthy();
+ });
+ });
+
+ it("should return focus to input box after selecting a method", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+
+ // focus starts off at .xhr-input
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ // click on method options and select GET
+ const methodEvent = { target: { value: "GET" } };
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("inputMethod")).toBe("ANY");
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ xhrBreakpointsComponent
+ .find(".xhr-input-method")
+ .simulate("change", methodEvent);
+
+ // if state.editing changes from false to true, infer that
+ // this._input.focus() is called, which shifts focus back to input box
+ expect(xhrBreakpointsComponent.state("inputMethod")).toBe("GET");
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ });
+
+ it("should submit the URL and method when adding a breakpoint", function () {
+ const setXHRBreakpointCallback = jest.fn();
+ const propsOverride = {
+ setXHRBreakpoint: setXHRBreakpointCallback,
+ onXHRAdded: jest.fn(),
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+ const availableXHRMethods = xhrMethods;
+ expect(!!availableXHRMethods.length).toBeTruthy();
+
+ // check each of the available methods to see whether
+ // adding them as a method to a new breakpoint works as expected
+ availableXHRMethods.forEach(function (method) {
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ const urlValue = `${method.toLowerCase()}URLValue`;
+
+ // simulate DOM event adding urlValue to .xhr-input
+ const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+ xhrInput.simulate("change", { target: { value: urlValue } });
+
+ // simulate DOM event adding the input method to .xhr-input-method
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ xhrInputMethod.simulate("change", { target: { value: method } });
+
+ xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+ expect(setXHRBreakpointCallback).toHaveBeenCalledWith(urlValue, method);
+ });
+ });
+
+ it("should submit the URL and method when editing a breakpoint", function () {
+ const setXHRBreakpointCallback = jest.fn();
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+ const propsOverride = {
+ updateXHRBreakpoint: setXHRBreakpointCallback,
+ onXHRAdded: jest.fn(),
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ {
+ path: "this is GET",
+ method: "GET",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is get"',
+ },
+ ],
+ };
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+
+ // load xhrBreakpoints pane with one existing xhrBreakpoint
+ const existingXHRbreakpoint =
+ xhrBreakpointsComponent.find(".xhr-container");
+ expect(existingXHRbreakpoint).toHaveLength(1);
+
+ // double click on existing breakpoint
+ existingXHRbreakpoint.simulate("doubleclick");
+ const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+ xhrInput.simulate("focus");
+
+ // change inputs and submit form
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ xhrInput.simulate("change", { target: { value: "POSTURLValue" } });
+ xhrInputMethod.simulate("change", { target: { value: "POST" } });
+ xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+ expect(setXHRBreakpointCallback).toHaveBeenCalledWith(
+ 1,
+ "POSTURLValue",
+ "POST"
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap
new file mode 100644
index 0000000000..cc2ddf09f6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap
@@ -0,0 +1,408 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EventListeners should render 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ />
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render categories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render checked subcategories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ <ul>
+ <li
+ className="event-listener-event"
+ key="category1.subcategory1"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category1.subcategory1"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 1
+ </span>
+ </label>
+ </li>
+ <li
+ className="event-listener-event"
+ key="category1.subcategory2"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ value="category1.subcategory2"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 2
+ </span>
+ </label>
+ </li>
+ </ul>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render expanded categories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ <ul>
+ <li
+ className="event-listener-event"
+ key="category2.subcategory1"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category2.subcategory1"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 3
+ </span>
+ </label>
+ </li>
+ <li
+ className="event-listener-event"
+ key="category2.subcategory2"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category2.subcategory2"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 4
+ </span>
+ </label>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap
new file mode 100644
index 0000000000..6ecf6c2c2e
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap
@@ -0,0 +1,203 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Expressions should always have unique keys 1`] = `
+<div
+ className="pane"
+>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": undefined,
+ },
+ },
+ "name": "expression1",
+ "path": "expression1",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ <li
+ className="expression-container"
+ key="expression2"
+ title="expression2"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": undefined,
+ },
+ },
+ "name": "expression2",
+ "path": "expression2",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Expressions should render 1`] = `
+<div
+ className="pane"
+>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": "foo",
+ },
+ },
+ "name": "expression1",
+ "path": "expression1",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ <li
+ className="expression-container"
+ key="expression2"
+ title="expression2"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": "bar",
+ },
+ },
+ "name": "expression2",
+ "path": "expression2",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ </ul>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
new file mode 100644
index 0000000000..e7c4527621
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
@@ -0,0 +1,619 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`XHR Breakpoints should render with 0 expressions passed from props 1`] = `
+<XHRBreakpoints
+ disableXHRBreakpoint={[Function]}
+ enableXHRBreakpoint={[Function]}
+ onXHRAdded={[Function]}
+ removeXHRBreakpoint={[Function]}
+ setXHRBreakpoint={[Function]}
+ shouldPauseOnAny={false}
+ showInput={false}
+ togglePauseOnAny={[Function]}
+ updateXHRBreakpoint={[Function]}
+ xhrBreakpoints={
+ Array [
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "",
+ "text": "URL contains \\"\\"",
+ },
+ ]
+ }
+>
+ <div
+ className="breakpoints-options empty"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ isChecked={false}
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <label
+ className="breakpoints-exceptions"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </label>
+ </ExceptionOption>
+ </div>
+ <form
+ className="xhr-input-container xhr-input-form"
+ key="xhr-input-container"
+ onSubmit={[Function]}
+ >
+ <input
+ className="xhr-input-url"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Break when URL contains"
+ type="text"
+ value=""
+ />
+ <select
+ className="xhr-input-method"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ value="ANY"
+ >
+ <option
+ key="ANY"
+ onMouseDown={[Function]}
+ value="ANY"
+ >
+ ANY
+ </option>
+ <option
+ key="GET"
+ onMouseDown={[Function]}
+ value="GET"
+ >
+ GET
+ </option>
+ <option
+ key="POST"
+ onMouseDown={[Function]}
+ value="POST"
+ >
+ POST
+ </option>
+ <option
+ key="PUT"
+ onMouseDown={[Function]}
+ value="PUT"
+ >
+ PUT
+ </option>
+ <option
+ key="HEAD"
+ onMouseDown={[Function]}
+ value="HEAD"
+ >
+ HEAD
+ </option>
+ <option
+ key="DELETE"
+ onMouseDown={[Function]}
+ value="DELETE"
+ >
+ DELETE
+ </option>
+ <option
+ key="PATCH"
+ onMouseDown={[Function]}
+ value="PATCH"
+ >
+ PATCH
+ </option>
+ <option
+ key="OPTIONS"
+ onMouseDown={[Function]}
+ value="OPTIONS"
+ >
+ OPTIONS
+ </option>
+ </select>
+ <input
+ style={
+ Object {
+ "display": "none",
+ }
+ }
+ type="submit"
+ />
+ </form>
+</XHRBreakpoints>
+`;
+
+exports[`XHR Breakpoints should render with 8 expressions passed from props 1`] = `
+<XHRBreakpoints
+ disableXHRBreakpoint={[Function]}
+ enableXHRBreakpoint={[Function]}
+ onXHRAdded={[Function]}
+ removeXHRBreakpoint={[Function]}
+ setXHRBreakpoint={[Function]}
+ shouldPauseOnAny={false}
+ showInput={false}
+ togglePauseOnAny={[Function]}
+ updateXHRBreakpoint={[Function]}
+ xhrBreakpoints={
+ Array [
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "",
+ "text": "URL contains \\"\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "this is any",
+ "text": "URL contains \\"this is any\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "GET",
+ "path": "this is get",
+ "text": "URL contains \\"this is get\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "POST",
+ "path": "this is post",
+ "text": "URL contains \\"this is post\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "PUT",
+ "path": "this is put",
+ "text": "URL contains \\"this is put\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "HEAD",
+ "path": "this is head",
+ "text": "URL contains \\"this is head\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "DELETE",
+ "path": "this is delete",
+ "text": "URL contains \\"this is delete\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "PATCH",
+ "path": "this is patch",
+ "text": "URL contains \\"this is patch\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "OPTIONS",
+ "path": "this is options",
+ "text": "URL contains \\"this is options\\"",
+ },
+ ]
+ }
+>
+ <div
+ className="breakpoints-options"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ isChecked={false}
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <label
+ className="breakpoints-exceptions"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </label>
+ </ExceptionOption>
+ </div>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="xhr-container"
+ key="this is any-ANY"
+ onDoubleClick={[Function]}
+ title="this is any"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ ANY
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is any
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is get-GET"
+ onDoubleClick={[Function]}
+ title="this is get"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ GET
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is get
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is post-POST"
+ onDoubleClick={[Function]}
+ title="this is post"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ POST
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is post
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is put-PUT"
+ onDoubleClick={[Function]}
+ title="this is put"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ PUT
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is put
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is head-HEAD"
+ onDoubleClick={[Function]}
+ title="this is head"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ HEAD
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is head
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is delete-DELETE"
+ onDoubleClick={[Function]}
+ title="this is delete"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ DELETE
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is delete
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is patch-PATCH"
+ onDoubleClick={[Function]}
+ title="this is patch"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ PATCH
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is patch
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is options-OPTIONS"
+ onDoubleClick={[Function]}
+ title="this is options"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ OPTIONS
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is options
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ </ul>
+</XHRBreakpoints>
+`;