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.js238
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js92
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeadingsContextMenu.js92
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css253
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js366
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js32
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js162
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build15
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/Breakpoint.spec.js99
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/BreakpointsContextMenu.spec.js135
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js24
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap226
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap19
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css33
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js344
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css76
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js163
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css154
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js307
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.css175
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.js426
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js208
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js15
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameMenu.js112
-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.js199
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js250
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build14
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js157
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/FrameMenu.spec.js121
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js298
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js140
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap476
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap1291
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap1000
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.css103
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.js325
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css86
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Thread.js73
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.css63
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.js40
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css58
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js173
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css131
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js381
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/index.js583
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/moz.build22
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js81
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js137
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js78
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js342
-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.snap191
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap601
55 files changed, 11811 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..0e1feb2c43
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { PureComponent } from "react";
+import { connect } from "../../../utils/connect";
+import { createSelector } from "reselect";
+import classnames from "classnames";
+import actions from "../../../actions";
+import { memoize } from "lodash";
+
+import showContextMenu from "./BreakpointsContextMenu";
+import { CloseButton } from "../../shared/Button";
+
+import {
+ getLocationWithoutColumn,
+ getSelectedText,
+ makeBreakpointId,
+} from "../../../utils/breakpoint";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { features } from "../../../utils/prefs";
+
+import type {
+ Breakpoint as BreakpointType,
+ Frame,
+ Source,
+ SourceLocation,
+ Context,
+} from "../../../types";
+import type SourceEditor from "../../../utils/editor/source-editor";
+
+type FormattedFrame = Frame & {
+ selectedLocation: SourceLocation,
+};
+
+import {
+ getBreakpointsList,
+ getSelectedFrame,
+ getSelectedSource,
+ getCurrentThread,
+ getContext,
+} from "../../../selectors";
+
+type OwnProps = {|
+ source: Source,
+ selectedSource: ?Source,
+ breakpoint: BreakpointType,
+ editor: SourceEditor,
+|};
+type Props = {
+ cx: Context,
+ breakpoint: BreakpointType,
+ breakpoints: BreakpointType[],
+ selectedSource: ?Source,
+ source: Source,
+ frame: FormattedFrame,
+ editor: SourceEditor,
+ enableBreakpoint: typeof actions.enableBreakpoint,
+ removeBreakpoint: typeof actions.removeBreakpoint,
+ removeBreakpoints: typeof actions.removeBreakpoints,
+ removeAllBreakpoints: typeof actions.removeAllBreakpoints,
+ disableBreakpoint: typeof actions.disableBreakpoint,
+ setBreakpointOptions: typeof actions.setBreakpointOptions,
+ toggleAllBreakpoints: typeof actions.toggleAllBreakpoints,
+ toggleBreakpoints: typeof actions.toggleBreakpoints,
+ toggleDisabledBreakpoint: typeof actions.toggleDisabledBreakpoint,
+ openConditionalPanel: typeof actions.openConditionalPanel,
+ selectSpecificLocation: typeof actions.selectSpecificLocation,
+};
+
+class Breakpoint extends PureComponent<Props> {
+ onContextMenu = (e: SyntheticEvent<HTMLElement>) => {
+ showContextMenu({ ...this.props, contextMenuEvent: e });
+ };
+
+ get selectedLocation() {
+ const { breakpoint, selectedSource } = this.props;
+ return getSelectedLocation(breakpoint, selectedSource);
+ }
+
+ onDoubleClick = () => {
+ const { breakpoint, openConditionalPanel } = this.props;
+ if (breakpoint.options.condition) {
+ openConditionalPanel(this.selectedLocation);
+ } else if (breakpoint.options.logValue) {
+ openConditionalPanel(this.selectedLocation, true);
+ }
+ };
+
+ selectBreakpoint = (event: SyntheticEvent<>) => {
+ event.preventDefault();
+ const { cx, selectSpecificLocation } = this.props;
+ selectSpecificLocation(cx, this.selectedLocation);
+ };
+
+ removeBreakpoint = (event: SyntheticEvent<>) => {
+ const { cx, removeBreakpoint, breakpoint } = this.props;
+ event.stopPropagation();
+ removeBreakpoint(cx, breakpoint);
+ };
+
+ handleBreakpointCheckbox = () => {
+ const { cx, breakpoint, enableBreakpoint, disableBreakpoint } = this.props;
+ if (breakpoint.disabled) {
+ enableBreakpoint(cx, breakpoint);
+ } else {
+ disableBreakpoint(cx, breakpoint);
+ }
+ };
+
+ isCurrentlyPausedAtBreakpoint() {
+ const { frame } = this.props;
+ if (!frame) {
+ return false;
+ }
+
+ const bpId = features.columnBreakpoints
+ ? makeBreakpointId(this.selectedLocation)
+ : getLocationWithoutColumn(this.selectedLocation);
+ const frameId = features.columnBreakpoints
+ ? makeBreakpointId(frame.selectedLocation)
+ : getLocationWithoutColumn(frame.selectedLocation);
+ return bpId == frameId;
+ }
+
+ getBreakpointLocation() {
+ const { source } = this.props;
+ const { column, line } = this.selectedLocation;
+
+ const isWasm = source?.isWasm;
+ const columnVal = features.columnBreakpoints && column ? `:${column}` : "";
+ 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 = memoize(
+ (text: string = "", editor: SourceEditor) => {
+ const node = document.createElement("div");
+ editor.CodeMirror.runMode(text, "application/javascript", node);
+ return { __html: node.innerHTML };
+ },
+ text => text
+ );
+
+ render() {
+ const { breakpoint, editor } = 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}
+ onChange={this.handleBreakpointCheckbox}
+ onClick={ev => ev.stopPropagation()}
+ 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)} />
+ </span>
+ <div className="breakpoint-line-close">
+ <div className="breakpoint-line devtools-monospace">
+ {this.getBreakpointLocation()}
+ </div>
+ <CloseButton
+ handleClick={e => this.removeBreakpoint(e)}
+ tooltip={L10N.getStr("breakpoints.removeBreakpointTooltip")}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+const getFormattedFrame = createSelector(
+ getSelectedSource,
+ getSelectedFrame,
+ (selectedSource: ?Source, frame: ?Frame): ?FormattedFrame => {
+ if (!frame) {
+ return null;
+ }
+
+ return {
+ ...frame,
+ selectedLocation: getSelectedLocation(frame, selectedSource),
+ };
+ }
+);
+
+const mapStateToProps = (state, p: OwnProps) => ({
+ cx: getContext(state),
+ breakpoints: getBreakpointsList(state),
+ frame: getFormattedFrame(state, getCurrentThread(state)),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ enableBreakpoint: actions.enableBreakpoint,
+ removeBreakpoint: actions.removeBreakpoint,
+ removeBreakpoints: actions.removeBreakpoints,
+ removeAllBreakpoints: actions.removeAllBreakpoints,
+ disableBreakpoint: actions.disableBreakpoint,
+ selectSpecificLocation: actions.selectSpecificLocation,
+ setBreakpointOptions: actions.setBreakpointOptions,
+ toggleAllBreakpoints: actions.toggleAllBreakpoints,
+ toggleBreakpoints: actions.toggleBreakpoints,
+ toggleDisabledBreakpoint: actions.toggleDisabledBreakpoint,
+ openConditionalPanel: actions.openConditionalPanel,
+})(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..e224accae1
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { PureComponent } from "react";
+import { connect } from "../../../utils/connect";
+import actions from "../../../actions";
+import {
+ getTruncatedFileName,
+ getDisplayPath,
+ getSourceQueryString,
+ getFileURL,
+} from "../../../utils/source";
+import {
+ getHasSiblingOfSameName,
+ getBreakpointsForSource,
+ getContext,
+} from "../../../selectors";
+
+import SourceIcon from "../../shared/SourceIcon";
+
+import type { Source, Breakpoint, Context } from "../../../types";
+import showContextMenu from "./BreakpointHeadingsContextMenu";
+
+type OwnProps = {|
+ sources: Source[],
+ source: Source,
+|};
+type Props = {
+ cx: Context,
+ sources: Source[],
+ source: Source,
+ hasSiblingOfSameName: boolean,
+ breakpointsForSource: Breakpoint[],
+ disableBreakpointsInSource: typeof actions.disableBreakpointsInSource,
+ enableBreakpointsInSource: typeof actions.enableBreakpointsInSource,
+ removeBreakpointsInSource: typeof actions.removeBreakpointsInSource,
+ selectSource: typeof actions.selectSource,
+};
+
+class BreakpointHeading extends PureComponent<Props> {
+ onContextMenu = (e: SyntheticEvent<HTMLElement>) => {
+ showContextMenu({ ...this.props, contextMenuEvent: e });
+ };
+
+ render() {
+ const {
+ cx,
+ sources,
+ source,
+ hasSiblingOfSameName,
+ selectSource,
+ } = this.props;
+
+ const path = getDisplayPath(source, sources);
+ const query = hasSiblingOfSameName ? getSourceQueryString(source) : "";
+
+ return (
+ <div
+ className="breakpoint-heading"
+ title={getFileURL(source, false)}
+ onClick={() => selectSource(cx, source.id)}
+ onContextMenu={this.onContextMenu}
+ >
+ <SourceIcon
+ source={source}
+ modifier={icon =>
+ ["file", "javascript"].includes(icon) ? null : icon
+ }
+ />
+ <div className="filename">
+ {getTruncatedFileName(source, query)}
+ {path && <span>{`../${path}/..`}</span>}
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, { source }) => ({
+ cx: getContext(state),
+ hasSiblingOfSameName: getHasSiblingOfSameName(state, source),
+ breakpointsForSource: getBreakpointsForSource(state, source.id),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ selectSource: actions.selectSource,
+ enableBreakpointsInSource: actions.enableBreakpointsInSource,
+ disableBreakpointsInSource: actions.disableBreakpointsInSource,
+ removeBreakpointsInSource: actions.removeBreakpointsInSource,
+})(BreakpointHeading);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeadingsContextMenu.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeadingsContextMenu.js
new file mode 100644
index 0000000000..443b4681b4
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeadingsContextMenu.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { buildMenu, showMenu } from "../../../context-menu/menu";
+
+import actions from "../../../actions";
+import type { Breakpoint, Source, Context } from "../../../types";
+
+type Props = {
+ cx: Context,
+ source: Source,
+ breakpointsForSource: Breakpoint[],
+ disableBreakpointsInSource: typeof actions.disableBreakpointsInSource,
+ enableBreakpointsInSource: typeof actions.enableBreakpointsInSource,
+ removeBreakpointsInSource: typeof actions.removeBreakpointsInSource,
+ contextMenuEvent: SyntheticEvent<HTMLElement>,
+};
+
+export default function showContextMenu(props: Props) {
+ const {
+ cx,
+ source,
+ breakpointsForSource,
+ disableBreakpointsInSource,
+ enableBreakpointsInSource,
+ removeBreakpointsInSource,
+ contextMenuEvent,
+ } = props;
+
+ contextMenuEvent.preventDefault();
+
+ const enableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.label"
+ );
+ const disableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.label"
+ );
+ const removeInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.label"
+ );
+ const enableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.accesskey"
+ );
+ const disableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.accesskey"
+ );
+ const removeInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.accesskey"
+ );
+
+ const disableInSourceItem = {
+ id: "node-menu-disable-in-source",
+ label: disableInSourceLabel,
+ accesskey: disableInSourceKey,
+ disabled: false,
+ click: () => disableBreakpointsInSource(cx, source),
+ };
+
+ const enableInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: enableInSourceLabel,
+ accesskey: enableInSourceKey,
+ disabled: false,
+ click: () => enableBreakpointsInSource(cx, source),
+ };
+
+ const removeInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: removeInSourceLabel,
+ accesskey: removeInSourceKey,
+ disabled: false,
+ click: () => removeBreakpointsInSource(cx, source),
+ };
+
+ const hideDisableInSourceItem = breakpointsForSource.every(
+ breakpoint => breakpoint.disabled
+ );
+ const hideEnableInSourceItem = breakpointsForSource.every(
+ breakpoint => !breakpoint.disabled
+ );
+
+ const items = [
+ { item: disableInSourceItem, hidden: () => hideDisableInSourceItem },
+ { item: enableInSourceItem, hidden: () => hideEnableInSourceItem },
+ { item: removeInSourceItem, hidden: () => false },
+ ];
+
+ showMenu(contextMenuEvent, buildMenu(items));
+}
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..7dae8d2304
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css
@@ -0,0 +1,253 @@
+/* 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-toggle {
+ margin: 2px 3px;
+}
+
+.breakpoints-exceptions-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-exceptions,
+.breakpoints-exceptions-caught {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-inline-start: 16px;
+ padding-inline-end: 12px;
+}
+
+.breakpoints-exceptions {
+ padding-bottom: 3px;
+ padding-top: 3px;
+ user-select: none;
+}
+
+.breakpoints-exceptions-caught {
+ padding-bottom: 3px;
+ padding-top: 3px;
+ padding-inline-start: 36px;
+}
+
+.breakpoints-exceptions-options {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.xhr-breakpoints-pane .breakpoints-exceptions-options {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-exceptions-options:not(.empty) {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-exceptions input,
+.breakpoints-exceptions-caught 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;
+}
+
+html[dir="rtl"] .breakpoints-list .breakpoint,
+html[dir="rtl"] .breakpoints-list .breakpoint-heading,
+html[dir="rtl"] .breakpoints-exceptions {
+ border-right: 4px solid transparent;
+}
+
+html:not([dir="rtl"]) .breakpoints-list .breakpoint,
+html:not([dir="rtl"]) .breakpoints-list .breakpoint-heading,
+html:not([dir="rtl"]) .breakpoints-exceptions {
+ border-left: 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/BreakpointsContextMenu.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js
new file mode 100644
index 0000000000..20f8691a6e
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { buildMenu, showMenu } from "../../../context-menu/menu";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import actions from "../../../actions";
+import { features } from "../../../utils/prefs";
+import { formatKeyShortcut } from "../../../utils/text";
+
+import type { Breakpoint, Source, Context } from "../../../types";
+
+type Props = {
+ cx: Context,
+ breakpoint: Breakpoint,
+ breakpoints: Breakpoint[],
+ selectedSource: ?Source,
+ removeBreakpoint: typeof actions.removeBreakpoint,
+ removeBreakpoints: typeof actions.removeBreakpoints,
+ removeAllBreakpoints: typeof actions.removeAllBreakpoints,
+ toggleBreakpoints: typeof actions.toggleBreakpoints,
+ toggleAllBreakpoints: typeof actions.toggleAllBreakpoints,
+ toggleDisabledBreakpoint: typeof actions.toggleDisabledBreakpoint,
+ selectSpecificLocation: typeof actions.selectSpecificLocation,
+ setBreakpointOptions: typeof actions.setBreakpointOptions,
+ openConditionalPanel: typeof actions.openConditionalPanel,
+ contextMenuEvent: SyntheticEvent<HTMLElement>,
+};
+
+export default function showContextMenu(props: Props) {
+ const {
+ cx,
+ breakpoint,
+ breakpoints,
+ selectedSource,
+ removeBreakpoint,
+ removeBreakpoints,
+ removeAllBreakpoints,
+ toggleBreakpoints,
+ toggleAllBreakpoints,
+ toggleDisabledBreakpoint,
+ selectSpecificLocation,
+ setBreakpointOptions,
+ openConditionalPanel,
+ contextMenuEvent,
+ } = props;
+
+ contextMenuEvent.preventDefault();
+
+ const deleteSelfLabel = L10N.getStr("breakpointMenuItem.deleteSelf2.label");
+ const deleteAllLabel = L10N.getStr("breakpointMenuItem.deleteAll2.label");
+ const deleteOthersLabel = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.label"
+ );
+ const enableSelfLabel = L10N.getStr("breakpointMenuItem.enableSelf2.label");
+ const enableAllLabel = L10N.getStr("breakpointMenuItem.enableAll2.label");
+ const enableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.label"
+ );
+ const disableSelfLabel = L10N.getStr("breakpointMenuItem.disableSelf2.label");
+ const disableAllLabel = L10N.getStr("breakpointMenuItem.disableAll2.label");
+ const disableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.label"
+ );
+ const enableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.enabledbg.label"
+ );
+ const disableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.disabledbg.label"
+ );
+ const removeConditionLabel = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.label"
+ );
+ const addConditionLabel = L10N.getStr(
+ "breakpointMenuItem.addCondition2.label"
+ );
+ const editConditionLabel = L10N.getStr(
+ "breakpointMenuItem.editCondition2.label"
+ );
+
+ const deleteSelfKey = L10N.getStr("breakpointMenuItem.deleteSelf2.accesskey");
+ const deleteAllKey = L10N.getStr("breakpointMenuItem.deleteAll2.accesskey");
+ const deleteOthersKey = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.accesskey"
+ );
+ const enableSelfKey = L10N.getStr("breakpointMenuItem.enableSelf2.accesskey");
+ const enableAllKey = L10N.getStr("breakpointMenuItem.enableAll2.accesskey");
+ const enableOthersKey = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.accesskey"
+ );
+ const disableSelfKey = L10N.getStr(
+ "breakpointMenuItem.disableSelf2.accesskey"
+ );
+ const disableAllKey = L10N.getStr("breakpointMenuItem.disableAll2.accesskey");
+ const disableOthersKey = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.accesskey"
+ );
+ const removeConditionKey = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.accesskey"
+ );
+ const editConditionKey = L10N.getStr(
+ "breakpointMenuItem.editCondition2.accesskey"
+ );
+ const addConditionKey = L10N.getStr(
+ "breakpointMenuItem.addCondition2.accesskey"
+ );
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const otherBreakpoints = breakpoints.filter(b => b.id !== breakpoint.id);
+ const enabledBreakpoints = breakpoints.filter(b => !b.disabled);
+ const disabledBreakpoints = breakpoints.filter(b => b.disabled);
+ const otherEnabledBreakpoints = breakpoints.filter(
+ b => !b.disabled && b.id !== breakpoint.id
+ );
+ const otherDisabledBreakpoints = breakpoints.filter(
+ b => b.disabled && b.id !== breakpoint.id
+ );
+
+ const deleteSelfItem = {
+ id: "node-menu-delete-self",
+ label: deleteSelfLabel,
+ accesskey: deleteSelfKey,
+ disabled: false,
+ click: () => {
+ removeBreakpoint(cx, breakpoint);
+ },
+ };
+
+ const deleteAllItem = {
+ id: "node-menu-delete-all",
+ label: deleteAllLabel,
+ accesskey: deleteAllKey,
+ disabled: false,
+ click: () => removeAllBreakpoints(cx),
+ };
+
+ const deleteOthersItem = {
+ id: "node-menu-delete-other",
+ label: deleteOthersLabel,
+ accesskey: deleteOthersKey,
+ disabled: false,
+ click: () => removeBreakpoints(cx, otherBreakpoints),
+ };
+
+ const enableSelfItem = {
+ id: "node-menu-enable-self",
+ label: enableSelfLabel,
+ accesskey: enableSelfKey,
+ disabled: false,
+ click: () => {
+ toggleDisabledBreakpoint(cx, breakpoint);
+ },
+ };
+
+ const enableAllItem = {
+ id: "node-menu-enable-all",
+ label: enableAllLabel,
+ accesskey: enableAllKey,
+ disabled: false,
+ click: () => toggleAllBreakpoints(cx, false),
+ };
+
+ const enableOthersItem = {
+ id: "node-menu-enable-others",
+ label: enableOthersLabel,
+ accesskey: enableOthersKey,
+ disabled: false,
+ click: () => toggleBreakpoints(cx, false, otherDisabledBreakpoints),
+ };
+
+ const disableSelfItem = {
+ id: "node-menu-disable-self",
+ label: disableSelfLabel,
+ accesskey: disableSelfKey,
+ disabled: false,
+ click: () => {
+ toggleDisabledBreakpoint(cx, breakpoint);
+ },
+ };
+
+ const disableAllItem = {
+ id: "node-menu-disable-all",
+ label: disableAllLabel,
+ accesskey: disableAllKey,
+ disabled: false,
+ click: () => toggleAllBreakpoints(cx, true),
+ };
+
+ const disableOthersItem = {
+ id: "node-menu-disable-others",
+ label: disableOthersLabel,
+ accesskey: disableOthersKey,
+ click: () => toggleBreakpoints(cx, true, otherEnabledBreakpoints),
+ };
+
+ const enableDbgStatementItem = {
+ id: "node-menu-enable-dbgStatement",
+ label: enableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ setBreakpointOptions(cx, selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ }),
+ };
+
+ const disableDbgStatementItem = {
+ id: "node-menu-disable-dbgStatement",
+ label: disableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ setBreakpointOptions(cx, selectedLocation, {
+ ...breakpoint.options,
+ condition: "false",
+ }),
+ };
+
+ const removeConditionItem = {
+ id: "node-menu-remove-condition",
+ label: removeConditionLabel,
+ accesskey: removeConditionKey,
+ disabled: false,
+ click: () =>
+ setBreakpointOptions(cx, selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ }),
+ };
+
+ const addConditionItem = {
+ id: "node-menu-add-condition",
+ label: addConditionLabel,
+ accesskey: addConditionKey,
+ click: () => {
+ selectSpecificLocation(cx, selectedLocation);
+ openConditionalPanel(selectedLocation);
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const editConditionItem = {
+ id: "node-menu-edit-condition",
+ label: editConditionLabel,
+ accesskey: editConditionKey,
+ click: () => {
+ selectSpecificLocation(cx, selectedLocation);
+ openConditionalPanel(selectedLocation);
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const addLogPointItem = {
+ id: "node-menu-add-log-point",
+ label: L10N.getStr("editor.addLogPoint"),
+ accesskey: L10N.getStr("editor.addLogPoint.accesskey"),
+ disabled: false,
+ click: () => openConditionalPanel(selectedLocation, true),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+ };
+
+ const editLogPointItem = {
+ id: "node-menu-edit-log-point",
+ label: L10N.getStr("editor.editLogPoint"),
+ accesskey: L10N.getStr("editor.editLogPoint.accesskey"),
+ disabled: false,
+ click: () => openConditionalPanel(selectedLocation, true),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+ };
+
+ const removeLogPointItem = {
+ id: "node-menu-remove-log",
+ label: L10N.getStr("editor.removeLogPoint.label"),
+ accesskey: L10N.getStr("editor.removeLogPoint.accesskey"),
+ disabled: false,
+ click: () =>
+ setBreakpointOptions(cx, selectedLocation, {
+ ...breakpoint.options,
+ logValue: null,
+ }),
+ };
+
+ const logPointItem = breakpoint.options.logValue
+ ? editLogPointItem
+ : addLogPointItem;
+
+ const hideEnableSelfItem = !breakpoint.disabled;
+ const hideEnableAllItem = disabledBreakpoints.length === 0;
+ const hideEnableOthersItem = otherDisabledBreakpoints.length === 0;
+ const hideDisableAllItem = enabledBreakpoints.length === 0;
+ const hideDisableOthersItem = otherEnabledBreakpoints.length === 0;
+ const hideDisableSelfItem = breakpoint.disabled;
+ const hideEnableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition !== "false");
+ const hideDisableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition === "false");
+ const items = [
+ { item: enableSelfItem, hidden: () => hideEnableSelfItem },
+ { item: enableAllItem, hidden: () => hideEnableAllItem },
+ { item: enableOthersItem, hidden: () => hideEnableOthersItem },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideEnableSelfItem && hideEnableAllItem && hideEnableOthersItem,
+ },
+ { item: deleteSelfItem },
+ { item: deleteAllItem },
+ { item: deleteOthersItem, hidden: () => breakpoints.length === 1 },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideDisableSelfItem && hideDisableAllItem && hideDisableOthersItem,
+ },
+
+ { item: disableSelfItem, hidden: () => hideDisableSelfItem },
+ { item: disableAllItem, hidden: () => hideDisableAllItem },
+ { item: disableOthersItem, hidden: () => hideDisableOthersItem },
+ {
+ item: { type: "separator" },
+ },
+ {
+ item: enableDbgStatementItem,
+ hidden: () => hideEnableDbgStatementItem,
+ },
+ {
+ item: disableDbgStatementItem,
+ hidden: () => hideDisableDbgStatementItem,
+ },
+ {
+ item: { type: "separator" },
+ hidden: () => hideDisableDbgStatementItem && hideEnableDbgStatementItem,
+ },
+ {
+ item: addConditionItem,
+ hidden: () => breakpoint.options.condition,
+ },
+ {
+ item: editConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: removeConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: logPointItem,
+ hidden: () => !features.logPoints,
+ },
+ {
+ item: removeLogPointItem,
+ hidden: () => !features.logPoints || !breakpoint.options.logValue,
+ },
+ ];
+
+ showMenu(contextMenuEvent, buildMenu(items));
+ return null;
+}
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..4a078784c5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+
+type ExceptionOptionProps = {
+ className: string,
+ isChecked: boolean,
+ label: string,
+ onChange: Function,
+};
+
+export default function ExceptionOption({
+ className,
+ isChecked = false,
+ label,
+ onChange,
+}: ExceptionOptionProps) {
+ return (
+ <div className={className} onClick={onChange}>
+ <input
+ type="checkbox"
+ checked={isChecked ? "checked" : ""}
+ onChange={e => e.stopPropagation() && onChange()}
+ />
+ <div className="breakpoint-exceptions-label">{label}</div>
+ </div>
+ );
+}
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..06f4a7c48a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import classnames from "classnames";
+import { connect } from "../../../utils/connect";
+
+import ExceptionOption from "./ExceptionOption";
+
+import Breakpoint from "./Breakpoint";
+import BreakpointHeading from "./BreakpointHeading";
+
+import actions from "../../../actions";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { createHeadlessEditor } from "../../../utils/editor/create-editor";
+
+import {
+ makeBreakpointId,
+ sortSelectedBreakpoints,
+} from "../../../utils/breakpoint";
+
+import { getSelectedSource, getBreakpointSources } from "../../../selectors";
+
+import type { Source } from "../../../types";
+import type { BreakpointSources } from "../../../selectors/breakpointSources";
+import type SourceEditor from "../../../utils/editor/source-editor";
+
+import "./Breakpoints.css";
+
+type OwnProps = {|
+ shouldPauseOnExceptions: boolean,
+ shouldPauseOnCaughtExceptions: boolean,
+ pauseOnExceptions: Function,
+|};
+type Props = {
+ breakpointSources: BreakpointSources,
+ selectedSource: ?Source,
+ shouldPauseOnExceptions: boolean,
+ shouldPauseOnCaughtExceptions: boolean,
+ pauseOnExceptions: Function,
+};
+
+class Breakpoints extends Component<Props> {
+ headlessEditor: ?SourceEditor;
+
+ componentWillUnmount() {
+ this.removeEditor();
+ }
+
+ getEditor(): SourceEditor {
+ if (!this.headlessEditor) {
+ this.headlessEditor = createHeadlessEditor();
+ }
+ return this.headlessEditor;
+ }
+
+ removeEditor() {
+ if (!this.headlessEditor) {
+ return;
+ }
+ this.headlessEditor.destroy();
+ this.headlessEditor = (null: any);
+ }
+
+ renderExceptionsOptions() {
+ const {
+ breakpointSources,
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ pauseOnExceptions,
+ } = this.props;
+
+ const isEmpty = breakpointSources.length == 0;
+
+ return (
+ <div
+ className={classnames("breakpoints-exceptions-options", {
+ empty: isEmpty,
+ })}
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ label={L10N.getStr("pauseOnExceptionsItem2")}
+ isChecked={shouldPauseOnExceptions}
+ onChange={() => pauseOnExceptions(!shouldPauseOnExceptions, false)}
+ />
+
+ {shouldPauseOnExceptions && (
+ <ExceptionOption
+ className="breakpoints-exceptions-caught"
+ label={L10N.getStr("pauseOnCaughtExceptionsItem")}
+ isChecked={shouldPauseOnCaughtExceptions}
+ onChange={() =>
+ pauseOnExceptions(true, !shouldPauseOnCaughtExceptions)
+ }
+ />
+ )}
+ </div>
+ );
+ }
+
+ 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 }) => {
+ const sortedBreakpoints = sortSelectedBreakpoints(
+ breakpoints,
+ selectedSource
+ );
+
+ return [
+ <BreakpointHeading
+ key={source.id}
+ source={source}
+ sources={sources}
+ />,
+ ...sortedBreakpoints.map(breakpoint => (
+ <Breakpoint
+ breakpoint={breakpoint}
+ source={source}
+ selectedSource={selectedSource}
+ editor={editor}
+ key={makeBreakpointId(
+ getSelectedLocation(breakpoint, selectedSource)
+ )}
+ />
+ )),
+ ];
+ })}
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <div className="pane">
+ {this.renderExceptionsOptions()}
+ {this.renderBreakpoints()}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpointSources: getBreakpointSources(state),
+ selectedSource: getSelectedSource(state),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ 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..2b075efdd4
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build
@@ -0,0 +1,15 @@
+# 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",
+ "BreakpointHeadingsContextMenu.js",
+ "BreakpointsContextMenu.js",
+ "ExceptionOption.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/Breakpoint.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/Breakpoint.spec.js
new file mode 100644
index 0000000000..986d2b972d
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/Breakpoint.spec.js
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import Breakpoint from "../Breakpoint";
+import {
+ createSourceObject,
+ createOriginalSourceObject,
+} from "../../../../utils/test-head";
+
+describe("Breakpoint", () => {
+ it("simple", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("disabled", () => {
+ const { component } = render({}, makeBreakpoint({ disabled: true }));
+ expect(component).toMatchSnapshot();
+ });
+
+ it("paused at a generatedLocation", () => {
+ const { component } = render({
+ frame: { selectedLocation: generatedLocation },
+ });
+ expect(component).toMatchSnapshot();
+ });
+
+ it("paused at an original location", () => {
+ const source = createSourceObject("foo");
+ const origSource = createOriginalSourceObject(source);
+
+ const { component } = render(
+ {
+ selectedSource: origSource,
+ frame: { selectedLocation: location },
+ },
+ { location, options: {} }
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it("paused at a different", () => {
+ const { component } = render({
+ frame: { selectedLocation: { ...generatedLocation, line: 14 } },
+ });
+ expect(component).toMatchSnapshot();
+ });
+});
+
+const generatedLocation = { sourceId: "foo", line: 53, column: 73 };
+const location = { sourceId: "foo/original", line: 5, column: 7 };
+
+function render(overrides = {}, breakpointOverrides = {}) {
+ const props = generateDefaults(overrides, breakpointOverrides);
+ // $FlowIgnore
+ const component = shallow(<Breakpoint.WrappedComponent {...props} />);
+ const defaultState = component.state();
+ const instance = component.instance();
+
+ return { component, props, defaultState, instance };
+}
+
+function makeBreakpoint(overrides = {}) {
+ return {
+ location,
+ generatedLocation,
+ disabled: false,
+ options: {},
+ ...overrides,
+ id: 1,
+ };
+}
+
+function generateDefaults(overrides = {}, breakpointOverrides = {}) {
+ const source = createSourceObject("foo");
+ const breakpoint = makeBreakpoint(breakpointOverrides);
+ const selectedSource = createSourceObject("foo");
+ return {
+ source,
+ breakpoint,
+ selectedSource,
+ frame: (null: any),
+ editor: {
+ CodeMirror: {
+ runMode: function() {
+ return "";
+ },
+ },
+ },
+ ...overrides,
+ };
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/BreakpointsContextMenu.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/BreakpointsContextMenu.spec.js
new file mode 100644
index 0000000000..4797a3a69a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/BreakpointsContextMenu.spec.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import BreakpointsContextMenu from "../BreakpointsContextMenu";
+import { buildMenu } from "../../../../context-menu/menu";
+
+import {
+ makeMockBreakpoint,
+ makeMockSource,
+ mockcx,
+} from "../../../../utils/test-mockup";
+
+jest.mock("../../../../context-menu/menu");
+
+function render(disabled = false) {
+ const props = generateDefaults(disabled);
+ const component = shallow(<BreakpointsContextMenu {...props} />);
+ return { component, props };
+}
+
+function generateDefaults(disabled) {
+ const source = makeMockSource(
+ "https://example.com/main.js",
+ "source-https://example.com/main.js"
+ );
+ const breakpoints = [
+ {
+ ...makeMockBreakpoint(source, 1),
+ id: "https://example.com/main.js:1:",
+ disabled,
+ options: {
+ condition: "",
+ logValue: "",
+ hidden: false,
+ },
+ },
+ {
+ ...makeMockBreakpoint(source, 2),
+ id: "https://example.com/main.js:2:",
+ disabled,
+ options: {
+ hidden: false,
+ },
+ },
+ {
+ ...makeMockBreakpoint(source, 3),
+ id: "https://example.com/main.js:3:",
+ disabled,
+ },
+ ];
+
+ const props = {
+ cx: mockcx,
+ breakpoints,
+ breakpoint: breakpoints[0],
+ removeBreakpoint: jest.fn(),
+ removeBreakpoints: jest.fn(),
+ removeAllBreakpoints: jest.fn(),
+ toggleBreakpoints: jest.fn(),
+ toggleAllBreakpoints: jest.fn(),
+ toggleDisabledBreakpoint: jest.fn(),
+ selectSpecificLocation: jest.fn(),
+ setBreakpointCondition: jest.fn(),
+ openConditionalPanel: jest.fn(),
+ contextMenuEvent: ({ preventDefault: jest.fn() }: any),
+ selectedSource: makeMockSource(),
+ setBreakpointOptions: jest.fn(),
+ };
+ return props;
+}
+
+describe("BreakpointsContextMenu", () => {
+ afterEach(() => {
+ buildMenu.mockReset();
+ });
+
+ describe("context menu actions affecting other breakpoints", () => {
+ it("'remove others' calls removeBreakpoints with proper arguments", () => {
+ const { props } = render();
+ const menuItems = buildMenu.mock.calls[0][0];
+ const deleteOthers = menuItems.find(
+ item => item.item.id === "node-menu-delete-other"
+ );
+ deleteOthers.item.click();
+
+ expect(props.removeBreakpoints).toHaveBeenCalled();
+
+ const otherBreakpoints = [props.breakpoints[1], props.breakpoints[2]];
+ expect(props.removeBreakpoints.mock.calls[0][1]).toEqual(
+ otherBreakpoints
+ );
+ });
+
+ it("'enable others' calls toggleBreakpoints with proper arguments", () => {
+ const { props } = render(true);
+ const menuItems = buildMenu.mock.calls[0][0];
+ const enableOthers = menuItems.find(
+ item => item.item.id === "node-menu-enable-others"
+ );
+ enableOthers.item.click();
+
+ expect(props.toggleBreakpoints).toHaveBeenCalled();
+
+ expect(props.toggleBreakpoints.mock.calls[0][1]).toBe(false);
+
+ const otherBreakpoints = [props.breakpoints[1], props.breakpoints[2]];
+ expect(props.toggleBreakpoints.mock.calls[0][2]).toEqual(
+ otherBreakpoints
+ );
+ });
+
+ it("'disable others' calls toggleBreakpoints with proper arguments", () => {
+ const { props } = render();
+ const menuItems = buildMenu.mock.calls[0][0];
+ const disableOthers = menuItems.find(
+ item => item.item.id === "node-menu-disable-others"
+ );
+ disableOthers.item.click();
+
+ expect(props.toggleBreakpoints).toHaveBeenCalled();
+ expect(props.toggleBreakpoints.mock.calls[0][1]).toBe(true);
+
+ const otherBreakpoints = [props.breakpoints[1], props.breakpoints[2]];
+ expect(props.toggleBreakpoints.mock.calls[0][2]).toEqual(
+ otherBreakpoints
+ );
+ });
+ });
+});
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..86be19740e
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import ExceptionOption from "../ExceptionOption";
+
+describe("ExceptionOption renders", () => {
+ it("with values", () => {
+ const component = shallow(
+ <ExceptionOption
+ label="testLabel"
+ isChecked={true}
+ onChange={() => null}
+ className="testClassName"
+ />
+ );
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap
new file mode 100644
index 0000000000..03aef3ad6f
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap
@@ -0,0 +1,226 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Breakpoint disabled 1`] = `
+<div
+ className="breakpoint disabled"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ onDoubleClick={[Function]}
+>
+ <input
+ aria-labelledby="1-label"
+ checked={false}
+ className="breakpoint-checkbox"
+ id={1}
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <span
+ className="breakpoint-label cm-s-mozilla devtools-monospace"
+ id="1-label"
+ onClick={[Function]}
+ >
+ <span
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "",
+ }
+ }
+ />
+ </span>
+ <div
+ className="breakpoint-line-close"
+ >
+ <div
+ className="breakpoint-line devtools-monospace"
+ >
+ 53:73
+ </div>
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove breakpoint"
+ />
+ </div>
+</div>
+`;
+
+exports[`Breakpoint paused at a different 1`] = `
+<div
+ className="breakpoint"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ onDoubleClick={[Function]}
+>
+ <input
+ aria-labelledby="1-label"
+ checked={true}
+ className="breakpoint-checkbox"
+ id={1}
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <span
+ className="breakpoint-label cm-s-mozilla devtools-monospace"
+ id="1-label"
+ onClick={[Function]}
+ >
+ <span
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "",
+ }
+ }
+ />
+ </span>
+ <div
+ className="breakpoint-line-close"
+ >
+ <div
+ className="breakpoint-line devtools-monospace"
+ >
+ 53:73
+ </div>
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove breakpoint"
+ />
+ </div>
+</div>
+`;
+
+exports[`Breakpoint paused at a generatedLocation 1`] = `
+<div
+ className="breakpoint paused"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ onDoubleClick={[Function]}
+>
+ <input
+ aria-labelledby="1-label"
+ checked={true}
+ className="breakpoint-checkbox"
+ id={1}
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <span
+ className="breakpoint-label cm-s-mozilla devtools-monospace"
+ id="1-label"
+ onClick={[Function]}
+ >
+ <span
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "",
+ }
+ }
+ />
+ </span>
+ <div
+ className="breakpoint-line-close"
+ >
+ <div
+ className="breakpoint-line devtools-monospace"
+ >
+ 53:73
+ </div>
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove breakpoint"
+ />
+ </div>
+</div>
+`;
+
+exports[`Breakpoint paused at an original location 1`] = `
+<div
+ className="breakpoint paused"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ onDoubleClick={[Function]}
+>
+ <input
+ aria-labelledby="1-label"
+ checked={true}
+ className="breakpoint-checkbox"
+ id={1}
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <span
+ className="breakpoint-label cm-s-mozilla devtools-monospace"
+ id="1-label"
+ onClick={[Function]}
+ >
+ <span
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "",
+ }
+ }
+ />
+ </span>
+ <div
+ className="breakpoint-line-close"
+ >
+ <div
+ className="breakpoint-line devtools-monospace"
+ >
+ 5:7
+ </div>
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove breakpoint"
+ />
+ </div>
+</div>
+`;
+
+exports[`Breakpoint simple 1`] = `
+<div
+ className="breakpoint"
+ onClick={[Function]}
+ onContextMenu={[Function]}
+ onDoubleClick={[Function]}
+>
+ <input
+ aria-labelledby="1-label"
+ checked={true}
+ className="breakpoint-checkbox"
+ id={1}
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <span
+ className="breakpoint-label cm-s-mozilla devtools-monospace"
+ id="1-label"
+ onClick={[Function]}
+ >
+ <span
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "",
+ }
+ }
+ />
+ </span>
+ <div
+ className="breakpoint-line-close"
+ >
+ <div
+ className="breakpoint-line devtools-monospace"
+ >
+ 53:73
+ </div>
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove breakpoint"
+ />
+ </div>
+</div>
+`;
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..19b5937676
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExceptionOption renders with values 1`] = `
+<div
+ className="testClassName"
+ onClick={[Function]}
+>
+ <input
+ checked="checked"
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ testLabel
+ </div>
+</div>
+`;
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..b6298fad6b
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import PropTypes from "prop-types";
+import React, { Component } from "react";
+
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+import { features, prefs } from "../../utils/prefs";
+import {
+ getIsWaitingOnBreak,
+ getSkipPausing,
+ getCurrentThread,
+ isTopFrameSelected,
+ getThreadContext,
+} from "../../selectors";
+import { formatKeyShortcut } from "../../utils/text";
+import actions from "../../actions";
+import { debugBtn } from "../shared/Button/CommandBarButton";
+import AccessibleImage from "../shared/AccessibleImage";
+import "./CommandBar.css";
+
+import { appinfo } from "devtools-services";
+import type { ThreadContext } from "../../types";
+
+// $FlowIgnore
+const MenuButton = require("devtools/client/shared/components/menu/MenuButton");
+// $FlowIgnore
+const MenuItem = require("devtools/client/shared/components/menu/MenuItem");
+// $FlowIgnore
+const MenuList = require("devtools/client/shared/components/menu/MenuList");
+
+const isMacOS = 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"];
+type CommandActionType = "resume" | "stepOver" | "stepIn" | "stepOut";
+
+const KEYS = {
+ WINNT: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ },
+ Darwin: {
+ resume: "Cmd+\\",
+ stepOver: "Cmd+'",
+ stepIn: "Cmd+;",
+ stepOut: "Cmd+Shift+:",
+ stepOutDisplay: "Cmd+Shift+;",
+ },
+ Linux: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ },
+};
+
+function getKey(action) {
+ return getKeyForOS(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);
+ if (isMacOS) {
+ const winKey =
+ getKeyForOS("WINNT", `${action}Display`) || getKeyForOS("WINNT", action);
+ // display both Windows type and Mac specific keys
+ return formatKeyShortcut([key, winKey].join(" "));
+ }
+ return formatKeyShortcut(key);
+}
+
+type OwnProps = {|
+ horizontal: boolean,
+|};
+type Props = {
+ cx: ThreadContext,
+ isWaitingOnBreak: boolean,
+ horizontal: boolean,
+ skipPausing: boolean,
+ javascriptEnabled: boolean,
+ topFrameSelected: boolean,
+ resume: typeof actions.resume,
+ stepIn: typeof actions.stepIn,
+ stepOut: typeof actions.stepOut,
+ stepOver: typeof actions.stepOver,
+ breakOnNext: typeof actions.breakOnNext,
+ pauseOnExceptions: typeof actions.pauseOnExceptions,
+ toggleSkipPausing: typeof actions.toggleSkipPausing,
+ toggleInlinePreview: typeof actions.toggleInlinePreview,
+ toggleEditorWrapping: typeof actions.toggleEditorWrapping,
+ toggleSourceMapsEnabled: typeof actions.toggleSourceMapsEnabled,
+ toggleJavaScriptEnabled: typeof actions.toggleJavaScriptEnabled,
+};
+
+class CommandBar extends Component<Props> {
+ 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: Event, action: CommandActionType) {
+ const { cx } = this.props;
+ e.preventDefault();
+ e.stopPropagation();
+ if (action === "resume") {
+ this.props.cx.isPaused
+ ? this.props.resume(cx)
+ : this.props.breakOnNext(cx);
+ } else {
+ this.props[action](cx);
+ }
+ }
+
+ renderStepButtons() {
+ const { cx, topFrameSelected } = this.props;
+ const className = cx.isPaused ? "active" : "disabled";
+ const isDisabled = !cx.isPaused;
+
+ return [
+ this.renderPauseButton(),
+ debugBtn(
+ () => this.props.stepOver(cx),
+ "stepOver",
+ className,
+ L10N.getFormatStr("stepOverTooltip", formatKey("stepOver")),
+ isDisabled
+ ),
+ debugBtn(
+ () => this.props.stepIn(cx),
+ "stepIn",
+ className,
+ L10N.getFormatStr("stepInTooltip", formatKey("stepIn")),
+ isDisabled || (features.frameStep && !topFrameSelected)
+ ),
+ debugBtn(
+ () => this.props.stepOut(cx),
+ "stepOut",
+ className,
+ L10N.getFormatStr("stepOutTooltip", formatKey("stepOut")),
+ isDisabled
+ ),
+ ];
+ }
+
+ resume() {
+ this.props.resume(this.props.cx);
+ }
+
+ renderPauseButton() {
+ const { cx, breakOnNext, isWaitingOnBreak } = this.props;
+
+ if (cx.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(cx),
+ "pause",
+ "active",
+ L10N.getFormatStr("pauseButtonTooltip", formatKey("resume"))
+ );
+ }
+
+ renderSkipPausingButton() {
+ const { skipPausing, toggleSkipPausing } = this.props;
+
+ if (!features.skipPausing) {
+ return null;
+ }
+
+ 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}
+ >
+ <AccessibleImage
+ className={skipPausing ? "enable-pausing" : "disable-pausing"}
+ />
+ </button>
+ );
+ }
+
+ renderSettingsButton() {
+ const { toolboxDoc } = this.context;
+
+ return (
+ <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()}
+ </MenuButton>
+ );
+ }
+
+ renderSettingsMenuItems() {
+ return (
+ <MenuList id="debugger-settings-menu-list">
+ <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);
+ }}
+ />
+ <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)
+ }
+ />
+ <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)}
+ />
+ <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)
+ }
+ />
+ </MenuList>
+ );
+ }
+
+ render() {
+ return (
+ <div
+ className={classnames("command-bar", {
+ vertical: !this.props.horizontal,
+ })}
+ >
+ {this.renderStepButtons()}
+ <div className="filler" />
+ {this.renderSkipPausingButton()}
+ <div className="devtools-separator" />
+ {this.renderSettingsButton()}
+ </div>
+ );
+ }
+}
+
+CommandBar.contextTypes = {
+ shortcuts: PropTypes.object,
+ toolboxDoc: PropTypes.object,
+};
+
+const mapStateToProps = state => ({
+ cx: getThreadContext(state),
+ isWaitingOnBreak: getIsWaitingOnBreak(state, getCurrentThread(state)),
+ skipPausing: getSkipPausing(state),
+ topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
+ javascriptEnabled: state.ui.javascriptEnabled,
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ 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,
+})(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..71d523d927
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js
@@ -0,0 +1,163 @@
+/* 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 "react";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+import { translateNodeFrontToGrip } from "inspector-shared-utils";
+
+import {
+ deleteDOMMutationBreakpoint,
+ toggleDOMMutationBreakpointState,
+} from "framework-actions";
+
+import actions from "../../actions";
+import { connect } from "../../utils/connect";
+
+import { CloseButton } from "../shared/Button";
+
+import "./DOMMutationBreakpoints.css";
+import type { DOMMutationBreakpoint } from "../../types";
+
+type Props = {
+ breakpoints: DOMMutationBreakpoint[],
+ openElementInInspector: typeof actions.openElementInInspectorCommand,
+ highlightDomElement: typeof actions.highlightDomElement,
+ unHighlightDomElement: typeof actions.unHighlightDomElement,
+ openInspector: typeof actions.openInspector,
+ deleteBreakpoint: typeof deleteDOMMutationBreakpoint,
+ toggleBreakpoint: typeof toggleDOMMutationBreakpointState,
+ setSkipPausing: typeof actions.setSkipPausing,
+};
+
+const localizationTerms = {
+ subtree: L10N.getStr("domMutationTypes.subtree"),
+ attribute: L10N.getStr("domMutationTypes.attribute"),
+ removal: L10N.getStr("domMutationTypes.removal"),
+};
+
+class DOMMutationBreakpointsContents extends Component<Props> {
+ 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: DOMMutationBreakpoint) {
+ 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>
+ <div className="dom-mutation-type">
+ {localizationTerms[mutationType] || mutationType}
+ </div>
+ </div>
+ <CloseButton
+ handleClick={() => deleteBreakpoint(nodeFront, mutationType)}
+ />
+ </li>
+ );
+ }
+
+ /* 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 }}
+ />
+ </div>
+ );
+ }
+
+ render() {
+ const { breakpoints } = this.props;
+
+ if (breakpoints.length === 0) {
+ return this.renderEmpty();
+ }
+
+ return (
+ <ul className="dom-mutation-list">
+ {breakpoints.map(breakpoint => this.renderItem(breakpoint))}
+ </ul>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpoints: state.domMutationBreakpoints.breakpoints,
+});
+
+const DOMMutationBreakpointsPanel = connect(
+ mapStateToProps,
+ {
+ deleteBreakpoint: deleteDOMMutationBreakpoint,
+ toggleBreakpoint: toggleDOMMutationBreakpointState,
+ },
+ undefined,
+ { storeKey: "toolbox-store" }
+)(DOMMutationBreakpointsContents);
+
+class DomMutationBreakpoints extends Component<Props> {
+ render() {
+ return (
+ <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..2ca0670367
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css
@@ -0,0 +1,154 @@
+/* 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;
+}
+
+.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;
+ outline: 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;
+}
+
+: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..0c94b38969
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import classnames from "classnames";
+
+import { connect } from "../../utils/connect";
+import actions from "../../actions";
+import {
+ getActiveEventListeners,
+ getEventListenerBreakpointTypes,
+ getEventListenerExpanded,
+} from "../../selectors";
+
+import AccessibleImage from "../shared/AccessibleImage";
+
+import type {
+ EventListenerEvent,
+ EventListenerActiveList,
+ EventListenerCategory,
+ EventListenerCategoryList,
+ EventListenerExpandedList,
+} from "../../actions/types";
+
+import "./EventListeners.css";
+
+type State = {
+ searchText: string,
+ focused: boolean,
+};
+
+type OwnProps = {||};
+type Props = {
+ categories: EventListenerCategoryList,
+ expandedCategories: EventListenerExpandedList,
+ activeEventListeners: EventListenerActiveList,
+ addEventListeners: typeof actions.addEventListenerBreakpoints,
+ removeEventListeners: typeof actions.removeEventListenerBreakpoints,
+ addEventListenerExpanded: typeof actions.addEventListenerExpanded,
+ removeEventListenerExpanded: typeof actions.removeEventListenerExpanded,
+};
+
+class EventListeners extends Component<Props, State> {
+ state = {
+ searchText: "",
+ focused: false,
+ };
+
+ hasMatch(eventOrCategoryName: string, searchText: string) {
+ 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: string) {
+ const {
+ expandedCategories,
+ removeEventListenerExpanded,
+ addEventListenerExpanded,
+ } = this.props;
+
+ if (expandedCategories.includes(category)) {
+ removeEventListenerExpanded(category);
+ } else {
+ addEventListenerExpanded(category);
+ }
+ }
+
+ onCategoryClick(category: EventListenerCategory, isChecked: boolean) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ const eventsIds = category.events.map(event => event.id);
+
+ if (isChecked) {
+ addEventListeners(eventsIds);
+ } else {
+ removeEventListeners(eventsIds);
+ }
+ }
+
+ onEventTypeClick(eventId: string, isChecked: boolean) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ if (isChecked) {
+ addEventListeners([eventId]);
+ } else {
+ removeEventListeners([eventId]);
+ }
+ }
+
+ onInputChange = (event: SyntheticEvent<HTMLInputElement>) => {
+ this.setState({ searchText: event.currentTarget.value });
+ };
+
+ onKeyDown = (event: SyntheticKeyboardEvent<HTMLElement>) => {
+ if (event.key === "Escape") {
+ this.setState({ searchText: "" });
+ }
+ };
+
+ onFocus = (event: SyntheticEvent<>) => {
+ this.setState({ focused: true });
+ };
+
+ onBlur = (event: SyntheticEvent<>) => {
+ 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}
+ />
+ </form>
+ );
+ }
+
+ 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)}
+ </li>
+ );
+ })}
+ </ul>
+ );
+ }
+
+ 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);
+ });
+ })}
+ </ul>
+ );
+ }
+
+ renderCategoryHeading(category: EventListenerCategory) {
+ 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)}
+ >
+ <AccessibleImage className={classnames("arrow", { expanded })} />
+ </button>
+ <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}</span>
+ </label>
+ </div>
+ );
+ }
+
+ renderCategoryListing(category: EventListenerCategory) {
+ const { expandedCategories } = this.props;
+
+ const expanded = expandedCategories.includes(category.name);
+ if (!expanded) {
+ return null;
+ }
+
+ return (
+ <ul>
+ {category.events.map(event => {
+ return this.renderListenerEvent(event, category.name);
+ })}
+ </ul>
+ );
+ }
+
+ renderCategory(category: string) {
+ return <span className="category-label">{category} â–¸ </span>;
+ }
+
+ renderListenerEvent(event: EventListenerEvent, category: string) {
+ 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}
+ </span>
+ </label>
+ </li>
+ );
+ }
+
+ render() {
+ const { searchText } = this.state;
+
+ return (
+ <div className="event-listeners">
+ <div className="event-search-container">
+ {this.renderSearchInput()}
+ {this.renderClearSearchButton()}
+ </div>
+ <div className="event-listeners-content">
+ {searchText
+ ? this.renderSearchResultsList()
+ : this.renderCategoriesList()}
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ activeEventListeners: getActiveEventListeners(state),
+ categories: getEventListenerBreakpointTypes(state),
+ expandedCategories: getEventListenerExpanded(state),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(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..c4291c80ff
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css
@@ -0,0 +1,175 @@
+/* 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-input-container.error {
+ border: 1px solid red;
+}
+
+.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..f242962122
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
@@ -0,0 +1,426 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { Component } from "react";
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+import { features } from "../../utils/prefs";
+
+// $FlowIgnore
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+import actions from "../../actions";
+import {
+ getExpressions,
+ getExpressionError,
+ getAutocompleteMatchset,
+ getThreadContext,
+} from "../../selectors";
+import { getValue } from "../../utils/expressions";
+import { getGrip, getFront } from "../../utils/evaluation-result";
+
+import { CloseButton } from "../shared/Button";
+import { debounce } from "lodash";
+
+import type { List } from "immutable";
+import type { Expression, ThreadContext } from "../../types";
+
+import "./Expressions.css";
+
+const { ObjectInspector } = objectInspector;
+
+type State = {
+ editing: boolean,
+ editIndex: number,
+ inputValue: string,
+ focused: boolean,
+};
+
+type OwnProps = {|
+ showInput: boolean,
+ onExpressionAdded: () => void,
+|};
+type Props = {
+ cx: ThreadContext,
+ expressions: List<Expression>,
+ expressionError: boolean,
+ showInput: boolean,
+ autocompleteMatches: ?(string[]),
+ onExpressionAdded: () => void,
+ autocomplete: typeof actions.autocomplete,
+ clearAutocomplete: typeof actions.clearAutocomplete,
+ addExpression: typeof actions.addExpression,
+ clearExpressionError: typeof actions.clearExpressionError,
+ updateExpression: typeof actions.updateExpression,
+ deleteExpression: typeof actions.deleteExpression,
+ openLink: typeof actions.openLink,
+ openElementInInspector: typeof actions.openElementInInspectorCommand,
+ highlightDomElement: typeof actions.highlightDomElement,
+ unHighlightDomElement: typeof actions.unHighlightDomElement,
+};
+
+class Expressions extends Component<Props, State> {
+ _input: ?HTMLInputElement;
+ renderExpression: (
+ expression: Expression,
+ index: number
+ ) => React$Element<"li">;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ focused: false,
+ };
+ }
+
+ 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(() => {
+ this.props.clearExpressionError();
+ return { editing: false, editIndex: -1, inputValue: "", focused: false };
+ });
+ };
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (this.state.editing && !nextProps.expressionError) {
+ 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: Props, nextState: State) {
+ const { editing, inputValue, focused } = this.state;
+ const {
+ expressions,
+ expressionError,
+ showInput,
+ autocompleteMatches,
+ } = this.props;
+
+ return (
+ autocompleteMatches !== nextProps.autocompleteMatches ||
+ expressions !== nextProps.expressions ||
+ expressionError !== nextProps.expressionError ||
+ editing !== nextState.editing ||
+ inputValue !== nextState.inputValue ||
+ nextProps.showInput !== showInput ||
+ focused !== nextState.focused
+ );
+ }
+
+ componentDidUpdate(prevProps: Props, prevState: State) {
+ 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: Expression, index: number) {
+ this.setState({
+ inputValue: expression.input,
+ editing: true,
+ editIndex: index,
+ });
+ }
+
+ deleteExpression(
+ e: SyntheticMouseEvent<HTMLDivElement>,
+ expression: Expression
+ ) {
+ e.stopPropagation();
+ const { deleteExpression } = this.props;
+ deleteExpression(expression);
+ }
+
+ handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
+ const { target } = e;
+ if (features.autocompleteExpression) {
+ this.findAutocompleteMatches(target.value, target.selectionStart);
+ }
+ this.setState({ inputValue: target.value });
+ };
+
+ findAutocompleteMatches = debounce(
+ (value: string, selectionStart: number) => {
+ const { autocomplete } = this.props;
+ autocomplete(this.props.cx, value, selectionStart);
+ },
+ 250
+ );
+
+ handleKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Escape") {
+ this.clear();
+ }
+ };
+
+ hideInput = () => {
+ this.setState({ focused: false });
+ this.props.onExpressionAdded();
+ this.props.clearExpressionError();
+ };
+
+ onFocus = () => {
+ this.setState({ focused: true });
+ };
+
+ onBlur() {
+ this.clear();
+ this.hideInput();
+ }
+
+ handleExistingSubmit = async (
+ e: SyntheticEvent<HTMLFormElement>,
+ expression: Expression
+ ) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.props.updateExpression(
+ this.props.cx,
+ this.state.inputValue,
+ expression
+ );
+ };
+
+ handleNewSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
+ const { inputValue } = this.state;
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.props.clearExpressionError();
+ await this.props.addExpression(this.props.cx, this.state.inputValue);
+ this.setState({
+ editing: false,
+ editIndex: -1,
+ inputValue: this.props.expressionError ? inputValue : "",
+ });
+
+ this.props.clearAutocomplete();
+ };
+
+ renderExpression = (expression: Expression, index: number) => {
+ const {
+ expressionError,
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const { editing, editIndex } = this.state;
+ const { input, updating } = expression;
+ const isEditingExpr = editing && editIndex === index;
+ if (isEditingExpr || (isEditingExpr && expressionError)) {
+ return this.renderExpressionEditInput(expression);
+ }
+
+ if (updating) {
+ return;
+ }
+
+ let value = getValue(expression);
+ let front = null;
+ if (value && value.unavailable !== true) {
+ value = getGrip(value);
+ front = getFront(value);
+ }
+
+ const root = {
+ name: expression.input,
+ path: input,
+ contents: {
+ value,
+ front,
+ },
+ };
+
+ return (
+ <li className="expression-container" key={input} title={expression.input}>
+ <div className="expression-content">
+ <ObjectInspector
+ roots={[root]}
+ autoExpandDepth={0}
+ disableWrap={true}
+ openLink={openLink}
+ 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}
+ />
+ <div className="expression-container__close-btn">
+ <CloseButton
+ handleClick={e => this.deleteExpression(e, expression)}
+ tooltip={L10N.getStr("expressions.remove.tooltip")}
+ />
+ </div>
+ </div>
+ </li>
+ );
+ };
+
+ renderExpressions() {
+ const { expressions, showInput } = this.props;
+
+ return (
+ <>
+ <ul className="pane expressions-list">
+ {expressions.map(this.renderExpression)}
+ </ul>
+ {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} />;
+ })}
+ </datalist>
+ );
+ }
+ return <datalist id="autocomplete-matches" />;
+ }
+
+ renderNewExpressionInput() {
+ const { expressionError } = this.props;
+ const { editing, inputValue, focused } = this.state;
+ const error = editing === false && expressionError === true;
+ const placeholder: string = error
+ ? L10N.getStr("expressions.errorMsg")
+ : L10N.getStr("expressions.placeholder");
+
+ return (
+ <form
+ className={classnames(
+ "expression-input-container expression-input-form",
+ { focused, error }
+ )}
+ onSubmit={this.handleNewSubmit}
+ >
+ <input
+ className="input-expression"
+ type="text"
+ placeholder={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" }} />
+ </form>
+ );
+ }
+
+ renderExpressionEditInput(expression: Expression) {
+ const { expressionError } = this.props;
+ const { inputValue, editing, focused } = this.state;
+ const error = editing === true && expressionError === true;
+
+ return (
+ <form
+ key={expression.input}
+ className={classnames(
+ "expression-input-container expression-input-form",
+ { focused, error }
+ )}
+ onSubmit={(e: SyntheticEvent<HTMLFormElement>) =>
+ this.handleExistingSubmit(e, expression)
+ }
+ >
+ <input
+ className={classnames("input-expression", { error })}
+ 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" }} />
+ </form>
+ );
+ }
+
+ render() {
+ const { expressions } = this.props;
+
+ if (expressions.length === 0) {
+ return this.renderNewExpressionInput();
+ }
+
+ return this.renderExpressions();
+ }
+}
+
+const mapStateToProps = state => ({
+ cx: getThreadContext(state),
+ autocompleteMatches: (getAutocompleteMatchset(state): ?(string[])),
+ expressions: getExpressions(state),
+ expressionError: getExpressionError(state),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ autocomplete: actions.autocomplete,
+ clearAutocomplete: actions.clearAutocomplete,
+ addExpression: actions.addExpression,
+ clearExpressionError: actions.clearExpressionError,
+ 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..fc75618930
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { Component, memo } from "react";
+import PropTypes from "prop-types";
+
+import classNames from "classnames";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+import { formatDisplayName } from "../../../utils/pause/frames";
+import { getFilename, getFileURL } from "../../../utils/source";
+import FrameMenu from "./FrameMenu";
+import FrameIndent from "./FrameIndent";
+import actions from "../../../actions";
+
+import type { Frame, ThreadContext } from "../../../types";
+
+type FrameTitleProps = {
+ frame: Frame,
+ options: Object,
+ l10n: Object,
+};
+
+function FrameTitle({ frame, options = {}, l10n }: FrameTitleProps) {
+ const displayName = formatDisplayName(frame, options, l10n);
+ return <span className="title">{displayName}</span>;
+}
+
+type FrameLocationProps = { frame: Frame, displayFullUrl: boolean };
+
+const FrameLocation = memo(
+ ({ frame, displayFullUrl = false }: FrameLocationProps) => {
+ if (!frame.source) {
+ return null;
+ }
+
+ if (frame.library) {
+ return (
+ <span className="location">
+ {frame.library}
+ <AccessibleImage
+ className={`annotation-logo ${frame.library.toLowerCase()}`}
+ />
+ </span>
+ );
+ }
+
+ const { location, source } = frame;
+ const filename = displayFullUrl
+ ? getFileURL(source, false)
+ : getFilename(source);
+
+ return (
+ <span className="location" title={source.url}>
+ <span className="filename">{filename}</span>:
+ <span className="line">{location.line}</span>
+ </span>
+ );
+ }
+);
+
+FrameLocation.displayName = "FrameLocation";
+
+type FrameComponentProps = {
+ cx: ThreadContext,
+ frame: Frame,
+ selectedFrame: Frame,
+ copyStackTrace: Function,
+ toggleFrameworkGrouping: Function,
+ selectFrame: typeof actions.selectFrame,
+ selectLocation: typeof actions.selectLocation,
+ frameworkGroupingOn: boolean,
+ hideLocation: boolean,
+ shouldMapDisplayName: boolean,
+ toggleBlackBox: Function,
+ displayFullUrl: boolean,
+ getFrameTitle?: string => string,
+ disableContextMenu: boolean,
+ panel: "debugger" | "webconsole",
+ restart: typeof actions.restart,
+};
+
+export default class FrameComponent extends Component<FrameComponentProps> {
+ static defaultProps = {
+ hideLocation: false,
+ shouldMapDisplayName: true,
+ disableContextMenu: false,
+ };
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ get isDebugger() {
+ return this.props.panel == "debugger";
+ }
+
+ onContextMenu(event: SyntheticMouseEvent<HTMLElement>) {
+ const {
+ frame,
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ frameworkGroupingOn,
+ cx,
+ restart,
+ } = this.props;
+ FrameMenu(
+ frame,
+ frameworkGroupingOn,
+ { copyStackTrace, toggleFrameworkGrouping, toggleBlackBox, restart },
+ event,
+ cx
+ );
+ }
+
+ onMouseDown(
+ e: SyntheticMouseEvent<HTMLElement>,
+ frame: Frame,
+ selectedFrame: Frame
+ ) {
+ if (e.button !== 0) {
+ return;
+ }
+
+ this.props.selectFrame(this.props.cx, frame);
+ }
+
+ onKeyUp(
+ event: SyntheticKeyboardEvent<HTMLElement>,
+ frame: Frame,
+ selectedFrame: Frame
+ ) {
+ if (event.key != "Enter") {
+ return;
+ }
+
+ this.props.selectFrame(this.props.cx, frame);
+ }
+
+ render() {
+ const {
+ frame,
+ selectedFrame,
+ hideLocation,
+ shouldMapDisplayName,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ } = this.props;
+ const { l10n } = this.context;
+
+ const className = classNames("frame", {
+ selected: selectedFrame && selectedFrame.id === frame.id,
+ });
+
+ if (!frame.source) {
+ throw new Error("no frame source");
+ }
+
+ const title = getFrameTitle
+ ? getFrameTitle(
+ `${getFileURL(frame.source, false)}:${frame.location.line}`
+ )
+ : undefined;
+
+ return (
+ <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 && (
+ <span className="location-async-cause">
+ {this.isSelectable && <FrameIndent />}
+ {this.isDebugger ? (
+ <span className="async-label">{frame.asyncCause}</span>
+ ) : (
+ l10n.getFormatStr("stacktrace.asyncStack", frame.asyncCause)
+ )}
+ {this.isSelectable && <br className="clipboard-only" />}
+ </span>
+ )}
+ {this.isSelectable && <FrameIndent />}
+ <FrameTitle
+ frame={frame}
+ options={{ shouldMapDisplayName }}
+ l10n={l10n}
+ />
+ {!hideLocation && <span className="clipboard-only"> </span>}
+ {!hideLocation && (
+ <FrameLocation frame={frame} displayFullUrl={displayFullUrl} />
+ )}
+ {this.isSelectable && <br className="clipboard-only" />}
+ </div>
+ );
+ }
+}
+
+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..1b80e88cef
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+
+export default function FrameIndent() {
+ return (
+ <span className="frame-indent clipboard-only">
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ </span>
+ );
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameMenu.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameMenu.js
new file mode 100644
index 0000000000..d535a4b19e
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameMenu.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import { showMenu } from "../../../context-menu/menu";
+import { copyToTheClipboard } from "../../../utils/clipboard";
+import type { ContextMenuItem, Frame, ThreadContext } from "../../../types";
+import { kebabCase } from "lodash";
+
+const blackboxString = "ignoreContextItem.ignore";
+const unblackboxString = "ignoreContextItem.unignore";
+
+function formatMenuElement(
+ labelString: string,
+ click: Function,
+ disabled: boolean = false
+): ContextMenuItem {
+ const label = L10N.getStr(labelString);
+ const accesskey = L10N.getStr(`${labelString}.accesskey`);
+ const id = `node-menu-${kebabCase(label)}`;
+ return {
+ id,
+ label,
+ accesskey,
+ disabled,
+ click,
+ };
+}
+
+function copySourceElement(url) {
+ return formatMenuElement("copySourceUri2", () => copyToTheClipboard(url));
+}
+
+function copyStackTraceElement(copyStackTrace) {
+ return formatMenuElement("copyStackTrace", () => copyStackTrace());
+}
+
+function toggleFrameworkGroupingElement(
+ toggleFrameworkGrouping,
+ frameworkGroupingOn
+) {
+ const actionType = frameworkGroupingOn
+ ? "framework.disableGrouping"
+ : "framework.enableGrouping";
+
+ return formatMenuElement(actionType, () => toggleFrameworkGrouping());
+}
+
+function blackBoxSource(cx, source, toggleBlackBox) {
+ const toggleBlackBoxString = source.isBlackBoxed
+ ? unblackboxString
+ : blackboxString;
+
+ return formatMenuElement(toggleBlackBoxString, () =>
+ toggleBlackBox(cx, source)
+ );
+}
+
+function restartFrame(cx, frame, restart) {
+ return formatMenuElement("restartFrame", () => restart(cx, frame));
+}
+
+function isValidRestartFrame(frame: Frame, callbacks: Object) {
+ // Hides 'Restart Frame' item for call stack groups context menu,
+ // otherwise can be misleading for the user which frame gets restarted.
+ if (!callbacks.restart) {
+ return false;
+ }
+
+ // Any frame state than 'on-stack' is either dismissed by the server
+ // or can potentially cause unexpected errors.
+ // Global frame has frame.callee equal to null and can't be restarted.
+ return frame.type === "call" && frame.state === "on-stack";
+}
+
+export default function FrameMenu(
+ frame: Frame,
+ frameworkGroupingOn: boolean,
+ callbacks: Object,
+ event: SyntheticMouseEvent<HTMLElement>,
+ cx: ThreadContext
+) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const menuOptions = [];
+
+ if (isValidRestartFrame(frame, callbacks)) {
+ const restartFrameItem = restartFrame(cx, frame, callbacks.restart);
+ menuOptions.push(restartFrameItem);
+ }
+
+ const toggleFrameworkElement = toggleFrameworkGroupingElement(
+ callbacks.toggleFrameworkGrouping,
+ frameworkGroupingOn
+ );
+ menuOptions.push(toggleFrameworkElement);
+
+ const { source } = frame;
+ if (source) {
+ const copySourceUri2 = copySourceElement(source.url);
+ menuOptions.push(copySourceUri2);
+ menuOptions.push(blackBoxSource(cx, source, callbacks.toggleBlackBox));
+ }
+
+ const copyStackTraceItem = copyStackTraceElement(callbacks.copyStackTrace);
+
+ menuOptions.push(copyStackTraceItem);
+
+ showMenu(event, menuOptions);
+}
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..a260979b74
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+
+import { getLibraryFromUrl } from "../../../utils/pause/frames";
+
+import FrameMenu from "./FrameMenu";
+import AccessibleImage from "../../shared/AccessibleImage";
+import FrameComponent from "./Frame";
+
+import "./Group.css";
+
+import actions from "../../../actions";
+import type { Frame, ThreadContext } from "../../../types";
+import Badge from "../../shared/Badge";
+import FrameIndent from "./FrameIndent";
+
+type FrameLocationProps = { frame: Frame, expanded: boolean };
+function FrameLocation({ frame, expanded }: FrameLocationProps) {
+ const library = frame.library || getLibraryFromUrl(frame);
+ if (!library) {
+ return null;
+ }
+
+ const arrowClassName = classNames("arrow", { expanded });
+ return (
+ <span className="group-description">
+ <AccessibleImage className={arrowClassName} />
+ <AccessibleImage className={`annotation-logo ${library.toLowerCase()}`} />
+ <span className="group-description-name">{library}</span>
+ </span>
+ );
+}
+
+FrameLocation.displayName = "FrameLocation";
+
+type Props = {
+ cx: ThreadContext,
+ group: Frame[],
+ selectedFrame: Frame,
+ selectFrame: typeof actions.selectFrame,
+ selectLocation: typeof actions.selectLocation,
+ toggleFrameworkGrouping: Function,
+ copyStackTrace: Function,
+ toggleBlackBox: Function,
+ frameworkGroupingOn: boolean,
+ displayFullUrl: boolean,
+ getFrameTitle?: string => string,
+ disableContextMenu: boolean,
+ panel: "debugger" | "webconsole",
+ restart: typeof actions.restart,
+};
+
+type State = {
+ expanded: boolean,
+};
+
+export default class Group extends Component<Props, State> {
+ toggleFrames: Function;
+
+ constructor(...args: any[]) {
+ super(...args);
+ this.state = { expanded: false };
+ }
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ onContextMenu(event: SyntheticMouseEvent<HTMLElement>) {
+ const {
+ group,
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ frameworkGroupingOn,
+ cx,
+ } = this.props;
+ const frame = group[0];
+ FrameMenu(
+ frame,
+ frameworkGroupingOn,
+ { copyStackTrace, toggleFrameworkGrouping, toggleBlackBox },
+ event,
+ cx
+ );
+ }
+
+ toggleFrames = (event: SyntheticMouseEvent<HTMLElement>) => {
+ event.stopPropagation();
+ this.setState(prevState => ({ expanded: !prevState.expanded }));
+ };
+
+ renderFrames() {
+ const {
+ cx,
+ group,
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ toggleFrameworkGrouping,
+ frameworkGroupingOn,
+ toggleBlackBox,
+ copyStackTrace,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ restart,
+ } = this.props;
+
+ const { expanded } = this.state;
+ if (!expanded) {
+ return null;
+ }
+
+ return (
+ <div className="frames-list">
+ {group.reduce((acc, frame, i) => {
+ if (this.isSelectable) {
+ acc.push(<FrameIndent key={`frame-indent-${i}`} />);
+ }
+ return acc.concat(
+ <FrameComponent
+ cx={cx}
+ copyStackTrace={copyStackTrace}
+ frame={frame}
+ frameworkGroupingOn={frameworkGroupingOn}
+ hideLocation={true}
+ key={frame.id}
+ selectedFrame={selectedFrame}
+ selectFrame={selectFrame}
+ selectLocation={selectLocation}
+ shouldMapDisplayName={false}
+ toggleBlackBox={toggleBlackBox}
+ toggleFrameworkGrouping={toggleFrameworkGrouping}
+ displayFullUrl={displayFullUrl}
+ getFrameTitle={getFrameTitle}
+ disableContextMenu={disableContextMenu}
+ panel={panel}
+ restart={restart}
+ />
+ );
+ }, [])}
+ </div>
+ );
+ }
+
+ 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 (
+ <div
+ role="listitem"
+ key={frame.id}
+ className={classNames("group")}
+ onClick={this.toggleFrames}
+ tabIndex={0}
+ title={title}
+ >
+ {this.isSelectable && <FrameIndent />}
+ <FrameLocation frame={frame} expanded={expanded} />
+ {this.isSelectable && <span className="clipboard-only"> </span>}
+ <Badge>{this.props.group.length}</Badge>
+ {this.isSelectable && <br className="clipboard-only" />}
+ </div>
+ );
+ }
+
+ render() {
+ const { expanded } = this.state;
+ const { disableContextMenu } = this.props;
+ return (
+ <div
+ className={classNames("frames-group", { expanded })}
+ onContextMenu={disableContextMenu ? null : e => this.onContextMenu(e)}
+ >
+ {this.renderDescription()}
+ {this.renderFrames()}
+ </div>
+ );
+ }
+}
+
+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..0ca49590c6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import { connect } from "../../../utils/connect";
+import PropTypes from "prop-types";
+
+import type { Frame, ThreadContext } from "../../../types";
+
+import FrameComponent from "./Frame";
+import Group from "./Group";
+
+import actions from "../../../actions";
+import { collapseFrames, formatCopyName } from "../../../utils/pause/frames";
+import { copyToTheClipboard } from "../../../utils/clipboard";
+
+import {
+ getFrameworkGroupingState,
+ getSelectedFrame,
+ getCallStackFrames,
+ getCurrentThread,
+ getThreadContext,
+} from "../../../selectors";
+
+import "./Frames.css";
+
+const NUM_FRAMES_SHOWN = 7;
+
+type OwnProps = {|
+ getFrameTitle?: string => string,
+ panel: "debugger" | "webconsole",
+|};
+
+type Props = {
+ cx: ThreadContext,
+ frames: Array<Frame>,
+ frameworkGroupingOn: boolean,
+ selectedFrame: Object,
+ selectFrame: typeof actions.selectFrame,
+ selectLocation: typeof actions.selectLocation,
+ toggleBlackBox: Function,
+ toggleFrameworkGrouping: Function,
+ disableFrameTruncate: boolean,
+ disableContextMenu: boolean,
+ displayFullUrl: boolean,
+ getFrameTitle?: string => string,
+ panel: "debugger" | "webconsole",
+ restart: typeof actions.restart,
+};
+
+type State = {
+ showAllFrames: boolean,
+};
+
+class Frames extends Component<Props, State> {
+ renderFrames: Function;
+ toggleFramesDisplay: Function;
+ truncateFrames: Function;
+ copyStackTrace: Function;
+ toggleFrameworkGrouping: Function;
+ renderToggleButton: Function;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ showAllFrames: !!props.disableFrameTruncate,
+ };
+ }
+
+ shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
+ const { frames, selectedFrame, frameworkGroupingOn } = this.props;
+ const { showAllFrames } = this.state;
+ return (
+ frames !== nextProps.frames ||
+ selectedFrame !== nextProps.selectedFrame ||
+ showAllFrames !== nextState.showAllFrames ||
+ frameworkGroupingOn !== nextProps.frameworkGroupingOn
+ );
+ }
+
+ toggleFramesDisplay = (): void => {
+ this.setState(prevState => ({
+ showAllFrames: !prevState.showAllFrames,
+ }));
+ };
+
+ collapseFrames(frames: Array<Frame>) {
+ const { frameworkGroupingOn } = this.props;
+ if (!frameworkGroupingOn) {
+ return frames;
+ }
+
+ return collapseFrames(frames);
+ }
+
+ truncateFrames(frames: Array<Frame>): Array<Frame> {
+ const numFramesToShow = this.state.showAllFrames
+ ? frames.length
+ : NUM_FRAMES_SHOWN;
+
+ return frames.slice(0, numFramesToShow);
+ }
+
+ copyStackTrace = () => {
+ const { frames } = this.props;
+ const { l10n } = this.context;
+ const framesToCopy = frames.map(f => formatCopyName(f, l10n)).join("\n");
+ copyToTheClipboard(framesToCopy);
+ };
+
+ toggleFrameworkGrouping = () => {
+ const { toggleFrameworkGrouping, frameworkGroupingOn } = this.props;
+ toggleFrameworkGrouping(!frameworkGroupingOn);
+ };
+
+ renderFrames(frames: Frame[]) {
+ const {
+ cx,
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ toggleBlackBox,
+ frameworkGroupingOn,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ restart,
+ } = this.props;
+
+ const framesOrGroups = this.truncateFrames(this.collapseFrames(frames));
+ type FrameOrGroup = Frame | Frame[];
+
+ // 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 (
+ <div role="list">
+ {framesOrGroups.map((frameOrGroup: FrameOrGroup) =>
+ frameOrGroup.id ? (
+ <FrameComponent
+ cx={cx}
+ frame={(frameOrGroup: any)}
+ toggleFrameworkGrouping={this.toggleFrameworkGrouping}
+ copyStackTrace={this.copyStackTrace}
+ frameworkGroupingOn={frameworkGroupingOn}
+ selectFrame={selectFrame}
+ selectLocation={selectLocation}
+ selectedFrame={selectedFrame}
+ toggleBlackBox={toggleBlackBox}
+ key={String(frameOrGroup.id)}
+ displayFullUrl={displayFullUrl}
+ getFrameTitle={getFrameTitle}
+ disableContextMenu={disableContextMenu}
+ panel={panel}
+ restart={restart}
+ />
+ ) : (
+ <Group
+ cx={cx}
+ group={(frameOrGroup: any)}
+ toggleFrameworkGrouping={this.toggleFrameworkGrouping}
+ copyStackTrace={this.copyStackTrace}
+ frameworkGroupingOn={frameworkGroupingOn}
+ selectFrame={selectFrame}
+ selectLocation={selectLocation}
+ selectedFrame={selectedFrame}
+ toggleBlackBox={toggleBlackBox}
+ key={frameOrGroup[0].id}
+ displayFullUrl={displayFullUrl}
+ getFrameTitle={getFrameTitle}
+ disableContextMenu={disableContextMenu}
+ panel={panel}
+ restart={restart}
+ />
+ )
+ )}
+ </div>
+ );
+ }
+
+ renderToggleButton(frames: Frame[]) {
+ const { l10n } = this.context;
+ const buttonMessage = this.state.showAllFrames
+ ? l10n.getStr("callStack.collapse")
+ : l10n.getStr("callStack.expand");
+
+ frames = (this.collapseFrames(frames): any);
+ if (frames.length <= NUM_FRAMES_SHOWN) {
+ return null;
+ }
+
+ return (
+ <div className="show-more-container">
+ <button className="show-more" onClick={this.toggleFramesDisplay}>
+ {buttonMessage}
+ </button>
+ </div>
+ );
+ }
+
+ render() {
+ const { frames, disableFrameTruncate } = this.props;
+
+ if (!frames) {
+ return (
+ <div className="pane frames">
+ <div className="pane-info empty">
+ {L10N.getStr("callStack.notPaused")}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="pane frames">
+ {this.renderFrames(frames)}
+ {disableFrameTruncate ? null : this.renderToggleButton(frames)}
+ </div>
+ );
+ }
+}
+
+Frames.contextTypes = { l10n: PropTypes.object };
+
+const mapStateToProps = state => ({
+ cx: getThreadContext(state),
+ frames: getCallStackFrames(state),
+ frameworkGroupingOn: getFrameworkGroupingState(state),
+ selectedFrame: getSelectedFrame(state, getCurrentThread(state)),
+ disableFrameTruncate: false,
+ disableContextMenu: false,
+ displayFullUrl: false,
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ selectFrame: actions.selectFrame,
+ selectLocation: actions.selectLocation,
+ toggleBlackBox: actions.toggleBlackBox,
+ toggleFrameworkGrouping: actions.toggleFrameworkGrouping,
+ restart: actions.restart,
+})(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..f775363b14
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build
@@ -0,0 +1,14 @@
+# 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",
+ "FrameMenu.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..f5fbbc5014
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { shallow, mount } from "enzyme";
+import Frame from "../Frame.js";
+import {
+ makeMockFrame,
+ makeMockSource,
+ mockthreadcx,
+} from "../../../../utils/test-mockup";
+
+import FrameMenu from "../FrameMenu";
+jest.mock("../FrameMenu", () => jest.fn());
+
+function frameProperties(frame, selectedFrame: any, overrides = {}) {
+ return {
+ cx: mockthreadcx,
+ 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(<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(<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(<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(<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(<Frame {...props} />);
+ expect(component.prop("title")).toBe(`Jump to ${url}:10`);
+ expect(component).toMatchSnapshot();
+ });
+
+ describe("mouse events", () => {
+ it("does not call FrameMenu when disableContextMenu is true", () => {
+ const { component } = render(undefined, undefined, {
+ disableContextMenu: true,
+ });
+
+ const mockEvent = "mockEvent";
+ component.simulate("contextmenu", mockEvent);
+
+ expect(FrameMenu).toHaveBeenCalledTimes(0);
+ });
+
+ it("calls FrameMenu on right click", () => {
+ const { component, props } = render();
+ const {
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ cx,
+ restart,
+ } = props;
+ const mockEvent = "mockEvent";
+ component.simulate("contextmenu", mockEvent);
+
+ expect(FrameMenu).toHaveBeenCalledWith(
+ props.frame,
+ props.frameworkGroupingOn,
+ {
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ restart,
+ },
+ mockEvent,
+ cx
+ );
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/FrameMenu.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/FrameMenu.spec.js
new file mode 100644
index 0000000000..413c9928b0
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/FrameMenu.spec.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import FrameMenu from "../FrameMenu";
+import { kebabCase } from "lodash";
+
+import { showMenu } from "../../../../context-menu/menu";
+import { copyToTheClipboard } from "../../../../utils/clipboard";
+import {
+ makeMockFrame,
+ makeMockSource,
+ mockthreadcx,
+} from "../../../../utils/test-mockup";
+
+jest.mock("../../../../context-menu/menu", () => ({ showMenu: jest.fn() }));
+jest.mock("../../../../utils/clipboard", () => ({
+ copyToTheClipboard: jest.fn(),
+}));
+
+function generateMockId(labelString) {
+ const label = L10N.getStr(labelString);
+ return `node-menu-${kebabCase(label)}`;
+}
+
+describe("FrameMenu", () => {
+ let mockEvent: any;
+ let mockFrame;
+ let emptyFrame: any;
+ let callbacks;
+ let frameworkGroupingOn;
+ let toggleFrameworkGrouping;
+
+ beforeEach(() => {
+ mockFrame = makeMockFrame(undefined, makeMockSource("isFake"));
+ mockEvent = {
+ stopPropagation: jest.fn(),
+ preventDefault: jest.fn(),
+ };
+ callbacks = {
+ toggleFrameworkGrouping,
+ toggleBlackbox: jest.fn(),
+ copyToTheClipboard,
+ restart: jest.fn(),
+ };
+ emptyFrame = {};
+ });
+
+ afterEach(() => {
+ showMenu.mockClear();
+ });
+
+ it("sends three element in menuOpts to showMenu if source is present", () => {
+ const restartFrameId = generateMockId("restartFrame");
+ const sourceId = generateMockId("copySourceUri2");
+ const stacktraceId = generateMockId("copyStackTrace");
+ const frameworkGroupingId = generateMockId("framework.enableGrouping");
+ const blackBoxId = generateMockId("sourceFooter.ignore");
+
+ FrameMenu(
+ mockFrame,
+ frameworkGroupingOn,
+ callbacks,
+ mockEvent,
+ mockthreadcx
+ );
+
+ const receivedArray = showMenu.mock.calls[0][1];
+ expect(showMenu).toHaveBeenCalledWith(mockEvent, receivedArray);
+ const receivedArrayIds = receivedArray.map(item => item.id);
+ expect(receivedArrayIds).toEqual([
+ restartFrameId,
+ frameworkGroupingId,
+ sourceId,
+ blackBoxId,
+ stacktraceId,
+ ]);
+ });
+
+ it("sends one element in menuOpts without source", () => {
+ const stacktraceId = generateMockId("copyStackTrace");
+ const frameworkGrouping = generateMockId("framework.enableGrouping");
+
+ FrameMenu(
+ emptyFrame,
+ frameworkGroupingOn,
+ callbacks,
+ mockEvent,
+ mockthreadcx
+ );
+
+ const receivedArray = showMenu.mock.calls[0][1];
+ expect(showMenu).toHaveBeenCalledWith(mockEvent, receivedArray);
+ const receivedArrayIds = receivedArray.map(item => item.id);
+ expect(receivedArrayIds).toEqual([frameworkGrouping, stacktraceId]);
+ });
+
+ it("uses the disableGrouping text if frameworkGroupingOn is false", () => {
+ const stacktraceId = generateMockId("copyStackTrace");
+ const frameworkGrouping = generateMockId("framework.disableGrouping");
+
+ FrameMenu(emptyFrame, true, callbacks, mockEvent, mockthreadcx);
+
+ const receivedArray = showMenu.mock.calls[0][1];
+ const receivedArrayIds = receivedArray.map(item => item.id);
+ expect(receivedArrayIds).toEqual([frameworkGrouping, stacktraceId]);
+ });
+
+ it("uses the enableGrouping text if frameworkGroupingOn is true", () => {
+ const stacktraceId = generateMockId("copyStackTrace");
+ const frameworkGrouping = generateMockId("framework.enableGrouping");
+
+ FrameMenu(emptyFrame, false, callbacks, mockEvent, mockthreadcx);
+
+ const receivedArray = showMenu.mock.calls[0][1];
+ const receivedArrayIds = receivedArray.map(item => item.id);
+ expect(receivedArrayIds).toEqual([frameworkGrouping, stacktraceId]);
+ });
+});
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..a35361467b
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { mount, shallow } from "enzyme";
+import Frames from "../index.js";
+// eslint-disable-next-line
+import { formatCallStackFrames } from "../../../../selectors/getCallStackFrames";
+import { makeMockFrame, makeMockSource } from "../../../../utils/test-mockup";
+import { createInitial, insertResources } from "../../../../utils/resource";
+import type { SourceResourceState } from "../../../../reducers/sources";
+
+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 };
+ // $FlowIgnore
+ const component = shallow(<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: {
+ line: 55,
+ },
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ },
+ ];
+
+ const component = mount(
+ // $FlowIgnore
+ <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: {
+ line: 55,
+ },
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ },
+ ];
+ 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: {
+ line: 55,
+ },
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ },
+ {
+ id: 2,
+ library: "back",
+ displayName: "a",
+ location: {
+ line: 55,
+ },
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ },
+ {
+ id: 3,
+ library: "back",
+ displayName: "b",
+ location: {
+ line: 55,
+ },
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ },
+ ];
+ const getFrameTitle = () => {};
+ const component = render({
+ frames,
+ getFrameTitle,
+ frameworkGroupingOn: true,
+ });
+
+ expect(component.find("Group").prop("getFrameTitle")).toBe(getFrameTitle);
+ });
+ });
+
+ describe("Blackboxed Frames", () => {
+ it("filters blackboxed frames", () => {
+ const source1 = makeMockSource(undefined, "1");
+ const source2 = makeMockSource(undefined, "2");
+ (source2: any).isBlackBoxed = true;
+
+ const frames = [
+ makeMockFrame("1", source1),
+ makeMockFrame("2", source2),
+ makeMockFrame("3", source1),
+ makeMockFrame("8", source2),
+ ];
+
+ const sources: SourceResourceState = insertResources(createInitial(), [
+ { ...source1, content: null },
+ { ...source2, content: null },
+ ]);
+
+ const processedFrames = formatCallStackFrames(frames, sources, source1);
+ const selectedFrame = frames[0];
+
+ const component = render({
+ frames: processedFrames,
+ frameworkGroupingOn: false,
+ selectedFrame,
+ });
+
+ expect(component.find("Frame")).toHaveLength(2);
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe("Library Frames", () => {
+ it("toggling framework frames", () => {
+ const frames = [
+ { id: 1 },
+ { id: 2, library: "back" },
+ { id: 3, library: "back" },
+ { id: 8 },
+ ];
+
+ 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" },
+ {
+ id: "2-webpackBootstrapFrame",
+ source: { url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f" },
+ },
+ {
+ id: "3-webpackBundleFrame",
+ source: { url: "https://foo.com/bundle.js" },
+ },
+ {
+ id: "4-webpackBootstrapFrame",
+ source: { url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f" },
+ },
+ {
+ id: "5-webpackBundleFrame",
+ 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 },
+ { id: 2, library: "back" },
+ { id: 3, library: "back" },
+ { id: 8 },
+ ];
+
+ 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..5d588ff11c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+import Group from "../Group.js";
+import {
+ makeMockFrame,
+ makeMockSource,
+ mockthreadcx,
+} from "../../../../utils/test-mockup";
+
+import FrameMenu from "../FrameMenu";
+jest.mock("../FrameMenu", () => jest.fn());
+
+function render(overrides = {}) {
+ const frame = { ...makeMockFrame(), displayName: "foo", library: "Back" };
+ const defaultProps = {
+ cx: mockthreadcx,
+ 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(<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();
+ });
+
+ describe("mouse events", () => {
+ it("does not call FrameMenu when disableContextMenu is true", () => {
+ const { component } = render({
+ disableContextMenu: true,
+ });
+
+ const mockEvent = "mockEvent";
+ component.simulate("contextmenu", mockEvent);
+
+ expect(FrameMenu).toHaveBeenCalledTimes(0);
+ });
+
+ it("calls FrameMenu on right click", () => {
+ const { component, props } = render();
+ const {
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ cx,
+ } = props;
+ const mockEvent = "mockEvent";
+ component.simulate("contextmenu", mockEvent);
+
+ expect(FrameMenu).toHaveBeenCalledWith(
+ props.group[0],
+ props.frameworkGroupingOn,
+ {
+ copyStackTrace,
+ toggleFrameworkGrouping,
+ toggleBlackBox,
+ },
+ mockEvent,
+ cx
+ );
+ });
+ });
+});
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..fa15027880
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap
@@ -0,0 +1,476 @@
+// 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 />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ "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 {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ "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 />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "updateEvents",
+ "generatedLocation": Object {
+ "line": 12,
+ "sourceId": "source",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "line": 12,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "backbone.js",
+ "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 {
+ "line": 12,
+ "sourceId": "source",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "line": 12,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "backbone.js",
+ "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 />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "foo-view.js",
+ "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 {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "foo-view.js",
+ "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 />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "foo-view.js",
+ "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 {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 10,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "foo-view.js",
+ "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..fb918b5240
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap
@@ -0,0 +1,1291 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Frames Blackboxed Frames filters blackboxed frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "1",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "1",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-3",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "id": "3",
+ "index": 0,
+ "location": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "1",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "line": 4,
+ "sourceId": "1",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "1",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames groups all the Webpack-related frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": "1-appFrame",
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={false}
+ key="1-appFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Group
+ copyStackTrace={[Function]}
+ frameworkGroupingOn={true}
+ group={
+ Array [
+ Object {
+ "id": "2-webpackBootstrapFrame",
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ Object {
+ "id": "3-webpackBundleFrame",
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ Object {
+ "id": "4-webpackBootstrapFrame",
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ Object {
+ "id": "5-webpackBundleFrame",
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ ]
+ }
+ key="2-webpackBootstrapFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ }
+ }
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Group
+ copyStackTrace={[Function]}
+ frameworkGroupingOn={true}
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Group
+ copyStackTrace={[Function]}
+ frameworkGroupingOn={true}
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames disable frame truncation 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 11,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="11"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 12,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="12"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 13,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="13"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 14,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="14"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 15,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="15"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 16,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="16"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 17,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="17"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 18,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="18"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 19,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="19"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 20,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="20"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </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
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </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
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "displayName": "renderFoo",
+ "id": 1,
+ "location": Object {
+ "line": 55,
+ },
+ "source": Object {
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ }
+ }
+ frameworkGroupingOn={false}
+ getFrameTitle={[Function]}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames toggling the show more button 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ <Frame
+ copyStackTrace={[Function]}
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ frameworkGroupingOn={false}
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[Function]}
+ />
+ </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..af5e04faa0
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap
@@ -0,0 +1,1000 @@
+// 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 {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge>
+ 1
+ </Badge>
+ <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 {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/mahscripts.js",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge>
+ 3
+ </Badge>
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <FrameIndent
+ key="frame-indent-0"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/mahscripts.js",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ key="1"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ <FrameIndent
+ key="frame-indent-1"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "a",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/back.js",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ key="2"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ <FrameIndent
+ key="frame-indent-2"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "b",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/back.js",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ key="3"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ </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 {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/mahscripts.js",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge>
+ 3
+ </Badge>
+ <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 {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/mahscripts.js",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge>
+ 3
+ </Badge>
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <FrameIndent
+ key="frame-indent-0"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/mahscripts.js",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={true}
+ key="1"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ <FrameIndent
+ key="frame-indent-1"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-2",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/back.js",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={true}
+ key="2"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ <FrameIndent
+ key="frame-indent-2"
+ />
+ <Frame
+ copyStackTrace={[MockFunction]}
+ cx={
+ Object {
+ "isPaused": false,
+ "navigateCounter": 0,
+ "pauseCounter": 0,
+ "thread": "FakeThread",
+ }
+ }
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-3",
+ "generatedLocation": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 55,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "http://myfile.com/back.js",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ frameworkGroupingOn={true}
+ hideLocation={true}
+ key="3"
+ panel="webconsole"
+ restart={[MockFunction]}
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "line": 4,
+ "sourceId": "source",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "extensionName": null,
+ "id": "source",
+ "isBlackBoxed": false,
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "relativeUrl": "url",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ toggleBlackBox={[MockFunction]}
+ toggleFrameworkGrouping={[MockFunction]}
+ />
+ </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..7b8098fdce
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css
@@ -0,0 +1,103 @@
+/* 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 .pane.scopes-list {
+ font-family: var(--monospace-font-family);
+}
+
+.scopes-content .toggle-map-scopes a.mdn {
+ padding-inline-start: 3px;
+}
+
+.scopes-content .toggle-map-scopes .img.shortcuts {
+ background: var(--theme-comment);
+}
+
+.object-node.default-property {
+ opacity: 0.6;
+}
+
+.object-node {
+ padding-inline-start: 20px;
+}
+
+html[dir="rtl"] .object-node {
+ padding-right: 4px;
+}
+
+.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;
+}
+
+.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..6a5b980e09
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js
@@ -0,0 +1,325 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { PureComponent } from "react";
+import { showMenu } from "../../context-menu/menu";
+import { connect } from "../../utils/connect";
+import actions from "../../actions";
+
+import {
+ getSelectedSource,
+ getSelectedFrame,
+ getGeneratedFrameScope,
+ getOriginalFrameScope,
+ getPauseReason,
+ isMapScopesEnabled,
+ getThreadContext,
+ getLastExpandedScopes,
+} from "../../selectors";
+import { getScopes } from "../../utils/pause/scopes";
+import { getScopeItemPath } from "../../utils/pause/scopes/utils";
+
+// $FlowIgnore
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+import type { ThreadContext, Why } from "../../types";
+import type { NamedValue } from "../../utils/pause/scopes/types";
+
+import "./Scopes.css";
+
+const { ObjectInspector } = objectInspector;
+
+type OwnProps = {||};
+type Props = {
+ cx: ThreadContext,
+ selectedFrame: Object,
+ generatedFrameScopes: Object,
+ originalFrameScopes: Object | null,
+ isLoading: boolean,
+ why: ?Why,
+ mapScopesEnabled: boolean,
+ openLink: typeof actions.openLink,
+ openElementInInspector: typeof actions.openElementInInspectorCommand,
+ highlightDomElement: typeof actions.highlightDomElement,
+ unHighlightDomElement: typeof actions.unHighlightDomElement,
+ toggleMapScopes: typeof actions.toggleMapScopes,
+ setExpandedScope: typeof actions.setExpandedScope,
+ addWatchpoint: typeof actions.addWatchpoint,
+ removeWatchpoint: typeof actions.removeWatchpoint,
+ expandedScopes: string[],
+};
+
+type State = {
+ originalScopes: ?(NamedValue[]),
+ generatedScopes: ?(NamedValue[]),
+ showOriginal: boolean,
+};
+
+type Node = {
+ contents: ?{
+ watchpoint: ?"get" | "set",
+ },
+ name: string,
+ path: string,
+};
+
+class Scopes extends PureComponent<Props, State> {
+ constructor(props: Props) {
+ const {
+ why,
+ selectedFrame,
+ originalFrameScopes,
+ generatedFrameScopes,
+ } = props;
+
+ super(props);
+
+ this.state = {
+ originalScopes: getScopes(why, selectedFrame, originalFrameScopes),
+ generatedScopes: getScopes(why, selectedFrame, generatedFrameScopes),
+ showOriginal: true,
+ };
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ const {
+ cx,
+ selectedFrame,
+ originalFrameScopes,
+ generatedFrameScopes,
+ } = this.props;
+ const isPausedChanged = cx.isPaused !== nextProps.cx.isPaused;
+ const selectedFrameChanged = selectedFrame !== nextProps.selectedFrame;
+ const originalFrameScopesChanged =
+ originalFrameScopes !== nextProps.originalFrameScopes;
+ const generatedFrameScopesChanged =
+ generatedFrameScopes !== nextProps.generatedFrameScopes;
+
+ if (
+ isPausedChanged ||
+ selectedFrameChanged ||
+ originalFrameScopesChanged ||
+ generatedFrameScopesChanged
+ ) {
+ this.setState({
+ originalScopes: getScopes(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.originalFrameScopes
+ ),
+ generatedScopes: getScopes(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.generatedFrameScopes
+ ),
+ });
+ }
+ }
+
+ onToggleMapScopes = () => {
+ this.props.toggleMapScopes();
+ };
+
+ onContextMenu = (event: any, item: any) => {
+ 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];
+ return showMenu(event, menuItems);
+ }
+
+ 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: Node) => {
+ 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 {
+ cx,
+ isLoading,
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ mapScopesEnabled,
+ setExpandedScope,
+ expandedScopes,
+ } = this.props;
+ const { originalScopes, generatedScopes, showOriginal } = this.state;
+
+ const scopes =
+ (showOriginal && mapScopesEnabled && originalScopes) || generatedScopes;
+
+ function initiallyExpanded(item) {
+ return expandedScopes.some(path => path == getScopeItemPath(item));
+ }
+
+ if (scopes && scopes.length > 0 && !isLoading) {
+ return (
+ <div className="pane scopes-list">
+ <ObjectInspector
+ roots={scopes}
+ autoExpandAll={false}
+ autoExpandDepth={1}
+ disableWrap={true}
+ dimTopLevelWindow={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(cx, path, expand)}
+ initiallyExpanded={initiallyExpanded}
+ renderItemActions={this.renderWatchpointButton}
+ shouldRenderTooltip={true}
+ />
+ </div>
+ );
+ }
+
+ let stateText = L10N.getStr("scopes.notPaused");
+ if (cx.isPaused) {
+ if (isLoading) {
+ stateText = L10N.getStr("loadingText");
+ } else {
+ stateText = L10N.getStr("scopes.notAvailable");
+ }
+ }
+
+ return (
+ <div className="pane scopes-list">
+ <div className="pane-info">{stateText}</div>
+ </div>
+ );
+ }
+
+ render() {
+ return <div className="scopes-content">{this.renderScopesList()}</div>;
+ }
+}
+
+const mapStateToProps = state => {
+ const cx = getThreadContext(state);
+ const selectedFrame = getSelectedFrame(state, cx.thread);
+ const selectedSource = getSelectedSource(state);
+
+ const {
+ scope: originalFrameScopes,
+ pending: originalPending,
+ } = getOriginalFrameScope(
+ state,
+ cx.thread,
+ selectedSource?.id,
+ selectedFrame?.id
+ ) || { scope: null, pending: false };
+
+ const {
+ scope: generatedFrameScopes,
+ pending: generatedPending,
+ } = getGeneratedFrameScope(state, cx.thread, selectedFrame?.id) || {
+ scope: null,
+ pending: false,
+ };
+
+ return {
+ cx,
+ selectedFrame,
+ mapScopesEnabled: isMapScopesEnabled(state),
+ isLoading: generatedPending || originalPending,
+ why: getPauseReason(state, cx.thread),
+ originalFrameScopes,
+ generatedFrameScopes,
+ expandedScopes: getLastExpandedScopes(state, cx.thread),
+ };
+};
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+ toggleMapScopes: actions.toggleMapScopes,
+ setExpandedScope: actions.setExpandedScope,
+ addWatchpoint: actions.addWatchpoint,
+ removeWatchpoint: actions.removeWatchpoint,
+})(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..dec84252f8
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css
@@ -0,0 +1,86 @@
+/* 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;
+}
+
+.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 {
+ font-style: italic;
+ text-align: center;
+ padding: 0.5em;
+ user-select: none;
+ cursor: default;
+}
+
+.secondary-panes .breakpoints-buttons {
+ display: flex;
+}
+
+.dropdown {
+ width: 20em;
+ overflow: auto;
+}
+
+.secondary-panes input[type="checkbox"] {
+ margin: 0;
+ margin-inline-end: 4px;
+ vertical-align: middle;
+}
+
+.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..cc6d1c6350
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Thread.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+
+import actions from "../../actions";
+import { getCurrentThread, getIsPaused, getContext } from "../../selectors";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import type { Context, Thread as ThreadType } from "../../types";
+
+type OwnProps = {|
+ thread: ThreadType,
+|};
+type Props = {
+ cx: Context,
+ selectThread: typeof actions.selectThread,
+ isPaused: boolean,
+ thread: ThreadType,
+ currentThread: string,
+};
+
+export class Thread extends Component<Props> {
+ onSelectThread = () => {
+ const { thread } = this.props;
+ this.props.selectThread(this.props.cx, 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,
+ })}
+ key={thread.actor}
+ onClick={this.onSelectThread}
+ >
+ <div className="icon">
+ <AccessibleImage className={isWorker ? "worker" : "window"} />
+ </div>
+ <div className="label">{label}</div>
+ {isPaused ? (
+ <div className="pause-badge">
+ <AccessibleImage className="pause" />
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, props) => ({
+ cx: getContext(state),
+ currentThread: getCurrentThread(state),
+ isPaused: getIsPaused(state, props.thread.actor),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(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..49e150dd44
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Threads.css
@@ -0,0 +1,63 @@
+/* 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;
+}
+
+.threads-list > .thread:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.threads-list > .thread.selected {
+ background-color: var(--tab-line-selected-color);
+}
+
+.threads-list .icon {
+ flex: none;
+ margin-inline-end: 4px;
+}
+
+.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;
+ margin-inline-start: 4px;
+}
+
+.threads-list > .thread.selected {
+ background: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.threads-list > .thread.selected .img {
+ background-color: currentColor;
+}
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..dc854f3735
--- /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/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import { connect } from "../../utils/connect";
+
+import { getAllThreads } from "../../selectors";
+import Thread from "./Thread";
+
+import type { Thread as ThreadType } from "../../types";
+
+import "./Threads.css";
+
+type OwnProps = {||};
+type Props = {
+ threads: ThreadType[],
+};
+
+export class Threads extends Component<Props> {
+ render() {
+ const { threads } = this.props;
+
+ return (
+ <div className="pane threads-list">
+ {threads.map(thread => (
+ <Thread thread={thread} key={thread.actor} />
+ ))}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ threads: getAllThreads(state),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(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..cbe2ebf4c9
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css
@@ -0,0 +1,58 @@
+/* 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: hsl(54, 100%, 92%);
+ color: var(--theme-body-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;
+}
+
+.theme-dark .secondary-panes .why-paused {
+ background-color: hsl(42, 37%, 19%);
+ color: hsl(43, 94%, 81%);
+}
+
+.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..9371483621
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import React, { PureComponent } from "react";
+import { connect } from "../../utils/connect";
+import AccessibleImage from "../shared/AccessibleImage";
+import actions from "../../actions";
+
+// $FlowIgnore
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+
+import { getPauseReason } from "../../utils/pause";
+import {
+ getCurrentThread,
+ getPaneCollapse,
+ getPauseReason as getWhy,
+} from "../../selectors";
+import type { Grip, Why } from "../../types";
+
+import "./WhyPaused.css";
+
+type OwnProps = {|
+ +delay?: number,
+|};
+type Props = {
+ endPanelCollapsed: boolean,
+ +delay: ?number,
+ why: ?Why,
+ openElementInInspector: typeof actions.openElementInInspectorCommand,
+ highlightDomElement: typeof actions.highlightDomElement,
+ unHighlightDomElement: typeof actions.unHighlightDomElement,
+};
+
+type State = {
+ hideWhyPaused: string,
+};
+
+class WhyPaused extends PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = { hideWhyPaused: "" };
+ }
+
+ componentDidUpdate() {
+ const { delay } = this.props;
+
+ if (delay) {
+ setTimeout(() => {
+ this.setState({ hideWhyPaused: "" });
+ }, delay);
+ } else {
+ this.setState({ hideWhyPaused: "pane why-paused" });
+ }
+ }
+
+ renderExceptionSummary(exception: string | Grip) {
+ if (typeof exception === "string") {
+ return exception;
+ }
+
+ const { preview } = exception;
+ if (!preview || !preview.name || !preview.message) {
+ return;
+ }
+
+ return `${preview.name}: ${preview.message}`;
+ }
+
+ renderMessage(why: 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}</div>;
+ }
+
+ 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>
+ <div className="message">{why.message}</div>
+ <div className="mutationNode">
+ {ancestorRep}
+ {ancestorGrip ? (
+ <span className="why-paused-ancestor">
+ {action === "remove"
+ ? L10N.getStr("whyPaused.mutationBreakpointRemoved")
+ : L10N.getStr("whyPaused.mutationBreakpointAdded")}
+ {targetRep}
+ </span>
+ ) : (
+ targetRep
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ if (typeof message == "string") {
+ return <div className="message">{message}</div>;
+ }
+
+ return null;
+ }
+
+ render() {
+ const { endPanelCollapsed, why } = this.props;
+ const reason = getPauseReason(why);
+
+ if (!why || !reason || endPanelCollapsed) {
+ return <div className={this.state.hideWhyPaused} />;
+ }
+
+ return (
+ <div className="pane why-paused">
+ <div>
+ <div className="info icon">
+ <AccessibleImage className="info" />
+ </div>
+ <div className="pause reason">
+ {L10N.getStr(reason)}
+ {this.renderMessage(why)}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+ why: getWhy(state, getCurrentThread(state)),
+});
+
+export default connect<Props, OwnProps, _, _, _, _>(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..5f0352a93c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css
@@ -0,0 +1,131 @@
+/* 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;
+ border: 1px solid transparent;
+}
+
+.xhr-input-container.focused {
+ border: 1px solid var(--theme-highlight-blue);
+}
+
+:root.theme-dark .xhr-input-container.focused {
+ border: 1px solid var(--blue-50);
+}
+
+.xhr-input-container.error {
+ border: 1px solid red;
+}
+
+.xhr-input-form {
+ display: inline-flex;
+ width: 100%;
+ 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-grow: 1;
+ background-color: var(--theme-sidebar-background);
+ font-size: inherit;
+ height: 24px;
+ color: var(--theme-body-color);
+}
+
+.xhr-input-url::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.xhr-input-url:focus {
+ cursor: text;
+ outline: none;
+}
+
+.expressions-list .xhr-input-container {
+ height: var(--expression-item-height);
+}
+
+.expressions-list .xhr-input-url {
+ /* Prevent vertical bounce when editing an existing XHR Breakpoint */
+ height: 100%;
+}
+
+.xhr-container {
+ border-left: 4px solid transparent;
+ width: 100%;
+ color: var(--theme-body-color);
+ padding-inline-start: 16px;
+ padding-inline-end: 12px;
+ 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-method {
+ display: none;
+ /* Vertically center select in form */
+ margin-top: 2px;
+}
+
+.expressions-list .xhr-input-method {
+ margin-top: 0px;
+}
+
+.xhr-input-container.focused .xhr-input-method {
+ display: block;
+}
+
+.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;
+ overflow-x: hidden;
+}
+
+.xhr-container__close-btn {
+ display: flex;
+ padding-top: 2px;
+ padding-bottom: 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..0920fa0acf
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
@@ -0,0 +1,381 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+import actions from "../../actions";
+
+import { CloseButton } from "../shared/Button";
+
+import "./XHRBreakpoints.css";
+import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors";
+import ExceptionOption from "./Breakpoints/ExceptionOption";
+
+import type { XHRBreakpointsList } from "../../reducers/types";
+import type { XHRBreakpoint } from "../../types";
+
+type OwnProps = {|
+ onXHRAdded: () => void,
+ showInput: boolean,
+|};
+type Props = {
+ xhrBreakpoints: XHRBreakpointsList,
+ shouldPauseOnAny: boolean,
+ showInput: boolean,
+ onXHRAdded: Function,
+ setXHRBreakpoint: Function,
+ removeXHRBreakpoint: typeof actions.removeXHRBreakpoint,
+ enableXHRBreakpoint: typeof actions.enableXHRBreakpoint,
+ disableXHRBreakpoint: typeof actions.disableXHRBreakpoint,
+ togglePauseOnAny: typeof actions.togglePauseOnAny,
+ updateXHRBreakpoint: typeof actions.updateXHRBreakpoint,
+};
+
+type State = {
+ editing: boolean,
+ inputValue: string,
+ inputMethod: string,
+ editIndex: number,
+ focused: boolean,
+ clickedOnFormElement: boolean,
+};
+
+// 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<Props, State> {
+ _input: ?HTMLInputElement;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ inputValue: "",
+ inputMethod: "ANY",
+ focused: false,
+ editIndex: -1,
+ clickedOnFormElement: false,
+ };
+ }
+
+ 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: Props, prevState: State) {
+ 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: SyntheticEvent<HTMLFormElement>) => {
+ 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(
+ // $FlowIgnore
+ { inputMethod: e.target.children[1].value },
+ setXHRBreakpoint
+ );
+ };
+
+ handleExistingSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
+ 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: SyntheticInputEvent<HTMLInputElement>) => {
+ this.setState({ inputValue: e.target.value });
+ };
+
+ handleMethodChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
+ 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: SyntheticEvent<HTMLElement>) => {
+ this.setState({ editing: false, clickedOnFormElement: true });
+ };
+
+ handleTab = (e: SyntheticKeyboardEvent<HTMLElement>) => {
+ 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: number) => {
+ const { xhrBreakpoints } = this.props;
+ const { path, method } = xhrBreakpoints[index];
+ this.setState({
+ inputValue: path,
+ inputMethod: method,
+ editing: true,
+ editIndex: index,
+ });
+ };
+
+ renderXHRInput(onSubmit: (e: SyntheticEvent<HTMLFormElement>) => void) {
+ 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" }} />
+ </form>
+ );
+ }
+
+ handleCheckbox = (index: number) => {
+ const {
+ xhrBreakpoints,
+ enableXHRBreakpoint,
+ disableXHRBreakpoint,
+ } = this.props;
+ const breakpoint = xhrBreakpoints[index];
+ if (breakpoint.disabled) {
+ enableXHRBreakpoint(index);
+ } else {
+ disableXHRBreakpoint(index);
+ }
+ };
+
+ renderBreakpoint = (breakpoint: XHRBreakpoint) => {
+ const { path, disabled, method } = breakpoint;
+ const { editIndex } = this.state;
+ const { removeXHRBreakpoint, xhrBreakpoints } = this.props;
+
+ // The "pause on any" checkbox
+ if (!path) {
+ return;
+ }
+
+ // 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>
+ <input
+ type="checkbox"
+ className="xhr-checkbox"
+ checked={!disabled}
+ onChange={() => this.handleCheckbox(index)}
+ onClick={ev => ev.stopPropagation()}
+ />
+ <div className="xhr-label-method">{method}</div>
+ <div className="xhr-label-url">{path}</div>
+ <div className="xhr-container__close-btn">
+ <CloseButton handleClick={e => removeXHRBreakpoint(index)} />
+ </div>
+ </label>
+ </li>
+ );
+ };
+
+ renderBreakpoints = (explicitXhrBreakpoints: XHRBreakpointsList) => {
+ const { showInput } = this.props;
+
+ return (
+ <>
+ <ul className="pane expressions-list">
+ {explicitXhrBreakpoints.map(this.renderBreakpoint)}
+ </ul>
+ {showInput && this.renderXHRInput(this.handleNewSubmit)}
+ </>
+ );
+ };
+
+ renderCheckbox = (explicitXhrBreakpoints: XHRBreakpointsList) => {
+ const { shouldPauseOnAny, togglePauseOnAny } = this.props;
+
+ return (
+ <div
+ className={classnames("breakpoints-exceptions-options", {
+ empty: explicitXhrBreakpoints.length === 0,
+ })}
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ label={L10N.getStr("pauseOnAnyXHR")}
+ isChecked={shouldPauseOnAny}
+ onChange={() => togglePauseOnAny()}
+ />
+ </div>
+ );
+ };
+
+ renderMethodOption = (method: string) => {
+ 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}
+ </option>
+ );
+ };
+
+ renderMethodSelectElement = () => {
+ return (
+ <select
+ value={this.state.inputMethod}
+ className="xhr-input-method"
+ onChange={this.handleMethodChange}
+ onMouseDown={this.onMouseDown}
+ onKeyDown={this.handleTab}
+ >
+ {xhrMethods.map(this.renderMethodOption)}
+ </select>
+ );
+ };
+
+ render() {
+ const { xhrBreakpoints } = this.props;
+ const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints);
+
+ return (
+ <>
+ {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<Props, OwnProps, _, _, _, _>(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..2b186fbd72
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/index.js
@@ -0,0 +1,583 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import classnames from "classnames";
+import { isGeneratedId } from "devtools-source-map";
+import { connect } from "../../utils/connect";
+import { List } from "immutable";
+
+import actions from "../../actions";
+import {
+ getTopFrame,
+ getBreakpointsList,
+ getBreakpointsDisabled,
+ getExpressions,
+ getIsWaitingOnBreak,
+ getPauseCommand,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+ getThreads,
+ getCurrentThread,
+ getThreadContext,
+ getPauseReason,
+ getSourceFromId,
+ getSkipPausing,
+ shouldLogEventBreakpoints,
+} from "../../selectors";
+
+import AccessibleImage from "../shared/AccessibleImage";
+import { prefs, features } from "../../utils/prefs";
+
+import Breakpoints from "./Breakpoints";
+import Expressions from "./Expressions";
+import SplitBox from "devtools-splitter";
+import Frames from "./Frames";
+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";
+
+import "./SecondaryPanes.css";
+
+import type {
+ Expression,
+ Frame,
+ ThreadList,
+ ThreadContext,
+ Source,
+} from "../../types";
+
+type AccordionPaneItem = {
+ header: string,
+ component: any,
+ opened?: boolean,
+ onToggle?: () => void,
+ shouldOpen?: () => boolean,
+ buttons?: any,
+};
+
+function debugBtn(onClick, type, className, tooltip) {
+ return (
+ <button
+ onClick={onClick}
+ className={`${type} ${className}`}
+ key={type}
+ title={tooltip}
+ >
+ <AccessibleImage className={type} title={tooltip} aria-label={tooltip} />
+ </button>
+ );
+}
+
+type State = {
+ showExpressionsInput: boolean,
+ showXHRInput: boolean,
+};
+
+type OwnProps = {|
+ horizontal: boolean,
+|};
+type Props = {
+ cx: ThreadContext,
+ expressions: List<Expression>,
+ hasFrames: boolean,
+ horizontal: boolean,
+ breakpoints: Object,
+ selectedFrame: ?Frame,
+ breakpointsDisabled: boolean,
+ isWaitingOnBreak: boolean,
+ renderWhyPauseDelay: number,
+ mapScopesEnabled: boolean,
+ shouldPauseOnExceptions: boolean,
+ shouldPauseOnCaughtExceptions: boolean,
+ workers: ThreadList,
+ skipPausing: boolean,
+ logEventBreakpoints: boolean,
+ source: ?Source,
+ pauseReason: string,
+ toggleAllBreakpoints: typeof actions.toggleAllBreakpoints,
+ toggleMapScopes: typeof actions.toggleMapScopes,
+ evaluateExpressions: typeof actions.evaluateExpressions,
+ pauseOnExceptions: typeof actions.pauseOnExceptions,
+ breakOnNext: typeof actions.breakOnNext,
+ toggleEventLogging: typeof actions.toggleEventLogging,
+};
+
+const mdnLink =
+ "https://developer.mozilla.org/docs/Tools/Debugger/Using_the_Debugger_map_scopes_feature?utm_source=devtools&utm_medium=debugger-map-scopes";
+
+class SecondaryPanes extends Component<Props, State> {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ showExpressionsInput: false,
+ showXHRInput: false,
+ };
+ }
+
+ onExpressionAdded = () => {
+ this.setState({ showExpressionsInput: false });
+ };
+
+ onXHRAdded = () => {
+ this.setState({ showXHRInput: false });
+ };
+
+ renderBreakpointsToggle() {
+ const {
+ cx,
+ toggleAllBreakpoints,
+ breakpoints,
+ breakpointsDisabled,
+ } = this.props;
+ const isIndeterminate =
+ !breakpointsDisabled && breakpoints.some(x => x.disabled);
+
+ if (features.skipPausing || breakpoints.length === 0) {
+ return null;
+ }
+
+ const inputProps = {
+ type: "checkbox",
+ "aria-label": breakpointsDisabled
+ ? L10N.getStr("breakpoints.enable")
+ : L10N.getStr("breakpoints.disable"),
+ className: "breakpoints-toggle",
+ disabled: false,
+ key: "breakpoints-toggle",
+ onChange: e => {
+ e.stopPropagation();
+ toggleAllBreakpoints(cx, !breakpointsDisabled);
+ },
+ onClick: e => e.stopPropagation(),
+ checked: !breakpointsDisabled && !isIndeterminate,
+ ref: input => {
+ if (input) {
+ input.indeterminate = isIndeterminate;
+ }
+ },
+ title: breakpointsDisabled
+ ? L10N.getStr("breakpoints.enable")
+ : L10N.getStr("breakpoints.disable"),
+ };
+
+ return <input {...inputProps} />;
+ }
+
+ watchExpressionHeaderButtons() {
+ const { expressions } = this.props;
+
+ const buttons = [];
+
+ if (expressions.length) {
+ buttons.push(
+ debugBtn(
+ evt => {
+ evt.stopPropagation();
+ this.props.evaluateExpressions(this.props.cx);
+ },
+ "refresh",
+ "refresh",
+ L10N.getStr("watchExpressions.refreshButton")
+ )
+ );
+ }
+
+ buttons.push(
+ debugBtn(
+ evt => {
+ if (prefs.expressionsVisible) {
+ evt.stopPropagation();
+ }
+ this.setState({ showExpressionsInput: true });
+ },
+ "plus",
+ "plus",
+ L10N.getStr("expressions.placeholder")
+ )
+ );
+
+ return buttons;
+ }
+
+ xhrBreakpointsHeaderButtons() {
+ const buttons = [];
+
+ buttons.push(
+ debugBtn(
+ evt => {
+ if (prefs.xhrBreakpointsVisible) {
+ evt.stopPropagation();
+ }
+ this.setState({ showXHRInput: true });
+ },
+ "plus",
+ "plus",
+ L10N.getStr("xhrBreakpoints.label")
+ )
+ );
+
+ return buttons;
+ }
+
+ getScopeItem(): AccordionPaneItem {
+ return {
+ header: L10N.getStr("scopes.header"),
+ className: "scopes-pane",
+ component: <Scopes />,
+ opened: prefs.scopesVisible,
+ buttons: this.getScopesButtons(),
+ onToggle: opened => {
+ prefs.scopesVisible = opened;
+ },
+ };
+ }
+
+ getScopesButtons() {
+ const { selectedFrame, mapScopesEnabled, source } = this.props;
+
+ if (
+ !selectedFrame ||
+ isGeneratedId(selectedFrame.location.sourceId) ||
+ source?.isPrettyPrinted
+ ) {
+ return null;
+ }
+
+ return [
+ <div key="scopes-buttons">
+ <label
+ className="map-scopes-header"
+ title={L10N.getStr("scopes.mapping.label")}
+ onClick={e => e.stopPropagation()}
+ >
+ <input
+ type="checkbox"
+ checked={mapScopesEnabled ? "checked" : ""}
+ onChange={e => this.props.toggleMapScopes()}
+ />
+ {L10N.getStr("scopes.map.label")}
+ </label>
+ <a
+ className="mdn"
+ target="_blank"
+ href={mdnLink}
+ onClick={e => e.stopPropagation()}
+ title={L10N.getStr("scopes.helpTooltip.label")}
+ >
+ <AccessibleImage className="shortcuts" />
+ </a>
+ </div>,
+ ];
+ }
+
+ getEventButtons() {
+ const { logEventBreakpoints } = this.props;
+ return [
+ <div key="events-buttons">
+ <label
+ className="events-header"
+ title={L10N.getStr("eventlisteners.log.label")}
+ onClick={e => e.stopPropagation()}
+ >
+ <input
+ type="checkbox"
+ checked={logEventBreakpoints ? "checked" : ""}
+ onChange={e => this.props.toggleEventLogging()}
+ onKeyDown={e => e.stopPropagation()}
+ />
+ {L10N.getStr("eventlisteners.log")}
+ </label>
+ </div>,
+ ];
+ }
+
+ getWatchItem(): AccordionPaneItem {
+ return {
+ header: L10N.getStr("watchExpressions.header"),
+ className: "watch-expressions-pane",
+ buttons: this.watchExpressionHeaderButtons(),
+ component: (
+ <Expressions
+ showInput={this.state.showExpressionsInput}
+ onExpressionAdded={this.onExpressionAdded}
+ />
+ ),
+ opened: prefs.expressionsVisible,
+ onToggle: opened => {
+ prefs.expressionsVisible = opened;
+ },
+ };
+ }
+
+ getXHRItem(): AccordionPaneItem {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("xhrBreakpoints.header"),
+ className: "xhr-breakpoints-pane",
+ buttons: this.xhrBreakpointsHeaderButtons(),
+ component: (
+ <XHRBreakpoints
+ showInput={this.state.showXHRInput}
+ onXHRAdded={this.onXHRAdded}
+ />
+ ),
+ opened: prefs.xhrBreakpointsVisible || pauseReason === "XHR",
+ onToggle: opened => {
+ prefs.xhrBreakpointsVisible = opened;
+ },
+ };
+ }
+
+ getCallStackItem(): AccordionPaneItem {
+ return {
+ header: L10N.getStr("callStack.header"),
+ className: "call-stack-pane",
+ component: <Frames panel="debugger" />,
+ opened: prefs.callStackVisible,
+ onToggle: opened => {
+ prefs.callStackVisible = opened;
+ },
+ };
+ }
+
+ getThreadsItem(): AccordionPaneItem {
+ return {
+ header: L10N.getStr("threadsHeader"),
+ className: "threads-pane",
+ component: <Threads />,
+ opened: prefs.workersVisible,
+ onToggle: opened => {
+ prefs.workersVisible = opened;
+ },
+ };
+ }
+
+ getBreakpointsItem(): AccordionPaneItem {
+ const {
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ pauseOnExceptions,
+ pauseReason,
+ } = this.props;
+
+ return {
+ header: L10N.getStr("breakpoints.header"),
+ className: "breakpoints-pane",
+ buttons: [this.renderBreakpointsToggle()],
+ component: (
+ <Breakpoints
+ shouldPauseOnExceptions={shouldPauseOnExceptions}
+ shouldPauseOnCaughtExceptions={shouldPauseOnCaughtExceptions}
+ pauseOnExceptions={pauseOnExceptions}
+ />
+ ),
+ opened:
+ prefs.breakpointsVisible ||
+ pauseReason === "breakpoint" ||
+ pauseReason === "resumeLimit",
+ onToggle: opened => {
+ prefs.breakpointsVisible = opened;
+ },
+ };
+ }
+
+ getEventListenersItem(): AccordionPaneItem {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("eventListenersHeader1"),
+ className: "event-listeners-pane",
+ buttons: this.getEventButtons(),
+ component: <EventListeners />,
+ opened: prefs.eventListenersVisible || pauseReason === "eventBreakpoint",
+ onToggle: opened => {
+ prefs.eventListenersVisible = opened;
+ },
+ };
+ }
+
+ getDOMMutationsItem(): AccordionPaneItem {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("domMutationHeader"),
+ className: "dom-mutations-pane",
+ buttons: [],
+ component: <DOMMutationBreakpoints />,
+ opened:
+ prefs.domMutationBreakpointsVisible ||
+ pauseReason === "mutationBreakpoint",
+ onToggle: opened => {
+ prefs.domMutationBreakpointsVisible = opened;
+ },
+ };
+ }
+
+ getStartItems(): AccordionPaneItem[] {
+ const items: AccordionPaneItem[] = [];
+ const { horizontal, hasFrames } = this.props;
+
+ if (horizontal) {
+ if (features.workers && this.props.workers.length > 0) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+ }
+
+ items.push(this.getBreakpointsItem());
+
+ if (hasFrames) {
+ items.push(this.getCallStackItem());
+ if (horizontal) {
+ items.push(this.getScopeItem());
+ }
+ }
+
+ if (features.xhrBreakpoints) {
+ items.push(this.getXHRItem());
+ }
+
+ if (features.eventListenersBreakpoints) {
+ items.push(this.getEventListenersItem());
+ }
+
+ if (features.domMutationBreakpoints) {
+ items.push(this.getDOMMutationsItem());
+ }
+
+ return items;
+ }
+
+ getEndItems(): AccordionPaneItem[] {
+ if (this.props.horizontal) {
+ return [];
+ }
+
+ const items: AccordionPaneItem[] = [];
+ if (features.workers && this.props.workers.length > 0) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+
+ if (this.props.hasFrames) {
+ items.push(this.getScopeItem());
+ }
+
+ return items;
+ }
+
+ getItems(): AccordionPaneItem[] {
+ return [...this.getStartItems(), ...this.getEndItems()];
+ }
+
+ renderHorizontalLayout() {
+ const { renderWhyPauseDelay } = this.props;
+
+ return (
+ <div>
+ <WhyPaused delay={renderWhyPauseDelay} />
+ <Accordion items={this.getItems()} />
+ </div>
+ );
+ }
+
+ renderVerticalLayout() {
+ return (
+ <SplitBox
+ initialSize="300px"
+ minSize={10}
+ maxSize="50%"
+ splitterSize={1}
+ startPanel={
+ <div style={{ width: "inherit" }}>
+ <WhyPaused delay={this.props.renderWhyPauseDelay} />
+ <Accordion items={this.getStartItems()} />
+ </div>
+ }
+ endPanel={<Accordion items={this.getEndItems()} />}
+ />
+ );
+ }
+
+ render() {
+ const { skipPausing } = this.props;
+ return (
+ <div className="secondary-panes-wrapper">
+ <CommandBar horizontal={this.props.horizontal} />
+ <div
+ className={classnames(
+ "secondary-panes",
+ skipPausing && "skip-pausing"
+ )}
+ >
+ {this.props.horizontal
+ ? this.renderHorizontalLayout()
+ : this.renderVerticalLayout()}
+ </div>
+ </div>
+ );
+ }
+}
+
+// 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);
+
+ return {
+ cx: getThreadContext(state),
+ expressions: getExpressions(state),
+ hasFrames: !!getTopFrame(state, thread),
+ breakpoints: getBreakpointsList(state),
+ breakpointsDisabled: getBreakpointsDisabled(state),
+ isWaitingOnBreak: getIsWaitingOnBreak(state, thread),
+ renderWhyPauseDelay: getRenderWhyPauseDelay(state, thread),
+ selectedFrame,
+ mapScopesEnabled: isMapScopesEnabled(state),
+ shouldPauseOnExceptions: getShouldPauseOnExceptions(state),
+ shouldPauseOnCaughtExceptions: getShouldPauseOnCaughtExceptions(state),
+ workers: getThreads(state),
+ skipPausing: getSkipPausing(state),
+ logEventBreakpoints: shouldLogEventBreakpoints(state),
+ source:
+ selectedFrame && getSourceFromId(state, selectedFrame.location.sourceId),
+ pauseReason: pauseReason?.type ?? "",
+ };
+};
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ toggleAllBreakpoints: actions.toggleAllBreakpoints,
+ evaluateExpressions: actions.evaluateExpressions,
+ pauseOnExceptions: actions.pauseOnExceptions,
+ toggleMapScopes: actions.toggleMapScopes,
+ breakOnNext: actions.breakOnNext,
+ toggleEventLogging: actions.toggleEventLogging,
+})(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..2f80076923
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.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/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+import CommandBar from "../CommandBar";
+import { mockthreadcx } from "../../../utils/test-mockup";
+
+describe("CommandBar", () => {
+ it("f8 key command calls props.breakOnNext when not in paused state", () => {
+ const props = {
+ cx: mockthreadcx,
+ 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() } };
+
+ // $FlowIgnore
+ shallow(<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 = {
+ cx: { ...mockthreadcx, isPaused: true },
+ 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() } };
+
+ // $FlowIgnore
+ shallow(<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..2ccf8940ad
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React from "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);
+ // $FlowIgnore
+ const component = shallow(<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..c7ce944ab6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.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/>. */
+
+// @flow
+
+import React from "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);
+ // $FlowIgnore
+ const component = shallow(<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..54aa21d36d
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
@@ -0,0 +1,342 @@
+// @flow
+/* 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 "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 ""',
+ },
+ ],
+ ...propsOverride,
+ };
+}
+
+function renderXHRBreakpointsComponent(propsOverride) {
+ const props = generateDefaultState(propsOverride);
+ const xhrBreakpointsComponent = mount(
+ // $FlowIgnore
+ <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-exceptions-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-exceptions-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 > 0).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..d2c848b132
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap
@@ -0,0 +1,191 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Expressions should always have unique keys 1`] = `
+<Fragment>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ disableWrap={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}
+ disableWrap={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>
+</Fragment>
+`;
+
+exports[`Expressions should render 1`] = `
+<Fragment>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ disableWrap={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}
+ disableWrap={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>
+</Fragment>
+`;
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..b20c67ecf5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
@@ -0,0 +1,601 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`XHR Breakpoints should render with 0 expressions passed from props 1`] = `
+<XHRBreakpoints
+ xhrBreakpoints={
+ Array [
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "",
+ "text": "URL contains \\"\\"",
+ },
+ ]
+ }
+>
+ <div
+ className="breakpoints-exceptions-options empty"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <div
+ className="breakpoints-exceptions"
+ onClick={[Function]}
+ >
+ <input
+ checked=""
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </div>
+ </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
+ 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-exceptions-options"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <div
+ className="breakpoints-exceptions"
+ onClick={[Function]}
+ >
+ <input
+ checked=""
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </div>
+ </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>
+`;