/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js", this ); // Test for Bug 835896 // WalkerSearch specific tests. This is to make sure search results are // coming back as expected. // See also test_inspector-search-front.html. add_task(async function () { const { walker } = await initInspectorFront( MAIN_DOMAIN + "inspector-search-data.html" ); await SpecialPowers.spawn( gBrowser.selectedBrowser, [[walker.actorID]], async function (actorID) { const { require } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); const { DevToolsServer, } = require("resource://devtools/server/devtools-server.js"); const { DocumentWalker: _documentWalker, } = require("resource://devtools/server/actors/inspector/document-walker.js"); // Convert actorID to current compartment string otherwise // searchAllConnectionsForActor is confused and won't find the actor. actorID = String(actorID); const walkerActor = DevToolsServer.searchAllConnectionsForActor(actorID); const walkerSearch = walkerActor.walkerSearch; const { WalkerSearch, WalkerIndex, } = require("resource://devtools/server/actors/utils/walker-search.js"); info("Testing basic index APIs exist."); const index = new WalkerIndex(walkerActor); Assert.greater( index.data.size, 0, "public index is filled after getting" ); index.clearIndex(); ok(!index._data, "private index is empty after clearing"); Assert.greater( index.data.size, 0, "public index is filled after getting" ); index.destroy(); info("Testing basic search APIs exist."); ok(walkerSearch, "walker search exists on the WalkerActor"); ok(walkerSearch.search, "walker search has `search` method"); ok(walkerSearch.index, "walker search has `index` property"); is( walkerSearch.walker, walkerActor, "referencing the correct WalkerActor" ); const walkerSearch2 = new WalkerSearch(walkerActor); ok(walkerSearch2, "a new search instance can be created"); ok(walkerSearch2.search, "new search instance has `search` method"); ok(walkerSearch2.index, "new search instance has `index` property"); isnot( walkerSearch2, walkerSearch, "new search instance differs from the WalkerActor's" ); walkerSearch2.destroy(); info("Testing search with an empty query."); let results = walkerSearch.search(""); is(results.length, 0, "No results when searching for ''"); results = walkerSearch.search(null); is(results.length, 0, "No results when searching for null"); results = walkerSearch.search(undefined); is(results.length, 0, "No results when searching for undefined"); results = walkerSearch.search(10); is(results.length, 0, "No results when searching for 10"); const inspectee = content.document; const testData = [ { desc: "Search for tag with one result.", search: "body", expected: [{ node: inspectee.body, type: "tag" }], }, { desc: "Search for tag with multiple results", search: "h2", expected: [ { node: inspectee.querySelectorAll("h2")[0], type: "tag" }, { node: inspectee.querySelectorAll("h2")[1], type: "tag" }, { node: inspectee.querySelectorAll("h2")[2], type: "tag" }, ], }, { desc: "Search for selector with multiple results", search: "body > h2", expected: [ { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, ], }, { desc: "Search for selector with multiple results", search: ":root h2", expected: [ { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, ], }, { desc: "Search for selector with multiple results", search: "* h2", expected: [ { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, ], }, { desc: "Search with multiple matches in a single tag expecting a single result", search: "💩", expected: [ { node: inspectee.getElementById("💩"), type: "attributeValue" }, ], }, { desc: "Search that has tag and text results", search: "h1", expected: [ { node: inspectee.querySelector("h1"), type: "tag" }, { node: inspectee.querySelector("h1 + p").childNodes[0], type: "text", }, { node: inspectee.querySelector("h1 + p > strong").childNodes[0], type: "text", }, ], }, { desc: "Search for XPath with one result", search: "//strong", expected: [ { node: inspectee.querySelector("strong"), type: "xpath" }, ], }, { desc: "Search for XPath with multiple results", search: "//h2", expected: [ { node: inspectee.querySelectorAll("h2")[0], type: "xpath" }, { node: inspectee.querySelectorAll("h2")[1], type: "xpath" }, { node: inspectee.querySelectorAll("h2")[2], type: "xpath" }, ], }, { desc: "Search for XPath via containing text", search: "//*[contains(text(), 'p tag')]", expected: [{ node: inspectee.querySelector("p"), type: "xpath" }], }, { desc: "Search for XPath matching text node", search: "//strong/text()", expected: [ { node: inspectee.querySelector("strong").firstChild, type: "xpath", }, ], }, { desc: "Search using XPath grouping expression", search: "(//*)[2]", expected: [{ node: inspectee.querySelector("head"), type: "xpath" }], }, { desc: "Search using XPath function", search: "id('arrows')", expected: [ { node: inspectee.querySelector("#arrows"), type: "xpath" }, ], }, ]; const isDeeply = (a, b, msg) => { return is(JSON.stringify(a), JSON.stringify(b), msg); }; for (const { desc, search, expected } of testData) { info("Running test: " + desc); results = walkerSearch.search(search); isDeeply( results, expected, "Search returns correct results with '" + search + "'" ); } info("Testing ::before and ::after element matching"); const beforeElt = new _documentWalker( inspectee.querySelector("#pseudo"), inspectee.defaultView ).firstChild(); const afterElt = new _documentWalker( inspectee.querySelector("#pseudo"), inspectee.defaultView ).lastChild(); const styleText = inspectee.querySelector("style").childNodes[0]; // ::before results = walkerSearch.search("::before"); isDeeply( results, [{ node: beforeElt, type: "tag" }], "Tag search works for pseudo element" ); results = walkerSearch.search("_moz_generated_content_before"); is(results.length, 0, "No results for anon tag name"); results = walkerSearch.search("before element"); isDeeply( results, [ { node: styleText, type: "text" }, { node: beforeElt, type: "text" }, ], "Text search works for pseudo element" ); // ::after results = walkerSearch.search("::after"); isDeeply( results, [{ node: afterElt, type: "tag" }], "Tag search works for pseudo element" ); results = walkerSearch.search("_moz_generated_content_after"); is(results.length, 0, "No results for anon tag name"); results = walkerSearch.search("after element"); isDeeply( results, [ { node: styleText, type: "text" }, { node: afterElt, type: "text" }, ], "Text search works for pseudo element" ); info("Testing search before and after a mutation."); const expected = [ { node: inspectee.querySelectorAll("h3")[0], type: "tag" }, { node: inspectee.querySelectorAll("h3")[1], type: "tag" }, { node: inspectee.querySelectorAll("h3")[2], type: "tag" }, ]; results = walkerSearch.search("h3"); isDeeply(results, expected, "Search works with tag results"); function mutateDocumentAndWaitForMutation(mutationFn) { // eslint-disable-next-line new-cap return new Promise(resolve => { info("Listening to markup mutation on the inspectee"); const observer = new inspectee.defaultView.MutationObserver(resolve); observer.observe(inspectee, { childList: true, subtree: true }); mutationFn(); }); } await mutateDocumentAndWaitForMutation(() => { expected[0].node.remove(); }); results = walkerSearch.search("h3"); isDeeply( results, [expected[1], expected[2]], "Results are updated after removal" ); // eslint-disable-next-line new-cap await new Promise(resolve => { info("Waiting for a mutation to happen"); const observer = new inspectee.defaultView.MutationObserver(() => { resolve(); }); observer.observe(inspectee, { attributes: true, subtree: true }); inspectee.body.setAttribute("h3", "true"); }); results = walkerSearch.search("h3"); isDeeply( results, [ { node: inspectee.body, type: "attributeName" }, expected[1], expected[2], ], "Results are updated after addition" ); // eslint-disable-next-line new-cap await new Promise(resolve => { info("Waiting for a mutation to happen"); const observer = new inspectee.defaultView.MutationObserver(() => { resolve(); }); observer.observe(inspectee, { attributes: true, childList: true, subtree: true, }); inspectee.body.removeAttribute("h3"); expected[1].node.remove(); expected[2].node.remove(); }); results = walkerSearch.search("h3"); is(results.length, 0, "Results are updated after removal"); } ); });