tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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