1
0
Fork 0
firefox/devtools/server/tests/browser/browser_inspector-search.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

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");
}
);
});