diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index e5349e814e0b..5f2e300f218d 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -3021,6 +3021,54 @@ Toolbox.prototype = { return this._initInspector; }, + /** + * An helper function that returns an object contain a highlighter and unhighlighter + * function. + * + * @param {Boolean} isGrip: Set to true if the `highlight` function is going to be + * called with a Grip (and not from a NodeFront). + * @returns {Object} an object of the following shape: + * - {AsyncFunction} highlight: A function that will initialize the highlighter front + * and call highlighter.highlight with the provided node + * front (which will be retrieved from a grip, if + * `fromGrip` is true.) + * - {AsyncFunction} unhighlight: A function that will unhighlight the node that is + * currently highlighted. If the `highlight` function + * isn't settled yet, it will wait until it's done and + * then unhighlight to prevent zombie highlighters. + * + */ + getHighlighter(fromGrip = false) { + let pendingHighlight; + return { + highlight: async (nodeFront, options) => { + pendingHighlight = (async () => { + await this.initInspector(); + if (!this.highlighter) { + return null; + } + + if (fromGrip) { + nodeFront = await this.walker.gripToNodeFront(nodeFront); + } + + return this.highlighter.highlight(nodeFront, options); + })(); + return pendingHighlight; + }, + unhighlight: async (forceHide) => { + if (pendingHighlight) { + await pendingHighlight; + pendingHighlight = null; + } + + return this.highlighter + ? this.highlighter.unhighlight(forceHide) + : null; + }, + }; + }, + _onNewSelectedNodeFront: function() { // Emit a "selection-changed" event when the toolbox.selection has been set // to a new node (or cleared). Currently used in the WebExtensions APIs (to diff --git a/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js b/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js index c6bc061e5e27..9864c41023d5 100644 --- a/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js @@ -21,10 +21,16 @@ const HTML = ` `; const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); +// Import helpers registering the test-actor in remote targets +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js", this); + add_task(async function() { const hud = await openNewTabAndConsole(TEST_URI); const toolbox = gDevTools.getToolbox(hud.target); + await registerTestActor(toolbox.target.client); + const testActor = await getTestActor(toolbox); + await ContentTask.spawn(gBrowser.selectedBrowser, null, () => { content.wrappedJSObject.logNode("h1"); }); @@ -33,24 +39,38 @@ add_task(async function() { const node = msg.querySelector(".objectBox-node"); ok(node !== null, "Node was logged as expected"); const view = node.ownerDocument.defaultView; + const nonHighlightEl = toolbox.doc.getElementById("toolbox-meatball-menu-button"); info("Highlight the node by moving the cursor on it"); // the inspector should be initialized first and then the node should // highlight after the hover effect. - const onNodeHighlight = toolbox.target.once("inspector") + let onNodeHighlight = toolbox.target.once("inspector") .then(inspector => inspector.highlighter.once("node-highlight")); EventUtils.synthesizeMouseAtCenter(node, {type: "mousemove"}, view); const nodeFront = await onNodeHighlight; is(nodeFront.displayName, "h1", "The correct node was highlighted"); + isVisible = await testActor.isHighlighting(); + ok(isVisible, "Highlighter is displayed"); info("Unhighlight the node by moving away from the node"); - const onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight"); - const btn = toolbox.doc.getElementById("toolbox-meatball-menu-button"); - EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, view); + let onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight"); + EventUtils.synthesizeMouseAtCenter(nonHighlightEl, {type: "mousemove"}, view); await onNodeUnhighlight; ok(true, "node-unhighlight event was fired when moving away from the node"); + + info("Check we don't have zombie highlighters when briefly hovering a node"); + onNodeHighlight = toolbox.highlighter.once("node-highlight"); + onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight"); + // Move hover the node and then right after move out. + EventUtils.synthesizeMouseAtCenter(node, {type: "mousemove"}, view); + EventUtils.synthesizeMouseAtCenter(nonHighlightEl, {type: "mousemove"}, view); + await Promise.all([onNodeHighlight, onNodeUnhighlight]); + ok(true, "The highlighter was removed"); + + isVisible = await testActor.isHighlighting(); + is(isVisible, false, "The highlighter is not displayed anymore"); }); diff --git a/devtools/client/webconsole/webconsole-wrapper.js b/devtools/client/webconsole/webconsole-wrapper.js index 6049173f8ce5..f9ffda684365 100644 --- a/devtools/client/webconsole/webconsole-wrapper.js +++ b/devtools/client/webconsole/webconsole-wrapper.js @@ -278,6 +278,8 @@ class WebConsoleWrapper { this.toolbox.threadClient.on("paused", this.dispatchPaused); this.toolbox.threadClient.on("progress", this.dispatchProgress); + const {highlight, unhighlight} = this.toolbox.getHighlighter(true); + Object.assign(serviceContainer, { onViewSourceInDebugger: frame => { this.toolbox.viewSourceInDebugger( @@ -318,19 +320,8 @@ class WebConsoleWrapper { }); }, sourceMapService: this.toolbox ? this.toolbox.sourceMapURLService : null, - highlightDomElement: async (grip, options = {}) => { - await this.toolbox.initInspector(); - if (!this.toolbox.highlighter) { - return null; - } - const nodeFront = await this.toolbox.walker.gripToNodeFront(grip); - return this.toolbox.highlighter.highlight(nodeFront, options); - }, - unHighlightDomElement: (forceHide = false) => { - return this.toolbox.highlighter - ? this.toolbox.highlighter.unhighlight(forceHide) - : null; - }, + highlightDomElement: highlight, + unHighlightDomElement: unhighlight, openNodeInInspector: async (grip) => { await this.toolbox.initInspector(); const onSelectInspector = this.toolbox.selectTool("inspector", "inspect_dom");