Bug 1550031 - Part 4: Manage DOM Mutation breakpoints in the toolbox. r=jlast,ochameau

Differential Revision: https://phabricator.services.mozilla.com/D39898

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Logan Smyth 2019-08-09 01:28:21 +00:00
Родитель 6baad29f20
Коммит 19df4a9319
20 изменённых файлов: 407 добавлений и 15 удалений

Просмотреть файл

@ -18,3 +18,4 @@
[options]
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe

Просмотреть файл

@ -0,0 +1,9 @@
/* 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
declare module "devtools/client/framework/store-provider" {
declare module.exports: any;
}

Просмотреть файл

@ -71,6 +71,10 @@ DebuggerPanel.prototype = {
return this._store.getState();
},
getToolboxStore: function() {
return this.toolbox.store;
},
openLink: function(url) {
openContentLink(url);
},

Просмотреть файл

@ -382,4 +382,5 @@ export type Panel = {|
openConsoleAndEvaluate: (input: string) => void,
highlightDomElement: (grip: Object) => void,
unHighlightDomElement: (grip: Object) => void,
getToolboxStore: () => any,
|};

Просмотреть файл

@ -101,7 +101,7 @@ export async function onConnect(
client: client.clientCommands,
});
bootstrapApp(store);
bootstrapApp(store, panel);
await connected;
return { store, actions, selectors, client: commands };
}

Просмотреть файл

@ -30,5 +30,8 @@ bootstrap(React, ReactDOM).then(connection => {
console.log("highlighting dom element"),
unHighlightDomElement: (grip: Object) =>
console.log("unhighlighting dom element"),
getToolboxStore: () => {
throw new Error("Cannot connect to Toolbox store when running Launchpad");
},
});
});

Просмотреть файл

@ -9,6 +9,7 @@ import { bindActionCreators, combineReducers } from "redux";
import ReactDOM from "react-dom";
const { Provider } = require("react-redux");
import ToolboxProvider from "devtools/client/framework/store-provider";
import { isFirefoxPanel, isDevelopment, isTesting } from "devtools-environment";
import SourceMaps, {
startSourceMapWorker,
@ -28,7 +29,7 @@ import type { Panel } from "../client/firefox/types";
let parser;
function renderPanel(component, store) {
function renderPanel(component, store, panel: Panel) {
const root = document.createElement("div");
root.className = "launchpad-root theme-body";
root.style.setProperty("flex", "1");
@ -39,7 +40,15 @@ function renderPanel(component, store) {
mount.appendChild(root);
ReactDOM.render(
React.createElement(Provider, { store }, React.createElement(component)),
React.createElement(
Provider,
{ store },
React.createElement(
ToolboxProvider,
{ store: panel.getToolboxStore() },
React.createElement(component)
)
),
root
);
}
@ -106,9 +115,9 @@ export function teardownWorkers() {
search.stop();
}
export function bootstrapApp(store: any) {
export function bootstrapApp(store: any, panel: Panel) {
if (isFirefoxPanel()) {
renderPanel(App, store);
renderPanel(App, store, panel);
} else {
const { renderRoot } = require("devtools-launchpad");
renderRoot(React, ReactDOM, App, store);

Просмотреть файл

@ -8,10 +8,15 @@ import * as React from "react";
export function connect<Config, RSP: {}, MDP: {}>(
mapStateToProps: (state: any, props: any) => RSP,
mapDispatchToProps?: (Function => MDP) | MDP
mapDispatchToProps?: (Function => MDP) | MDP,
mergeProps?: void,
opts?: ?{|
storeKey?: string,
|}
): (
Component: React.AbstractComponent<Config>
) => React.AbstractComponent<$Diff<Config, RSP & MDP>> {
// TODO: Bug 1572214 - We should use the standard type definitions directly.
// $FlowFixMe
return reduxConnect(mapStateToProps, mapDispatchToProps);
return reduxConnect(mapStateToProps, mapDispatchToProps, null, opts);
}

Просмотреть файл

@ -0,0 +1,144 @@
/* 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/>. */
"use strict";
const { assert } = require("devtools/shared/DevToolsUtils");
const {
getDOMMutationBreakpoint,
getDOMMutationBreakpoints,
} = require("devtools/client/framework/reducers/dom-mutation-breakpoints");
exports.registerWalkerListeners = registerWalkerListeners;
function registerWalkerListeners(toolbox) {
const { walker } = toolbox;
walker.on("mutations", mutations =>
handleWalkerMutations(mutations, toolbox)
);
}
function handleWalkerMutations(mutations, toolbox) {
// If we got BP updates for detach/unload, we want to drop those nodes from
// the list of active DOM mutation breakpoints. We explicitly check these
// cases because BP updates could also happen due to explicitly API
// operations to add/remove bps.
const mutationItems = mutations.filter(
mutation => mutation.type === "mutationBreakpoint"
);
if (mutationItems.length > 0) {
toolbox.store.dispatch(updateBreakpointsForMutations(mutationItems));
}
}
exports.createDOMMutationBreakpoint = createDOMMutationBreakpoint;
function createDOMMutationBreakpoint(nodeFront, mutationType) {
assert(typeof nodeFront === "object" && nodeFront);
assert(typeof mutationType === "string");
return async function(dispatch) {
const walker = nodeFront.parent();
dispatch({
type: "ADD_DOM_MUTATION_BREAKPOINT",
nodeFront,
mutationType,
});
await walker.setMutationBreakpoints(nodeFront, {
[mutationType]: true,
});
};
}
exports.deleteDOMMutationBreakpoint = deleteDOMMutationBreakpoint;
function deleteDOMMutationBreakpoint(nodeFront, mutationType) {
assert(typeof nodeFront === "object" && nodeFront);
assert(typeof mutationType === "string");
return async function(dispatch) {
dispatch({
type: "REMOVE_DOM_MUTATION_BREAKPOINT",
nodeFront,
mutationType,
});
const walker = nodeFront.parent();
await walker.setMutationBreakpoints(nodeFront, {
[mutationType]: false,
});
};
}
function updateBreakpointsForMutations(mutationItems) {
return async function(dispatch, getState) {
const removedNodeFronts = [];
const changedNodeFronts = new Set();
for (const { target: nodeFront, mutationReason } of mutationItems) {
switch (mutationReason) {
case "api":
changedNodeFronts.add(nodeFront);
break;
default:
console.error(
"Unexpected mutation reason",
mutationReason,
", removing"
);
// Fall Through
case "detach":
case "unload":
removedNodeFronts.push(nodeFront);
break;
}
}
if (removedNodeFronts.length > 0) {
dispatch({
type: "REMOVE_DOM_MUTATION_BREAKPOINTS_FOR_FRONTS",
nodeFronts: removedNodeFronts,
});
}
if (changedNodeFronts.length > 0) {
const enabledStates = [];
for (const {
id,
nodeFront,
mutationType,
enabled,
} of getDOMMutationBreakpoints(getState())) {
if (changedNodeFronts.has(nodeFront)) {
const bpEnabledOnFront = nodeFront.mutationBreakpoints[mutationType];
if (bpEnabledOnFront !== enabled) {
// Sync the bp state from the front into the store.
enabledStates.push([id, bpEnabledOnFront]);
}
}
}
dispatch({
type: "SET_DOM_MUTATION_BREAKPOINTS_ENABLED_STATE",
enabledStates,
});
}
};
}
exports.toggleDOMMutationBreakpointState = toggleDOMMutationBreakpointState;
function toggleDOMMutationBreakpointState(id, enabled) {
assert(typeof id === "string");
assert(typeof enabled === "boolean");
return async function(dispatch, getState) {
const bp = getDOMMutationBreakpoint(getState(), id);
if (!bp) {
throw new Error(`No DOM mutation BP with ID ${id}`);
}
const walker = bp.nodeFront.parent();
await walker.setMutationBreakpoints(bp.nodeFront, {
[bp.mutationType]: enabled,
});
};
}

Просмотреть файл

@ -0,0 +1,8 @@
/* 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/. */
"use strict";
module.exports = {
...require("./dom-mutation-breakpoints"),
};

Просмотреть файл

@ -0,0 +1,11 @@
# 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/.
DevToolsModules(
'dom-mutation-breakpoints.js',
'index.js',
)
with Files('**'):
BUG_COMPONENT = ('DevTools', 'Framework')

Просмотреть файл

@ -17,6 +17,8 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DIRS += [
'components',
'actions',
'reducers',
]
DevToolsModules(
@ -28,6 +30,8 @@ DevToolsModules(
'selection.js',
'sidebar.js',
'source-map-url-service.js',
'store-provider.js',
'store.js',
'target-from-url.js',
'target.js',
'toolbox-context-menu.js',

Просмотреть файл

@ -0,0 +1,115 @@
/* 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/>. */
"use strict";
const initialReducerState = {
counter: 1,
breakpoints: [],
};
exports.reducer = domMutationBreakpointReducer;
function domMutationBreakpointReducer(state = initialReducerState, action) {
switch (action.type) {
case "ADD_DOM_MUTATION_BREAKPOINT":
const hasExistingBp = state.breakpoints.some(
bp =>
bp.nodeFront === action.nodeFront &&
bp.mutationType === action.mutationType
);
if (hasExistingBp) {
break;
}
state = {
...state,
counter: state.counter + 1,
breakpoints: [
...state.breakpoints,
{
id: `${state.counter}`,
nodeFront: action.nodeFront,
mutationType: action.mutationType,
enabled: true,
},
],
};
break;
case "REMOVE_DOM_MUTATION_BREAKPOINT":
for (const [index, bp] of state.breakpoints.entries()) {
if (
bp.nodeFront === action.nodeFront &&
bp.mutationType === action.mutationType
) {
state = {
...state,
breakpoints: [
...state.breakpoints.slice(0, index),
...state.breakpoints.slice(index + 1),
],
};
break;
}
}
break;
case "REMOVE_DOM_MUTATION_BREAKPOINTS_FOR_FRONTS": {
const { nodeFronts } = action;
const nodeFrontSet = new Set(nodeFronts);
const breakpoints = state.breakpoints.filter(
bp => !nodeFrontSet.has(bp.nodeFront)
);
// Since we might not have made any actual changes, we verify first
// to avoid unnecessary changes in the state.
if (state.breakpoints.length !== breakpoints.length) {
state = {
...state,
breakpoints,
};
}
break;
}
case "SET_DOM_MUTATION_BREAKPOINTS_ENABLED_STATE": {
const { enabledStates } = action;
const toUpdateById = new Map(enabledStates);
const breakpoints = state.breakpoints.map(bp => {
const newBpState = toUpdateById.get(bp.id);
if (typeof newBpState === "boolean" && newBpState !== bp.enabled) {
bp = {
...bp,
enabled: newBpState,
};
}
return bp;
});
// Since we might not have made any actual changes, we verify first
// to avoid unnecessary changes in the state.
if (state.breakpoints.some((bp, i) => breakpoints[i] !== bp)) {
state = {
...state,
breakpoints,
};
}
break;
}
}
return state;
}
exports.getDOMMutationBreakpoints = getDOMMutationBreakpoints;
function getDOMMutationBreakpoints(state) {
return state.domMutationBreakpoints.breakpoints;
}
exports.getDOMMutationBreakpoint = getDOMMutationBreakpoint;
function getDOMMutationBreakpoint(state, id) {
return (
state.domMutationBreakpoints.breakpoints.find(v => v.id === id) || null
);
}

Просмотреть файл

@ -0,0 +1,8 @@
/* 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/. */
"use strict";
module.exports = {
domMutationBreakpoints: require("./dom-mutation-breakpoints").reducer,
};

Просмотреть файл

@ -0,0 +1,11 @@
# 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/.
DevToolsModules(
'dom-mutation-breakpoints.js',
'index.js',
)
with Files('**'):
BUG_COMPONENT = ('DevTools', 'Framework')

Просмотреть файл

@ -0,0 +1,8 @@
/* 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/. */
"use strict";
const { createProvider } = require("devtools/client/shared/vendor/react-redux");
module.exports = createProvider("toolbox-store");

Просмотреть файл

@ -0,0 +1,13 @@
/* 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/. */
"use strict";
const createStore = require("devtools/client/shared/redux/create-store");
const reducers = require("./reducers/index");
exports.createToolboxStore = () =>
createStore(reducers, {
// Uncomment this for logging in tests.
// shouldLog: true,
});

Просмотреть файл

@ -25,6 +25,20 @@ var ChromeUtils = require("ChromeUtils");
var { gDevTools } = require("devtools/client/framework/devtools");
var EventEmitter = require("devtools/shared/event-emitter");
var Telemetry = require("devtools/client/shared/telemetry");
loader.lazyRequireGetter(
this,
"createToolboxStore",
"devtools/client/framework/store",
true
);
loader.lazyRequireGetter(
this,
"registerWalkerListeners",
"devtools/client/framework/actions/index",
true
);
const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
var {
DOMHelpers,
@ -328,6 +342,13 @@ Toolbox.prototype = {
SIDE_ENABLED: "devtools.toolbox.sideEnabled",
},
get store() {
if (!this._store) {
this._store = createToolboxStore();
}
return this._store;
},
get currentToolId() {
return this._currentToolId;
},
@ -3321,6 +3342,7 @@ Toolbox.prototype = {
this.walker.on("highlighter-ready", this._highlighterReady);
this.walker.on("highlighter-hide", this._highlighterHidden);
this._selection.on("new-node-front", this._onNewSelectedNodeFront);
registerWalkerListeners(this);
}.bind(this)();
}
return this._initInspector;

Просмотреть файл

@ -21,6 +21,18 @@ const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/
const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
const RootContainer = require("devtools/client/inspector/markup/views/root-container");
loader.lazyRequireGetter(
this,
"createDOMMutationBreakpoint",
"devtools/client/framework/actions/index",
true
);
loader.lazyRequireGetter(
this,
"deleteDOMMutationBreakpoint",
"devtools/client/framework/actions/index",
true
);
loader.lazyRequireGetter(
this,
"MarkupContextMenu",
@ -1270,11 +1282,14 @@ MarkupView.prototype = {
return;
}
const toolboxStore = this.inspector.toolbox.store;
const nodeFront = this.inspector.selection.nodeFront;
const mutationBreakpoints = nodeFront.mutationBreakpoints;
await this.walker.setMutationBreakpoints(nodeFront, {
[name]: !mutationBreakpoints[name],
});
if (nodeFront.mutationBreakpoints[name]) {
toolboxStore.dispatch(deleteDOMMutationBreakpoint(nodeFront, name));
} else {
toolboxStore.dispatch(createDOMMutationBreakpoint(nodeFront, name));
}
},
/**

Просмотреть файл

@ -1941,7 +1941,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
return obj;
}, {});
this._updateMutationBreakpointState(rawNode, {
this._updateMutationBreakpointState("api", rawNode, {
...this.getMutationBreakpoints(node),
...bpsForNode,
});
@ -1953,7 +1953,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
* @param {Node} rawNode The DOM node.
* @param {Object} bpsForNode The state of each mutation bp type we support.
*/
_updateMutationBreakpointState(rawNode, bpsForNode) {
_updateMutationBreakpointState(mutationReason, rawNode, bpsForNode) {
const rawDoc = rawNode.ownerDocument || rawNode;
const docMutationBreakpoints = this._mutationBreakpointsForDoc(
@ -2000,6 +2000,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
target: actor.actorID,
type: "mutationBreakpoint",
mutationBreakpoints: this.getMutationBreakpoints(actor),
mutationReason,
});
}
},
@ -2145,7 +2146,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
const walker = this.getDocumentWalker(targetNode);
do {
this._updateMutationBreakpointState(walker.currentNode, null);
this._updateMutationBreakpointState("detach", walker.currentNode, null);
} while (walker.nextNode());
},
@ -2499,7 +2500,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
const mutationBps = this._mutationBreakpointsForDoc(doc);
const nodes = mutationBps ? Array.from(mutationBps.nodes.keys()) : [];
for (const node of nodes) {
this._updateMutationBreakpointState(node, null);
this._updateMutationBreakpointState("unload", node, null);
}
if (this.rootDoc === doc) {