summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes/Breakpoints')
-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
13 files changed, 1753 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>
+`;