458 lines
15 KiB
JavaScript
458 lines
15 KiB
JavaScript
/* 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 for selector with :has()",
|
|
search: "article:has(p)",
|
|
expected: [
|
|
{
|
|
node: inspectee.querySelectorAll("aside article")[0],
|
|
type: "selector",
|
|
},
|
|
{
|
|
node: inspectee.querySelectorAll("aside article")[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 for attributeName=attributeValue pairs without quotation marks",
|
|
search: "id=arrows",
|
|
expected: [
|
|
{ node: inspectee.getElementById("arrows"), type: "attributeName" },
|
|
],
|
|
},
|
|
{
|
|
desc: "Search for attributeName=attributeValue pairs with quotation marks",
|
|
search: 'id="arrows"',
|
|
expected: [
|
|
{ node: inspectee.getElementById("arrows"), type: "attributeName" },
|
|
],
|
|
},
|
|
{
|
|
desc: "Search for attributeName=attributeValue pairs with partial quotation marks",
|
|
search: 'id="arr',
|
|
expected: [
|
|
{ node: inspectee.getElementById("arrows"), type: "attributeName" },
|
|
],
|
|
},
|
|
{
|
|
desc: `Search for unmatched attributeName="attr"`,
|
|
search: 'id="arr"',
|
|
expected: [],
|
|
},
|
|
{
|
|
desc: "Search for attributeName=",
|
|
search: "id=",
|
|
expected: [
|
|
{ node: inspectee.getElementById("pseudo"), type: "attributeName" },
|
|
{ node: inspectee.getElementById("arrows"), type: "attributeName" },
|
|
{ node: inspectee.getElementById("💩"), type: "attributeName" },
|
|
],
|
|
},
|
|
{
|
|
desc: "Search for =attributeValue",
|
|
search: "=arr",
|
|
expected: [
|
|
{
|
|
node: inspectee.getElementById("arrows"),
|
|
type: "attributeValue",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
desc: `Search for ="attributeValue`,
|
|
search: `="arr`,
|
|
expected: [
|
|
{
|
|
node: inspectee.getElementById("arrows"),
|
|
type: "attributeValue",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
desc: `Search for ="attributeValue"`,
|
|
search: `="arrows"`,
|
|
expected: [
|
|
{
|
|
node: inspectee.getElementById("arrows"),
|
|
type: "attributeValue",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
desc: `Search for unmatched ="attributeValue"`,
|
|
search: `="arr"`,
|
|
expected: [],
|
|
},
|
|
{
|
|
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 via strict equal text",
|
|
search: "//*[text()='Heading 1']",
|
|
expected: [
|
|
{ node: inspectee.querySelector("h1#pseudo"), 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 assertSearchResults = (searchResults, expectedResults, msg) => {
|
|
is(
|
|
searchResults.length,
|
|
expectedResults.length,
|
|
`${msg} - got expected number of results`
|
|
);
|
|
if (searchResults.length === expectedResults.length) {
|
|
searchResults.forEach((result, i) => {
|
|
const { type, node } = expectedResults[i];
|
|
is(result.type, type, `${msg} - result #${i} type`);
|
|
if (result.node != node) {
|
|
const displayNode = el => {
|
|
return `<${el.nodeName.toLowerCase()}${el.id ? "#" + el.id : ""}>`;
|
|
};
|
|
ok(
|
|
false,
|
|
`${msg} - result #${i} unexpected node: Got ${displayNode(result.node)}, expected ${displayNode(node)}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
for (const { desc, search, expected } of testData) {
|
|
info("Running test: " + desc);
|
|
results = walkerSearch.search(search);
|
|
assertSearchResults(
|
|
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");
|
|
assertSearchResults(
|
|
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");
|
|
assertSearchResults(
|
|
results,
|
|
[
|
|
{ node: styleText, type: "text" },
|
|
{ node: beforeElt, type: "text" },
|
|
],
|
|
"Text search works for pseudo element"
|
|
);
|
|
|
|
// ::after
|
|
results = walkerSearch.search("::after");
|
|
assertSearchResults(
|
|
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");
|
|
assertSearchResults(
|
|
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");
|
|
assertSearchResults(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");
|
|
assertSearchResults(
|
|
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");
|
|
assertSearchResults(
|
|
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");
|
|
}
|
|
);
|
|
});
|