browser_inspector-search.js (16817B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 Services.scriptloader.loadSubScript( 7 "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js", 8 this 9 ); 10 11 // Test for Bug 835896 12 // WalkerSearch specific tests. This is to make sure search results are 13 // coming back as expected. 14 // See also test_inspector-search-front.html. 15 16 add_task(async function () { 17 const { walker } = await initInspectorFront( 18 MAIN_DOMAIN + "inspector-search-data.html" 19 ); 20 21 await SpecialPowers.spawn( 22 gBrowser.selectedBrowser, 23 [[walker.actorID]], 24 async function (actorID) { 25 const { require } = ChromeUtils.importESModule( 26 "resource://devtools/shared/loader/Loader.sys.mjs" 27 ); 28 const { 29 DevToolsServer, 30 } = require("resource://devtools/server/devtools-server.js"); 31 const { 32 DocumentWalker: _documentWalker, 33 } = require("resource://devtools/server/actors/inspector/document-walker.js"); 34 35 // Convert actorID to current compartment string otherwise 36 // searchAllConnectionsForActor is confused and won't find the actor. 37 actorID = String(actorID); 38 const walkerActor = DevToolsServer.searchAllConnectionsForActor(actorID); 39 const walkerSearch = walkerActor.walkerSearch; 40 const { 41 WalkerSearch, 42 WalkerIndex, 43 } = require("resource://devtools/server/actors/utils/walker-search.js"); 44 45 info("Testing basic index APIs exist."); 46 const index = new WalkerIndex(walkerActor); 47 Assert.greater( 48 index.data.size, 49 0, 50 "public index is filled after getting" 51 ); 52 53 index.clearIndex(); 54 ok(!index._data, "private index is empty after clearing"); 55 Assert.greater( 56 index.data.size, 57 0, 58 "public index is filled after getting" 59 ); 60 61 index.destroy(); 62 63 info("Testing basic search APIs exist."); 64 65 ok(walkerSearch, "walker search exists on the WalkerActor"); 66 ok(walkerSearch.search, "walker search has `search` method"); 67 ok(walkerSearch.index, "walker search has `index` property"); 68 is( 69 walkerSearch.walker, 70 walkerActor, 71 "referencing the correct WalkerActor" 72 ); 73 74 const walkerSearch2 = new WalkerSearch(walkerActor); 75 ok(walkerSearch2, "a new search instance can be created"); 76 ok(walkerSearch2.search, "new search instance has `search` method"); 77 ok(walkerSearch2.index, "new search instance has `index` property"); 78 isnot( 79 walkerSearch2, 80 walkerSearch, 81 "new search instance differs from the WalkerActor's" 82 ); 83 84 walkerSearch2.destroy(); 85 86 info("Testing search with an empty query."); 87 let results = walkerSearch.search(""); 88 is(results.length, 0, "No results when searching for ''"); 89 90 results = walkerSearch.search(null); 91 is(results.length, 0, "No results when searching for null"); 92 93 results = walkerSearch.search(undefined); 94 is(results.length, 0, "No results when searching for undefined"); 95 96 results = walkerSearch.search(10); 97 is(results.length, 0, "No results when searching for 10"); 98 99 const inspectee = content.document; 100 const testData = [ 101 { 102 desc: "Search for tag with one result.", 103 search: "body", 104 expected: [ 105 { node: inspectee.body, type: "xpath" }, 106 { node: inspectee.body, type: "tag" }, 107 ], 108 }, 109 { 110 desc: "Search for tag with multiple results", 111 search: "h2", 112 expected: [ 113 { 114 node: inspectee.querySelector("h2:nth-of-type(1)"), 115 type: "selector", 116 }, 117 { node: inspectee.querySelector("h2:nth-of-type(1)"), type: "tag" }, 118 { 119 node: inspectee.querySelector("h2:nth-of-type(2)"), 120 type: "selector", 121 }, 122 { node: inspectee.querySelector("h2:nth-of-type(2)"), type: "tag" }, 123 { 124 node: inspectee.querySelector("h2:nth-of-type(3)"), 125 type: "selector", 126 }, 127 { node: inspectee.querySelector("h2:nth-of-type(3)"), type: "tag" }, 128 ], 129 }, 130 { 131 desc: "Search for selector with multiple results", 132 search: "body > h2", 133 expected: [ 134 { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, 135 { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, 136 { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, 137 ], 138 }, 139 { 140 desc: "Search for selector with multiple results", 141 search: ":root h2", 142 expected: [ 143 { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, 144 { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, 145 { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, 146 ], 147 }, 148 { 149 desc: "Search for selector with multiple results", 150 search: "* h2", 151 expected: [ 152 { node: inspectee.querySelectorAll("h2")[0], type: "selector" }, 153 { node: inspectee.querySelectorAll("h2")[1], type: "selector" }, 154 { node: inspectee.querySelectorAll("h2")[2], type: "selector" }, 155 ], 156 }, 157 { 158 desc: "Search for selector with :has()", 159 search: "article:has(p)", 160 expected: [ 161 { 162 node: inspectee.querySelectorAll("aside article")[0], 163 type: "selector", 164 }, 165 { 166 node: inspectee.querySelectorAll("aside article")[2], 167 type: "selector", 168 }, 169 ], 170 }, 171 { 172 desc: "Search with multiple matches in a single tag expecting a single result", 173 search: "💩", 174 expected: [ 175 { node: inspectee.getElementById("💩"), type: "attributeName" }, 176 { node: inspectee.getElementById("💩"), type: "attributeValue" }, 177 ], 178 }, 179 { 180 desc: "Search for attributeName=attributeValue pairs without quotation marks", 181 search: "id=arrows", 182 expected: [ 183 { node: inspectee.getElementById("arrows"), type: "attributeName" }, 184 ], 185 }, 186 { 187 desc: "Search for attributeName=attributeValue pairs with quotation marks", 188 search: 'id="arrows"', 189 expected: [ 190 { node: inspectee.getElementById("arrows"), type: "attributeName" }, 191 ], 192 }, 193 { 194 desc: "Search for attributeName=attributeValue pairs with partial quotation marks", 195 search: 'id="arr', 196 expected: [ 197 { node: inspectee.getElementById("arrows"), type: "attributeName" }, 198 ], 199 }, 200 { 201 desc: `Search for unmatched attributeName="attr"`, 202 search: 'id="arr"', 203 expected: [], 204 }, 205 { 206 desc: "Search for attributeName=", 207 search: "id=", 208 expected: [ 209 { node: inspectee.getElementById("pseudo"), type: "attributeName" }, 210 { node: inspectee.getElementById("arrows"), type: "attributeName" }, 211 { node: inspectee.getElementById("💩"), type: "attributeName" }, 212 { 213 node: inspectee.getElementById("with-hyphen"), 214 type: "attributeName", 215 }, 216 ], 217 }, 218 { 219 desc: "Search for =attributeValue", 220 search: "=arr", 221 expected: [ 222 { 223 node: inspectee.getElementById("arrows"), 224 type: "attributeValue", 225 }, 226 ], 227 }, 228 { 229 desc: `Search for ="attributeValue`, 230 search: `="arr`, 231 expected: [ 232 { 233 node: inspectee.getElementById("arrows"), 234 type: "attributeValue", 235 }, 236 ], 237 }, 238 { 239 desc: `Search for ="attributeValue"`, 240 search: `="arrows"`, 241 expected: [ 242 { 243 node: inspectee.getElementById("arrows"), 244 type: "attributeValue", 245 }, 246 ], 247 }, 248 { 249 desc: `Search for unmatched ="attributeValue"`, 250 search: `="arr"`, 251 expected: [], 252 }, 253 { 254 desc: "Search that has tag and text results", 255 search: "h1", 256 expected: [ 257 { node: inspectee.querySelector("h1"), type: "selector" }, 258 { node: inspectee.querySelector("h1"), type: "tag" }, 259 { 260 node: inspectee.querySelector("h1 + p").childNodes[0], 261 type: "text", 262 }, 263 { 264 node: inspectee.querySelector("h1 + p > strong").childNodes[0], 265 type: "text", 266 }, 267 ], 268 }, 269 { 270 desc: "Search for XPath with one result", 271 search: "//strong", 272 expected: [ 273 { node: inspectee.querySelector("strong"), type: "xpath" }, 274 ], 275 }, 276 { 277 desc: "Search for XPath with multiple results", 278 search: "//h2", 279 expected: [ 280 { node: inspectee.querySelectorAll("h2")[0], type: "xpath" }, 281 { node: inspectee.querySelectorAll("h2")[1], type: "xpath" }, 282 { node: inspectee.querySelectorAll("h2")[2], type: "xpath" }, 283 ], 284 }, 285 { 286 desc: "Search for XPath via containing text", 287 search: "//*[contains(text(), 'p tag')]", 288 expected: [{ node: inspectee.querySelector("p"), type: "xpath" }], 289 }, 290 { 291 desc: "Search for XPath via strict equal text", 292 search: "//*[text()='Heading 1']", 293 expected: [ 294 { node: inspectee.querySelector("h1#pseudo"), type: "xpath" }, 295 ], 296 }, 297 { 298 desc: "Search for XPath matching text node", 299 search: "//strong/text()", 300 expected: [ 301 { 302 node: inspectee.querySelector("strong").firstChild, 303 type: "xpath", 304 }, 305 ], 306 }, 307 { 308 desc: "Search using XPath grouping expression", 309 search: "(//*)[2]", 310 expected: [{ node: inspectee.querySelector("head"), type: "xpath" }], 311 }, 312 { 313 desc: "Search using XPath function", 314 search: "id('arrows')", 315 expected: [ 316 { node: inspectee.querySelector("#arrows"), type: "xpath" }, 317 ], 318 }, 319 { 320 desc: "Search using div + id with hyphen", 321 search: "div#with-hyphen", 322 expected: [ 323 { 324 node: inspectee.querySelector("div#with-hyphen"), 325 type: "selector", 326 }, 327 ], 328 }, 329 { 330 desc: "Search using div + class with hyphen", 331 search: "div.with-hyphen", 332 expected: [ 333 { 334 node: inspectee.querySelector("div.with-hyphen"), 335 type: "selector", 336 }, 337 ], 338 }, 339 ]; 340 341 const assertSearchResults = (searchResults, expectedResults, msg) => { 342 is( 343 searchResults.length, 344 expectedResults.length, 345 `${msg} - got expected number of results` 346 ); 347 if (searchResults.length === expectedResults.length) { 348 searchResults.forEach((result, i) => { 349 const { type, node } = expectedResults[i]; 350 is(result.type, type, `${msg} - result #${i} type`); 351 if (result.node != node) { 352 const displayNode = el => { 353 return `<${el.nodeName.toLowerCase()}${el.id ? "#" + el.id : ""}>`; 354 }; 355 ok( 356 false, 357 `${msg} - result #${i} unexpected node: Got ${displayNode(result.node)}, expected ${displayNode(node)}` 358 ); 359 } 360 }); 361 } 362 }; 363 364 for (const { desc, search, expected } of testData) { 365 info("Running test: " + desc); 366 results = walkerSearch.search(search); 367 assertSearchResults( 368 results, 369 expected, 370 "Search returns correct results with '" + search + "'" 371 ); 372 } 373 374 info("Testing ::before and ::after element matching"); 375 376 const beforeElt = new _documentWalker( 377 inspectee.querySelector("#pseudo"), 378 inspectee.defaultView 379 ).firstChild(); 380 const afterElt = new _documentWalker( 381 inspectee.querySelector("#pseudo"), 382 inspectee.defaultView 383 ).lastChild(); 384 const styleText = inspectee.querySelector("style").childNodes[0]; 385 386 // ::before 387 results = walkerSearch.search("::before"); 388 assertSearchResults( 389 results, 390 [{ node: beforeElt, type: "tag" }], 391 "Tag search works for pseudo element" 392 ); 393 394 results = walkerSearch.search("_moz_generated_content_before"); 395 is(results.length, 0, "No results for anon tag name"); 396 397 results = walkerSearch.search("before element"); 398 assertSearchResults( 399 results, 400 [ 401 { node: styleText, type: "text" }, 402 { node: beforeElt, type: "text" }, 403 ], 404 "Text search works for pseudo element" 405 ); 406 407 // ::after 408 results = walkerSearch.search("::after"); 409 assertSearchResults( 410 results, 411 [{ node: afterElt, type: "tag" }], 412 "Tag search works for pseudo element" 413 ); 414 415 results = walkerSearch.search("_moz_generated_content_after"); 416 is(results.length, 0, "No results for anon tag name"); 417 418 results = walkerSearch.search("after element"); 419 assertSearchResults( 420 results, 421 [ 422 { node: styleText, type: "text" }, 423 { node: afterElt, type: "text" }, 424 ], 425 "Text search works for pseudo element" 426 ); 427 428 info("Testing search before and after a mutation."); 429 const expected = [ 430 { 431 node: inspectee.querySelector("h3:nth-of-type(1)"), 432 type: "selector", 433 }, 434 { node: inspectee.querySelector("h3:nth-of-type(1)"), type: "tag" }, 435 { 436 node: inspectee.querySelector("h3:nth-of-type(2)"), 437 type: "selector", 438 }, 439 { node: inspectee.querySelector("h3:nth-of-type(2)"), type: "tag" }, 440 { 441 node: inspectee.querySelector("h3:nth-of-type(3)"), 442 type: "selector", 443 }, 444 { node: inspectee.querySelector("h3:nth-of-type(3)"), type: "tag" }, 445 ]; 446 447 results = walkerSearch.search("h3"); 448 assertSearchResults(results, expected, "Search works with tag results"); 449 450 function mutateDocumentAndWaitForMutation(mutationFn) { 451 // eslint-disable-next-line new-cap 452 return new Promise(resolve => { 453 info("Listening to markup mutation on the inspectee"); 454 const observer = new inspectee.defaultView.MutationObserver(resolve); 455 observer.observe(inspectee, { childList: true, subtree: true }); 456 mutationFn(); 457 }); 458 } 459 await mutateDocumentAndWaitForMutation(() => { 460 expected[0].node.remove(); 461 }); 462 463 results = walkerSearch.search("h3"); 464 assertSearchResults( 465 results, 466 [expected[2], expected[3], expected[4], expected[5]], 467 "Results are updated after removal" 468 ); 469 470 // eslint-disable-next-line new-cap 471 await new Promise(resolve => { 472 info("Waiting for a mutation to happen"); 473 const observer = new inspectee.defaultView.MutationObserver(() => { 474 resolve(); 475 }); 476 observer.observe(inspectee, { attributes: true, subtree: true }); 477 inspectee.body.setAttribute("h3", "true"); 478 }); 479 480 results = walkerSearch.search("h3"); 481 assertSearchResults( 482 results, 483 [ 484 { node: inspectee.body, type: "attributeName" }, 485 expected[2], 486 expected[3], 487 expected[4], 488 expected[5], 489 ], 490 "Results are updated after addition" 491 ); 492 493 // eslint-disable-next-line new-cap 494 await new Promise(resolve => { 495 info("Waiting for a mutation to happen"); 496 const observer = new inspectee.defaultView.MutationObserver(() => { 497 resolve(); 498 }); 499 observer.observe(inspectee, { 500 attributes: true, 501 childList: true, 502 subtree: true, 503 }); 504 inspectee.body.removeAttribute("h3"); 505 expected[2].node.remove(); 506 expected[4].node.remove(); 507 }); 508 509 results = walkerSearch.search("h3"); 510 is(results.length, 0, "Results are updated after removal"); 511 } 512 ); 513 });