diff --git a/devtools/client/fronts/walker.js b/devtools/client/fronts/walker.js index 60a148b225b9..98baeed9d05f 100644 --- a/devtools/client/fronts/walker.js +++ b/devtools/client/fronts/walker.js @@ -391,64 +391,6 @@ class WalkerFront extends FrontClassWithSpec(walkerSpec) { documentNode.reparent(parentNode); } - /** - * Evaluate the cross iframes query selectors for the current walker front. - * - * @param {Array} selectors - * An array of CSS selectors to find the target accessible object. - * Several selectors can be needed if the element is nested in frames - * and not directly in the root document. - * @return {Promise} a promise that resolves when the node front is found for - * selection using inspector tools. - */ - async findNodeFront(nodeSelectors) { - const querySelectors = async nodeFront => { - const selector = nodeSelectors.shift(); - if (!selector) { - return nodeFront; - } - nodeFront = await nodeFront.walkerFront.querySelector( - nodeFront, - selector - ); - // It's possible the containing iframe isn't available by the time - // walkerFront.querySelector is called, which causes the re-selected node to be - // unavailable. There also isn't a way for us to know when all iframes on the page - // have been created after a reload. Because of this, we should should bail here. - if (!nodeFront) { - return null; - } - - if (nodeSelectors.length > 0) { - await nodeFront.waitForFrameLoad(); - - const { nodes } = await this.children(nodeFront); - - // If there are remaining selectors to process, they will target a document or a - // document-fragment under the current node. Whether the element is a frame or - // a web component, it can only contain one document/document-fragment, so just - // select the first one available. - nodeFront = nodes.find(node => { - const { nodeType } = node; - return ( - nodeType === Node.DOCUMENT_FRAGMENT_NODE || - nodeType === Node.DOCUMENT_NODE - ); - }); - - // The iframe selector might have matched an element which is not an - // iframe in the new page (or an iframe with no document?). In this - // case, bail out and fallback to the root body element. - if (!nodeFront) { - return null; - } - } - return querySelectors(nodeFront) || nodeFront; - }; - const nodeFront = await this.getRootNode(); - return querySelectors(nodeFront); - } - _onRootNodeAvailable(rootNode) { if (rootNode.isTopLevelDocument) { this.rootNode = rootNode; diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js index 70cdfd4604cd..93ab0b698cc6 100644 --- a/devtools/client/inspector/inspector.js +++ b/devtools/client/inspector/inspector.js @@ -571,7 +571,12 @@ Inspector.prototype = { // Try to find a default node using three strategies: const defaultNodeSelectors = [ // - first try to match css selectors for the selection - () => (cssSelectors.length ? walker.findNodeFront(cssSelectors) : null), + () => + cssSelectors.length + ? this.commands.inspectorCommand.findNodeFrontFromSelectors( + cssSelectors + ) + : null, // - otherwise try to get the "body" element () => walker.querySelector(rootNodeFront, "body"), // - finally get the documentElement element if nothing else worked. diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js index c42858169172..1de2b29a1854 100644 --- a/devtools/client/inspector/shared/highlighters-overlay.js +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -1381,7 +1381,9 @@ class HighlightersOverlay { return; } - const nodeFront = await this.inspectorFront.walker.findNodeFront(selectors); + const nodeFront = await this.inspector.commands.inspectorCommand.findNodeFrontFromSelectors( + selectors + ); if (nodeFront) { await showFunction(nodeFront, options); diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js index 732c6ca7b287..c6917bc5ad4c 100644 --- a/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js +++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_context_selector_inspector.js @@ -11,6 +11,12 @@ const FILE_FOLDER = `browser/devtools/client/webconsole/test/browser`; const TEST_URI = `https://example.com/${FILE_FOLDER}/test-console-evaluation-context-selector.html`; const IFRAME_PATH = `${FILE_FOLDER}/test-console-evaluation-context-selector-child.html`; +// Import helpers for the inspector +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", + this +); + requestLongerTimeout(2); add_task(async function() { @@ -29,7 +35,7 @@ add_task(async function() { await waitForEagerEvaluationResult(hud, `"example.com"`); info("Go to the inspector panel"); - const inspector = await openInspector(); + const inspector = await hud.toolbox.selectTool("inspector"); info("Expand all the nodes"); await inspector.markup.expandAll(); @@ -38,7 +44,7 @@ add_task(async function() { await hud.toolbox.openSplitConsole(); info("Select the first iframe h2 element"); - await selectIframeContentElement(inspector, ".iframe-1", "h2"); + await selectNodeInFrames([".iframe-1", "h2"], inspector); await waitFor(() => evaluationContextSelectorButton.innerText.includes("example.org") @@ -49,7 +55,7 @@ add_task(async function() { ok(true, "The instant evaluation result is updated in the iframe context"); info("Select the second iframe h2 element"); - await selectIframeContentElement(inspector, ".iframe-2", "h2"); + await selectNodeInFrames([".iframe-2", "h2"], inspector); await waitFor(() => evaluationContextSelectorButton.innerText.includes("example.net") @@ -60,9 +66,7 @@ add_task(async function() { ok(true, "The instant evaluation result is updated in the iframe context"); info("Select an element in the top document"); - const h1NodeFront = await inspector.walker.findNodeFront(["h1"]); - inspector.selection.setNodeFront(null); - inspector.selection.setNodeFront(h1NodeFront); + await selectNodeInFrames(["h1"], inspector); await waitForEagerEvaluationResult(hud, `"example.com"`); await waitFor(() => @@ -75,8 +79,7 @@ add_task(async function() { await testUseInConsole( hud, inspector, - ".iframe-1", - "h2", + [".iframe-1", "h2"], "temp0", `

` ); @@ -91,8 +94,7 @@ add_task(async function() { await testUseInConsole( hud, inspector, - ".iframe-2", - "h2", + [".iframe-2", "h2"], "temp0", `

` ); @@ -107,8 +109,7 @@ add_task(async function() { await testUseInConsole( hud, inspector, - ":root", - "h1", + ["h1"], "temp0", `

` ); @@ -118,35 +119,14 @@ add_task(async function() { ok(true, "The context selector was updated"); }); -async function selectIframeContentElement( - inspector, - iframeSelector, - iframeContentSelector -) { - inspector.selection.setNodeFront(null); - const iframeNodeFront = await inspector.walker.findNodeFront([ - iframeSelector, - ]); - const childrenNodeFront = await iframeNodeFront - .treeChildren()[0] - .walkerFront.findNodeFront([iframeContentSelector]); - inspector.selection.setNodeFront(childrenNodeFront); - return childrenNodeFront; -} - async function testUseInConsole( hud, inspector, - iframeSelector, - iframeContentSelector, + selectors, variableName, expectedTextResult ) { - const nodeFront = await selectIframeContentElement( - inspector, - iframeSelector, - iframeContentSelector - ); + const nodeFront = await selectNodeInFrames(selectors, inspector); const container = inspector.markup.getContainer(nodeFront); // Clear the input before clicking on "Use in Console" to workaround an bug diff --git a/devtools/shared/commands/inspector/inspector-command.js b/devtools/shared/commands/inspector/inspector-command.js index ce9f02c0812c..5f9b0f0c6853 100644 --- a/devtools/shared/commands/inspector/inspector-command.js +++ b/devtools/shared/commands/inspector/inspector-command.js @@ -134,6 +134,98 @@ class InspectorCommand { // Descending sort the list by count, i.e. second element of the arrays return sortSuggestions(mergedSuggestions); } + + /** + * Find a nodeFront from an array of selectors. The last item of the array is the selector + * for the element in its owner document, and the previous items are selectors to iframes + * that lead to the frame where the searched node lives in. + * + * For example, with the following markup + * + * + * + * + * If you want to retrieve the `

` nodeFront, `selectors` would be: + * [ + * "#level-1", + * "#level-2", + * "h1", + * ] + * + * @param {Array} selectors + * An array of CSS selectors to find the target accessible object. + * Several selectors can be needed if the element is nested in frames + * and not directly in the root document. + * @return {Promise} a promise that resolves when the node front is found + * for selection using inspector tools. It resolves with the deepest frame document + * that could be retrieved when the "final" nodeFront couldn't be found in the page. + */ + async findNodeFrontFromSelectors(nodeSelectors) { + if ( + !nodeSelectors || + !Array.isArray(nodeSelectors) || + nodeSelectors.length === 0 + ) { + console.warn( + "findNodeFrontFromSelectors expect a non-empty array but got", + nodeSelectors + ); + return null; + } + + const { walker } = await this.commands.targetCommand.targetFront.getFront( + "inspector" + ); + const querySelectors = async nodeFront => { + const selector = nodeSelectors.shift(); + if (!selector) { + return nodeFront; + } + nodeFront = await nodeFront.walkerFront.querySelector( + nodeFront, + selector + ); + // It's possible the containing iframe isn't available by the time + // walkerFront.querySelector is called, which causes the re-selected node to be + // unavailable. There also isn't a way for us to know when all iframes on the page + // have been created after a reload. Because of this, we should should bail here. + if (!nodeFront) { + return null; + } + + if (nodeSelectors.length > 0) { + await nodeFront.waitForFrameLoad(); + + const { nodes } = await walker.children(nodeFront); + + // If there are remaining selectors to process, they will target a document or a + // document-fragment under the current node. Whether the element is a frame or + // a web component, it can only contain one document/document-fragment, so just + // select the first one available. + nodeFront = nodes.find(node => { + const { nodeType } = node; + return ( + nodeType === Node.DOCUMENT_FRAGMENT_NODE || + nodeType === Node.DOCUMENT_NODE + ); + }); + + // The iframe selector might have matched an element which is not an + // iframe in the new page (or an iframe with no document?). In this + // case, bail out and fallback to the root body element. + if (!nodeFront) { + return null; + } + } + const childrenNodeFront = await querySelectors(nodeFront); + return childrenNodeFront || nodeFront; + }; + const rootNodeFront = await walker.getRootNode(); + return querySelectors(rootNodeFront); + } } // This is a fork of the server sort: diff --git a/devtools/shared/commands/inspector/tests/browser.ini b/devtools/shared/commands/inspector/tests/browser.ini index f28bd5d4b6d4..a3290ed82d49 100644 --- a/devtools/shared/commands/inspector/tests/browser.ini +++ b/devtools/shared/commands/inspector/tests/browser.ini @@ -7,5 +7,6 @@ support-files = !/devtools/client/shared/test/highlighter-test-actor.js head.js +[browser_inspector_command_findNodeFrontFromSelectors.js] [browser_inspector_command_getSuggestionsForQuery.js] [browser_inspector_command_search.js] diff --git a/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js b/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js new file mode 100644 index 000000000000..ecd9ab6a2f4d --- /dev/null +++ b/devtools/shared/commands/inspector/tests/browser_inspector_command_findNodeFrontFromSelectors.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + // Build a simple test page with a remote iframe, using two distinct origins .com and .org + const iframeHtml = encodeURIComponent(`

`); + const html = encodeURIComponent( + `
+ +
+ ` + ); + const tab = await addTab( + "https://example.com/document-builder.sjs?html=" + html + ); + + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + info("Check that it returns null when no params are passed"); + let nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors(); + is( + nodeFront, + null, + `findNodeFrontFromSelectors returns null when no param is passed` + ); + + info("Check that it returns null when a string is passed"); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors( + "body main" + ); + is( + nodeFront, + null, + `findNodeFrontFromSelectors returns null when passed a string` + ); + + info("Check it returns null when an empty array is passed"); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([]); + is( + nodeFront, + null, + `findNodeFrontFromSelectors returns null when passed an empty array` + ); + + info("Check that passing a selector for a non-matching element returns null"); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([ + "h1", + ]); + is( + nodeFront, + null, + "findNodeFrontFromSelectors returns null as there's no

element in the page" + ); + + info("Check passing a selector for an element in the top document"); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([ + "button", + ]); + is( + nodeFront.typeName, + "domnode", + "findNodeFrontFromSelectors returns a nodeFront" + ); + is( + nodeFront.displayName, + "button", + "findNodeFrontFromSelectors returned the appropriate nodeFront" + ); + + info("Check passing a selector for an element in an iframe"); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([ + "iframe", + "#in-iframe", + ]); + is( + nodeFront.displayName, + "h2", + "findNodeFrontFromSelectors returned the appropriate nodeFront" + ); + + info( + "Check passing a selector for an non-existing element in an existing iframe" + ); + nodeFront = await commands.inspectorCommand.findNodeFrontFromSelectors([ + "iframe", + "#non-existant-id", + ]); + is( + nodeFront.displayName, + "#document", + "findNodeFrontFromSelectors returned the last matching iframe document if the children selector isn't found" + ); + is( + nodeFront.parentNode().displayName, + "iframe", + "findNodeFrontFromSelectors returned the last matching iframe document if the children selector isn't found" + ); + + await commands.destroy(); +});