Bug 1569216 - Implement DOM Mutation Breakpoints UI for the debugger r=loganfsmyth

Differential Revision: https://phabricator.services.mozilla.com/D39517
This commit is contained in:
David Walsh
2019-08-13 02:35:50 +00:00
parent 7b1930a258
commit 2e1b24d913
17 changed files with 379 additions and 19 deletions

View File

@@ -0,0 +1,70 @@
/* 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 0;
text-align: center;
font-style: italic;
color: var(--theme-body-color);
}
.dom-mutation-list * {
-moz-user-select: none;
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;
}

View File

@@ -0,0 +1,141 @@
/* 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-reps";
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,
deleteBreakpoint: typeof deleteDOMMutationBreakpoint,
toggleBreakpoint: typeof toggleDOMMutationBreakpointState,
};
const localizationTerms = {
subtree: L10N.getStr("domMutationTypes.subtree"),
attribute: L10N.getStr("domMutationTypes.attribute"),
removal: L10N.getStr("domMutationTypes.removal"),
};
class DOMMutationBreakpointsContents extends Component<Props> {
renderItem(breakpoint: DOMMutationBreakpoint) {
const {
openElementInInspector,
highlightDomElement,
unHighlightDomElement,
toggleBreakpoint,
deleteBreakpoint,
} = this.props;
return (
<li>
<input
type="checkbox"
checked={breakpoint.enabled}
onChange={() => toggleBreakpoint(breakpoint.id, !breakpoint.enabled)}
/>
<div className="dom-mutation-info">
<div className="dom-mutation-label">
{Rep({
object: translateNodeFrontToGrip(breakpoint.nodeFront),
mode: MODE.LONG,
onDOMNodeClick: grip => openElementInInspector(grip),
onInspectIconClick: grip => openElementInInspector(grip),
onDOMNodeMouseOver: grip => highlightDomElement(grip),
onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
})}
</div>
<div className="dom-mutation-type">
{localizationTerms[breakpoint.mutationType] ||
breakpoint.mutationType}
</div>
</div>
<CloseButton
handleClick={() =>
deleteBreakpoint(breakpoint.nodeFront, breakpoint.mutationType)
}
/>
</li>
);
}
renderEmpty() {
return (
<div className="dom-mutation-empty">
{L10N.getStr("noDomMutationBreakpointsText")}
</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}
/>
);
}
}
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,
}
)(DomMutationBreakpoints);

View File

@@ -40,6 +40,7 @@ import CommandBar from "./CommandBar";
import UtilsBar from "./UtilsBar";
import XHRBreakpoints from "./XHRBreakpoints";
import EventListeners from "./EventListeners";
import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
import WhyPaused from "./WhyPaused";
import Scopes from "./Scopes";
@@ -373,6 +374,19 @@ class SecondaryPanes extends Component<Props, State> {
};
}
getDOMMutationsItem(): AccordionPaneItem {
return {
header: L10N.getStr("domMutationHeader"),
className: "dom-mutations-pane",
buttons: [],
component: <DOMMutationBreakpoints />,
opened: prefs.domMutationBreakpointsVisible,
onToggle: opened => {
prefs.domMutationBreakpointsVisible = opened;
},
};
}
getStartItems(): AccordionPaneItem[] {
const items: AccordionPaneItem[] = [];
const { horizontal, hasFrames } = this.props;
@@ -402,6 +416,10 @@ class SecondaryPanes extends Component<Props, State> {
items.push(this.getEventListenersItem());
}
if (features.domMutationBreakpoints) {
items.push(this.getDOMMutationsItem());
}
return items;
}

View File

@@ -10,6 +10,7 @@ DIRS += [
CompiledModules(
'CommandBar.js',
'DOMMutationBreakpoints.js',
'EventListeners.js',
'Expressions.js',
'index.js',
@@ -23,6 +24,7 @@ CompiledModules(
DevToolsModules(
'CommandBar.css',
'DOMMutationBreakpoints.css',
'EventListeners.css',
'Expressions.css',
'Scopes.css',

View File

@@ -39,6 +39,7 @@
@import url("./components/SecondaryPanes/Breakpoints/Breakpoints.css");
@import url("./components/SecondaryPanes/CommandBar.css");
@import url("./components/SecondaryPanes/EventListeners.css");
@import url("./components/SecondaryPanes/DOMMutationBreakpoints.css");
@import url("./components/SecondaryPanes/Expressions.css");
@import url("./components/SecondaryPanes/Frames/Frames.css");
@import url("./components/SecondaryPanes/Frames/Group.css");

View File

@@ -489,6 +489,13 @@ export type SourceDocuments = { [string]: Object };
export type BreakpointPosition = MappedLocation;
export type BreakpointPositions = { [number]: BreakpointPosition[] };
export type DOMMutationBreakpoint = {
id: number,
nodeFront: Object,
mutationType: "subtree" | "attribute" | "removal",
enabled: boolean,
};
export type { Context, ThreadContext } from "./utils/context";
export type Previews = {

View File

@@ -30,6 +30,7 @@ if (isDevelopment()) {
pref("devtools.debugger.xhr-breakpoints-visible", true);
pref("devtools.debugger.breakpoints-visible", true);
pref("devtools.debugger.event-listeners-visible", true);
pref("devtools.debugger.dom-mutation-breakpoints-visible", true);
pref("devtools.debugger.start-panel-collapsed", false);
pref("devtools.debugger.end-panel-collapsed", false);
pref("devtools.debugger.start-panel-size", 300);
@@ -65,6 +66,7 @@ if (isDevelopment()) {
pref("devtools.debugger.features.original-blackbox", true);
pref("devtools.debugger.features.windowless-workers", true);
pref("devtools.debugger.features.event-listeners-breakpoints", true);
pref("devtools.debugger.features.dom-mutation-breakpoints", true);
pref("devtools.debugger.features.log-points", true);
pref("devtools.debugger.features.inline-preview", true);
pref("devtools.debugger.log-actions", true);
@@ -89,6 +91,10 @@ export const prefs = new PrefsHelper("devtools", {
expressionsVisible: ["Bool", "debugger.expressions-visible"],
xhrBreakpointsVisible: ["Bool", "debugger.xhr-breakpoints-visible"],
eventListenersVisible: ["Bool", "debugger.event-listeners-visible"],
domMutationBreakpointsVisible: [
"Bool",
"debugger.dom-mutation-breakpoints-visible",
],
startPanelCollapsed: ["Bool", "debugger.start-panel-collapsed"],
endPanelCollapsed: ["Bool", "debugger.end-panel-collapsed"],
startPanelSize: ["Int", "debugger.start-panel-size"],
@@ -127,6 +133,7 @@ export const features = new PrefsHelper("devtools.debugger.features", {
xhrBreakpoints: ["Bool", "xhr-breakpoints"],
originalBlackbox: ["Bool", "original-blackbox"],
eventListenersBreakpoints: ["Bool", "event-listeners-breakpoints"],
domMutationBreakpoints: ["Bool", "dom-mutation-breakpoints"],
logPoints: ["Bool", "log-points"],
showOverlayStepButtons: ["Bool", "debugger.features.overlay-step-buttons"],
inlinePreview: ["Bool", "inline-preview"],

View File

@@ -7,8 +7,12 @@ support-files =
head.js
helpers.js
helpers/context.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/client/shared/test/test-actor-registry.js
!/devtools/client/shared/test/test-actor.js
[browser_dbg-asm.js]
[browser_dbg-audiocontext.js]
@@ -39,6 +43,7 @@ skip-if = !e10s || verify # This test is only valid in e10s
[browser_dbg-breakpoint-skipping.js]
[browser_dbg-breakpoint-skipping-console.js]
[browser_dbg-call-stack.js]
[browser_dbg-dom-mutation-breakpoints.js]
[browser_dbg-scopes.js]
[browser_dbg-chrome-create.js]
skip-if = (verify && !debug && (os == 'linux'))

View File

@@ -0,0 +1,62 @@
/* 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/>. */
// Tests adding, disble/enable, and removal of dom mutation breakpoints
/* import-globals-from ../../../inspector/test/shared-head.js */
// Import helpers for the inspector
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
this
);
const DMB_TEST_URL = "http://example.com/browser/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html";
add_task(async function() {
// Enable features
await pushPref("devtools.debugger.features.dom-mutation-breakpoints", true);
await pushPref("devtools.markup.mutationBreakpoints.enabled", true);
await pushPref("devtools.debugger.dom-mutation-breakpoints-visible", true);
info("Switches over to the inspector pane");
const { inspector, toolbox } = await openInspectorForURL(DMB_TEST_URL);
info("Sellecting the body node");
await selectNode("body", inspector);
info("Adding a DOM mutation breakpoint to body");
const allMenuItems = openContextMenuAndGetAllItems(inspector);
const breakOnMenuItem = allMenuItems.find(item => item.id === "node-menu-mutation-breakpoint-attribute");
breakOnMenuItem.click();
info("Switches over to the debugger pane");
await toolbox.selectTool("jsdebugger");
const dbg = createDebuggerContext(toolbox);
info("Confirms that one DOM mutation breakpoint exists");
const mutationItem = await waitForElementWithSelector(dbg, ".dom-mutation-list li");
ok(mutationItem, "A DOM mutation breakpoint exists");
mutationItem.scrollIntoView();
info("Enabling and disabling the DOM mutation breakpoint works ");
const checkbox = mutationItem.querySelector("input");
checkbox.click();
await waitFor(() => !checkbox.checked);
checkbox.click();
await waitFor(() => checkbox.checked);
info("Changing attribute to trigger debugger pause");
ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
content.document.querySelector("button").click();
});
await waitForPaused(dbg);
await resume(dbg);
});

View File

@@ -0,0 +1,19 @@
<!-- 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/. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Debugger test page</title>
</head>
<body>
<button title="Hello" onclick="changeAttribute()">Click me!</button>
<script>
function changeAttribute() {
document.body.setAttribute("title", "Goodbye");
}
</script>
</body>
</html>

View File

@@ -100,7 +100,7 @@ function updateBreakpointsForMutations(mutationItems) {
nodeFronts: removedNodeFronts,
});
}
if (changedNodeFronts.length > 0) {
if (changedNodeFronts.size > 0) {
const enabledStates = [];
for (const {
id,

View File

@@ -549,6 +549,7 @@ class MarkupContextMenu {
menu.append(
new MenuItem({
id: "node-menu-mutation-breakpoint-attribute",
checked: mutationBreakpoints.attribute,
click: () => this.markup.toggleMutationBreakpoint("attribute"),
disabled: !isSelectionElement,
@@ -814,6 +815,7 @@ class MarkupContextMenu {
new MenuItem({
label: INSPECTOR_L10N.getStr("inspectorBreakpointSubmenu.label"),
submenu: this._getDOMBreakpointSubmenu(isSelectionElement),
id: "node-menu-mutation-breakpoint",
})
);
}

View File

@@ -182,24 +182,6 @@ function clearCurrentNodeSelection(inspector) {
return updated;
}
/**
* Open the inspector in a tab with given URL.
* @param {string} url The URL to open.
* @param {String} hostType Optional hostType, as defined in Toolbox.HostType
* @return A promise that is resolved once the tab and inspector have loaded
* with an object: { tab, toolbox, inspector }.
*/
var openInspectorForURL = async function(url, hostType) {
const tab = await addTab(url);
const { inspector, toolbox, testActor } = await openInspector(hostType);
return { tab, inspector, toolbox, testActor };
};
async function getActiveInspector() {
const target = await TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.getToolbox(target).getPanel("inspector");
}
/**
* Right click on a node in the test page and click on the inspect menu item.
* @param {TestActor}

View File

@@ -131,6 +131,26 @@ noSourcesText=This page has no sources.
# header.
eventListenersHeader1=Event Listener Breakpoints
# LOCALIZATION NOTE (noDomMutationBreakpointsText): The text to display in the
# DOM Mutation Breakpoints pane when there are no events.
noDomMutationBreakpointsText=No breakpoints to display.
# LOCALIZATION NOTE (domMutationHeader): The text to display in the
# DOM Mutation Breakpoints header
domMutationHeader=DOM Mutation Breakpoints
# LOCALIZATION NOTE (domMutationTypes.attribute): The text to display in the
# DOM Mutation Breakpoints panel for an attribute change
domMutationTypes.attribute=Attribute Modification
# LOCALIZATION NOTE (domMutationTypes.removal): The text to display in the
# DOM Mutation Breakpoints panel for a DOM node removal
domMutationTypes.removal=Node Removal
# LOCALIZATION NOTE (domMutationTypes.subtree): The text to display in the
# DOM Mutation Breakpoints panel for a DOM subtree change
domMutationTypes.subtree=Subtree Modification
# LOCALIZATION NOTE (sources.search.key2): Key shortcut to open the search for
# searching all the source files the debugger has seen.
# Do not localize "CmdOrCtrl+P", or change the format of the string. These are

View File

@@ -35,6 +35,7 @@ pref("devtools.debugger.component-visible", true);
pref("devtools.debugger.workers-visible", true);
pref("devtools.debugger.breakpoints-visible", true);
pref("devtools.debugger.expressions-visible", true);
pref("devtools.debugger.dom-mutation-breakpoints-visible", true);
pref("devtools.debugger.xhr-breakpoints-visible", true);
pref("devtools.debugger.event-listeners-visible", true);
pref("devtools.debugger.start-panel-collapsed", false);
@@ -75,6 +76,7 @@ pref("devtools.debugger.features.xhr-breakpoints", true);
pref("devtools.debugger.features.original-blackbox", true);
pref("devtools.debugger.features.windowless-workers", true);
pref("devtools.debugger.features.event-listeners-breakpoints", true);
pref("devtools.debugger.features.dom-mutation-breakpoints", false);
pref("devtools.debugger.features.log-points", true);
pref("devtools.debugger.features.overlay-step-buttons", false);
pref("devtools.debugger.features.inline-preview", false);

View File

@@ -35,6 +35,8 @@ const mappings = Object.assign(
"wasmparser/dist/WasmParser": "devtools/client/shared/vendor/WasmParser",
"wasmparser/dist/WasmDis": "devtools/client/shared/vendor/WasmDis",
"whatwg-url": "devtools/client/shared/vendor/whatwg-url",
"framework-actions": "devtools/client/framework/actions/index",
"inspector-shared-utils": "devtools/client/inspector/shared/utils",
},
EXCLUDED_FILES
);

View File

@@ -4,6 +4,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../inspector/test/shared-head.js */
"use strict";
// This shared-head.js file is used for multiple mochitest test directories in
@@ -232,6 +234,24 @@ var refreshTab = async function(tab = gBrowser.selectedTab) {
info("Tab finished refreshing.");
};
/**
* Open the inspector in a tab with given URL.
* @param {string} url The URL to open.
* @param {String} hostType Optional hostType, as defined in Toolbox.HostType
* @return A promise that is resolved once the tab and inspector have loaded
* with an object: { tab, toolbox, inspector }.
*/
var openInspectorForURL = async function(url, hostType) {
const tab = await addTab(url);
const { inspector, toolbox, testActor } = await openInspector(hostType);
return { tab, inspector, toolbox, testActor };
};
async function getActiveInspector() {
const target = await TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.getToolbox(target).getPanel("inspector");
}
/**
* Simulate a key event from a <key> element.
* @param {DOMNode} key