diff options
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes')
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"> + + </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> +`; |