tor-browser

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

browser_highlights.js (13891B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* import-globals-from ../../mochitest/text.js */
      8 /* import-globals-from ../../mochitest/attributes.js */
      9 loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
     10 
     11 const boldAttrs = { "font-weight": "700" };
     12 const highlightAttrs = { mark: "true" };
     13 const fragmentAttrs = highlightAttrs;
     14 const spellingAttrs = { invalid: "spelling" };
     15 const grammarAttrs = { invalid: "grammar" };
     16 const snippet = `
     17 <p id="first">The first phrase.</p>
     18 <p id="second">The <i>second <b>phrase.</b></i></p>
     19 `;
     20 
     21 /**
     22 * Returns a promise that resolves once the attribute ranges match. If
     23 * shouldWaitForEvent is true, we first wait for a text attribute change event.
     24 */
     25 async function waitForTextAttrRanges(
     26  acc,
     27  ranges,
     28  attrs,
     29  shouldWaitForEvent = true
     30 ) {
     31  if (shouldWaitForEvent) {
     32    await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED);
     33  }
     34  await untilCacheOk(
     35    () => textAttrRangesMatch(acc, ranges, attrs),
     36    `Attr ranges match: ${JSON.stringify(ranges)}`
     37  );
     38 }
     39 
     40 /**
     41 * Test a text fragment within a single node.
     42 */
     43 addAccessibleTask(
     44  snippet,
     45  async function testTextFragmentSingleNode(browser, docAcc) {
     46    const first = findAccessibleChildByID(docAcc, "first");
     47    ok(
     48      textAttrRangesMatch(
     49        first,
     50        [
     51          [4, 16], // "first phrase"
     52        ],
     53        fragmentAttrs
     54      ),
     55      "first attr ranges correct"
     56    );
     57    const second = findAccessibleChildByID(docAcc, "second");
     58    ok(
     59      textAttrRangesMatch(second, [], fragmentAttrs),
     60      "second attr ranges correct"
     61    );
     62  },
     63  { chrome: true, topLevel: true, urlSuffix: "#:~:text=first%20phrase" }
     64 );
     65 
     66 /**
     67 * Test a text fragment crossing nodes.
     68 */
     69 addAccessibleTask(
     70  snippet,
     71  async function testTextFragmentCrossNode(browser, docAcc) {
     72    const first = findAccessibleChildByID(docAcc, "first");
     73    ok(
     74      textAttrRangesMatch(first, [], fragmentAttrs),
     75      "first attr ranges correct"
     76    );
     77    const second = findAccessibleChildByID(docAcc, "second");
     78    ok(
     79      textAttrRangesMatch(
     80        second,
     81        [
     82          // This run is split because of the bolded word.
     83          [4, 11], // "second "
     84          [11, 17], // "phrase"
     85        ],
     86        fragmentAttrs
     87      ),
     88      "second attr ranges correct"
     89    );
     90    // Ensure bold is still exposed in the presence of a fragment.
     91    testTextAttrs(
     92      second,
     93      11,
     94      { ...fragmentAttrs, ...boldAttrs },
     95      {},
     96      11,
     97      17,
     98      true
     99    ); // "phrase"
    100    testTextAttrs(second, 17, boldAttrs, {}, 17, 18, true); // "."
    101  },
    102  { chrome: true, topLevel: true, urlSuffix: "#:~:text=second%20phrase" }
    103 );
    104 
    105 /**
    106 * Test scrolling to a text fragment on the same page. This also tests that the
    107 * scrolling start event is fired.
    108 */
    109 add_task(async function testTextFragmentSamePage() {
    110  // We use add_task here because we need to verify that an
    111  // event is fired, but it might be fired before document load complete, so we
    112  // could miss it if we used addAccessibleTask.
    113  const docUrl = snippetToURL(snippet);
    114  const initialUrl = docUrl + "#:~:text=first%20phrase";
    115  let scrolled = waitForEvent(
    116    EVENT_SCROLLING_START,
    117    event =>
    118      event.accessible.role == ROLE_TEXT_LEAF &&
    119      getAccessibleDOMNodeID(event.accessible.parent) == "first"
    120  );
    121  await BrowserTestUtils.withNewTab(initialUrl, async function (browser) {
    122    info("Waiting for scroll to first");
    123    const first = (await scrolled).accessible.parent;
    124    info("Checking ranges");
    125    await waitForTextAttrRanges(
    126      first,
    127      [
    128        [4, 16], // "first phrase"
    129      ],
    130      fragmentAttrs,
    131      false
    132    );
    133    const second = first.nextSibling;
    134    await waitForTextAttrRanges(second, [], fragmentAttrs, false);
    135 
    136    info("Navigating to second");
    137    // The text fragment begins with the text "second", which is the second
    138    // child of the `second` Accessible.
    139    scrolled = waitForEvent(EVENT_SCROLLING_START, second.getChildAt(1));
    140    let rangeCheck = waitForTextAttrRanges(
    141      second,
    142      [
    143        [4, 11], // "second "
    144        [11, 17], // "phrase"
    145      ],
    146      fragmentAttrs,
    147      true
    148    );
    149    await invokeContentTask(browser, [], () => {
    150      content.location.hash = "#:~:text=second%20phrase";
    151    });
    152    await scrolled;
    153    info("Checking ranges");
    154    await rangeCheck;
    155    // XXX DOM should probably remove the highlight from "first phrase" since
    156    // we've navigated to "second phrase". For now, this test expects the
    157    // current DOM behaviour: "first" is still highlighted.
    158    await waitForTextAttrRanges(
    159      first,
    160      [
    161        [4, 16], // "first phrase"
    162      ],
    163      fragmentAttrs,
    164      false
    165    );
    166  });
    167 });
    168 
    169 /**
    170 * Test custom highlight mutations.
    171 */
    172 addAccessibleTask(
    173  snippet,
    174  async function testCustomHighlightMutations(browser, docAcc) {
    175    info("Checking initial highlight");
    176    const first = findAccessibleChildByID(docAcc, "first");
    177    ok(
    178      textAttrRangesMatch(
    179        first,
    180        [
    181          [4, 9], // "first"
    182        ],
    183        highlightAttrs
    184      ),
    185      "first attr ranges correct"
    186    );
    187    const second = findAccessibleChildByID(docAcc, "second");
    188    ok(
    189      textAttrRangesMatch(second, [], highlightAttrs),
    190      "second attr ranges correct"
    191    );
    192 
    193    info("Adding range2 to highlight1");
    194    let rangeCheck = waitForTextAttrRanges(
    195      first,
    196      [
    197        [0, 3], // "The "
    198        [4, 9], // "first"
    199      ],
    200      highlightAttrs,
    201      true
    202    );
    203    await invokeContentTask(browser, [], () => {
    204      content.firstText = content.document.getElementById("first").firstChild;
    205      // Highlight the word "The".
    206      content.range2 = new content.Range();
    207      content.range2.setStart(content.firstText, 0);
    208      content.range2.setEnd(content.firstText, 3);
    209      content.highlight1 = content.CSS.highlights.get("highlight1");
    210      content.highlight1.add(content.range2);
    211    });
    212    await rangeCheck;
    213 
    214    info("Adding highlight2");
    215    rangeCheck = waitForTextAttrRanges(
    216      first,
    217      [
    218        [0, 3], // "The "
    219        [4, 9], // "first"
    220        [10, 16], // "phrase"
    221      ],
    222      highlightAttrs,
    223      true
    224    );
    225    await invokeContentTask(browser, [], () => {
    226      // Highlight the word "phrase".
    227      const range3 = new content.Range();
    228      range3.setStart(content.firstText, 10);
    229      range3.setEnd(content.firstText, 16);
    230      const highlight2 = new content.Highlight(range3);
    231      content.CSS.highlights.set("highlight2", highlight2);
    232    });
    233    await rangeCheck;
    234 
    235    info("Removing range2");
    236    rangeCheck = waitForTextAttrRanges(
    237      first,
    238      [
    239        [4, 9], // "first"
    240        [10, 16], // "phrase"
    241      ],
    242      highlightAttrs,
    243      true
    244    );
    245    await invokeContentTask(browser, [], () => {
    246      content.highlight1.delete(content.range2);
    247    });
    248    await rangeCheck;
    249 
    250    info("Removing highlight1");
    251    rangeCheck = waitForTextAttrRanges(
    252      first,
    253      [
    254        [10, 16], // "phrase"
    255      ],
    256      highlightAttrs,
    257      true
    258    );
    259    await invokeContentTask(browser, [], () => {
    260      content.CSS.highlights.delete("highlight1");
    261    });
    262    await rangeCheck;
    263  },
    264  {
    265    chrome: true,
    266    topLevel: true,
    267    contentSetup: async function contentSetup() {
    268      const firstText = content.document.getElementById("first").firstChild;
    269      // Highlight the word "first".
    270      const range1 = new content.Range();
    271      range1.setStart(firstText, 4);
    272      range1.setEnd(firstText, 9);
    273      const highlight1 = new content.Highlight(range1);
    274      content.CSS.highlights.set("highlight1", highlight1);
    275    },
    276  }
    277 );
    278 
    279 /**
    280 * Test custom highlight types.
    281 */
    282 addAccessibleTask(
    283  snippet,
    284  async function testCustomHighlightTypes(browser, docAcc) {
    285    const first = findAccessibleChildByID(docAcc, "first");
    286    ok(
    287      textAttrRangesMatch(
    288        first,
    289        [
    290          [0, 3], // "the"
    291        ],
    292        highlightAttrs
    293      ),
    294      "first highlight ranges correct"
    295    );
    296    ok(
    297      textAttrRangesMatch(
    298        first,
    299        [
    300          [4, 9], // "first"
    301        ],
    302        spellingAttrs
    303      ),
    304      "first spelling ranges correct"
    305    );
    306    ok(
    307      textAttrRangesMatch(
    308        first,
    309        [
    310          [10, 16], // "phrase"
    311        ],
    312        grammarAttrs
    313      ),
    314      "first grammar ranges correct"
    315    );
    316    const second = findAccessibleChildByID(docAcc, "second");
    317    ok(
    318      textAttrRangesMatch(second, [], highlightAttrs),
    319      "second highlight ranges correct"
    320    );
    321  },
    322  {
    323    chrome: true,
    324    topLevel: true,
    325    contentSetup: async function contentSetup() {
    326      const firstText = content.document.getElementById("first").firstChild;
    327      // Highlight the word "The".
    328      const range1 = new content.Range();
    329      range1.setStart(firstText, 0);
    330      range1.setEnd(firstText, 3);
    331      const highlight = new content.Highlight(range1);
    332      content.CSS.highlights.set("highlight", highlight);
    333 
    334      // Make the word "first" a spelling error.
    335      const range2 = new content.Range();
    336      range2.setStart(firstText, 4);
    337      range2.setEnd(firstText, 9);
    338      const spelling = new content.Highlight(range2);
    339      spelling.type = "spelling-error";
    340      content.CSS.highlights.set("spelling", spelling);
    341 
    342      // Make the word "phrase" a grammar error.
    343      const range3 = new content.Range();
    344      range3.setStart(firstText, 10);
    345      range3.setEnd(firstText, 16);
    346      const grammar = new content.Highlight(range3);
    347      grammar.type = "grammar-error";
    348      content.CSS.highlights.set("grammar", grammar);
    349    },
    350  }
    351 );
    352 
    353 /**
    354 * Test overlapping custom highlights.
    355 */
    356 addAccessibleTask(
    357  snippet,
    358  async function testCustomHighlightOverlapping(browser, docAcc) {
    359    const first = findAccessibleChildByID(docAcc, "first");
    360    ok(
    361      textAttrRangesMatch(
    362        first,
    363        [
    364          [0, 3], // "the"
    365          [4, 6], // "fi"
    366          [6, 7], // "r"
    367          [7, 9], // "st"
    368          [10, 12], // "ph"
    369          [12, 15], // "ras"
    370          [15, 16], // "e"
    371        ],
    372        highlightAttrs
    373      ),
    374      "first highlight ranges correct"
    375    );
    376    ok(
    377      textAttrRangesMatch(
    378        first,
    379        [
    380          [0, 3], // "the"
    381          [4, 6], // "fi"
    382          [6, 7], // "r"
    383          [7, 9], // "st"
    384          [12, 15], // "ras"
    385        ],
    386        spellingAttrs
    387      ),
    388      "first spelling ranges correct"
    389    );
    390    const second = findAccessibleChildByID(docAcc, "second");
    391    ok(
    392      textAttrRangesMatch(
    393        second,
    394        [
    395          [4, 7], // "sec"
    396          [7, 8], // "o"
    397          [8, 10], // "nd"
    398          [11, 13], // "ph"
    399          [13, 16], // "ras"
    400          [16, 17], // "e"
    401        ],
    402        highlightAttrs
    403      ),
    404      "second highlight ranges correct"
    405    );
    406    ok(
    407      textAttrRangesMatch(
    408        second,
    409        [
    410          [4, 7], // "sec"
    411          [8, 10], // "nd"
    412        ],
    413        spellingAttrs
    414      ),
    415      "second spelling ranges correct"
    416    );
    417  },
    418  {
    419    chrome: true,
    420    topLevel: true,
    421    contentSetup: async function contentSetup() {
    422      const firstText = content.document.getElementById("first").firstChild;
    423      // Make the word "The" both a highlight and a spelling error.
    424      const range1 = new content.Range();
    425      range1.setStart(firstText, 0);
    426      range1.setEnd(firstText, 3);
    427      const highlight1 = new content.Highlight(range1);
    428      content.CSS.highlights.set("highlight1", highlight1);
    429      const spelling = new content.Highlight(range1);
    430      spelling.type = "spelling-error";
    431      content.CSS.highlights.set("spelling", spelling);
    432 
    433      // Highlight the word "first".
    434      const range2 = new content.Range();
    435      range2.setStart(firstText, 4);
    436      range2.setEnd(firstText, 9);
    437      highlight1.add(range2);
    438      // Make "fir" a spelling error.
    439      const range3 = new content.Range();
    440      range3.setStart(firstText, 4);
    441      range3.setEnd(firstText, 7);
    442      spelling.add(range3);
    443      // Make "rst" a spelling error.
    444      const range4 = new content.Range();
    445      range4.setStart(firstText, 6);
    446      range4.setEnd(firstText, 9);
    447      spelling.add(range4);
    448 
    449      // Highlight the word "phrase".
    450      const range5 = new content.Range();
    451      range5.setStart(firstText, 10);
    452      range5.setEnd(firstText, 16);
    453      highlight1.add(range5);
    454      // Make "ras" a spelling error.
    455      const range6 = new content.Range();
    456      range6.setStart(firstText, 12);
    457      range6.setEnd(firstText, 15);
    458      spelling.add(range6);
    459 
    460      const secondText = content.document.querySelector("#second i").firstChild;
    461      // Highlight the word "second".
    462      const range7 = new content.Range();
    463      range7.setStart(secondText, 0);
    464      range7.setEnd(secondText, 6);
    465      highlight1.add(range7);
    466      // Make "sec" a spelling error.
    467      const range8 = new content.Range();
    468      range8.setStart(secondText, 0);
    469      range8.setEnd(secondText, 3);
    470      spelling.add(range8);
    471      // Make "nd" a spelling error.
    472      const range9 = new content.Range();
    473      range9.setStart(secondText, 4);
    474      range9.setEnd(secondText, 6);
    475      spelling.add(range9);
    476 
    477      const phrase2Text =
    478        content.document.querySelector("#second b").firstChild;
    479      // Highlight the word "phrase".
    480      const range10 = new content.Range();
    481      range10.setStart(phrase2Text, 0);
    482      range10.setEnd(phrase2Text, 6);
    483      highlight1.add(range10);
    484      // Highlight "ras" using a different Highlight.
    485      const range11 = new content.Range();
    486      range11.setStart(phrase2Text, 2);
    487      range11.setEnd(phrase2Text, 5);
    488      const highlight2 = new content.Highlight(range11);
    489      content.CSS.highlights.set("highlight2", highlight2);
    490    },
    491  }
    492 );