tor-browser

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

browser_text_selection.js (12442B)


      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 
      9 function waitForSelectionChange(selectionAcc, caretAcc) {
     10  if (!caretAcc) {
     11    caretAcc = selectionAcc;
     12  }
     13  return waitForEvents(
     14    [
     15      [EVENT_TEXT_SELECTION_CHANGED, selectionAcc],
     16      // We must swallow the caret events as well to avoid confusion with later,
     17      // unrelated caret events.
     18      [EVENT_TEXT_CARET_MOVED, caretAcc],
     19    ],
     20    true
     21  );
     22 }
     23 
     24 function changeDomSelection(
     25  browser,
     26  anchorId,
     27  anchorOffset,
     28  focusId,
     29  focusOffset
     30 ) {
     31  return invokeContentTask(
     32    browser,
     33    [anchorId, anchorOffset, focusId, focusOffset],
     34    (
     35      contentAnchorId,
     36      contentAnchorOffset,
     37      contentFocusId,
     38      contentFocusOffset
     39    ) => {
     40      // We want the text node, so we use firstChild.
     41      content.window
     42        .getSelection()
     43        .setBaseAndExtent(
     44          content.document.getElementById(contentAnchorId).firstChild,
     45          contentAnchorOffset,
     46          content.document.getElementById(contentFocusId).firstChild,
     47          contentFocusOffset
     48        );
     49    }
     50  );
     51 }
     52 
     53 function testSelectionRange(
     54  browser,
     55  root,
     56  startContainer,
     57  startOffset,
     58  endContainer,
     59  endOffset
     60 ) {
     61  let selRange = root.selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
     62  testTextRange(
     63    selRange,
     64    getAccessibleDOMNodeID(root),
     65    startContainer,
     66    startOffset,
     67    endContainer,
     68    endOffset
     69  );
     70 }
     71 
     72 /**
     73 * Test text selection via keyboard.
     74 */
     75 addAccessibleTask(
     76  `
     77 <textarea id="textarea">ab</textarea>
     78 <div id="editable" contenteditable>
     79  <p id="p1">a</p>
     80  <p id="p2">bc</p>
     81  <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p>
     82 </div>
     83  `,
     84  async function (browser, docAcc) {
     85    queryInterfaces(docAcc, [nsIAccessibleText]);
     86 
     87    const textarea = findAccessibleChildByID(docAcc, "textarea", [
     88      nsIAccessibleText,
     89    ]);
     90    info("Focusing textarea");
     91    let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
     92    textarea.takeFocus();
     93    await caretMoved;
     94    testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
     95    is(textarea.selectionCount, 0, "textarea selectionCount is 0");
     96    is(docAcc.selectionCount, 0, "document selectionCount is 0");
     97 
     98    info("Selecting a in textarea");
     99    let selChanged = waitForSelectionChange(textarea);
    100    EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
    101    await selChanged;
    102    testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
    103    testTextGetSelection(textarea, 0, 1, 0);
    104 
    105    info("Selecting b in textarea");
    106    selChanged = waitForSelectionChange(textarea);
    107    EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
    108    await selChanged;
    109    testSelectionRange(browser, textarea, textarea, 0, textarea, 2);
    110    testTextGetSelection(textarea, 0, 2, 0);
    111 
    112    info("Unselecting b in textarea");
    113    selChanged = waitForSelectionChange(textarea);
    114    EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
    115    await selChanged;
    116    testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
    117    testTextGetSelection(textarea, 0, 1, 0);
    118 
    119    info("Unselecting a in textarea");
    120    // We don't fire selection changed when the selection collapses.
    121    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
    122    EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
    123    await caretMoved;
    124    testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
    125    is(textarea.selectionCount, 0, "textarea selectionCount is 0");
    126 
    127    const editable = findAccessibleChildByID(docAcc, "editable", [
    128      nsIAccessibleText,
    129    ]);
    130    const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]);
    131    info("Focusing editable, caret to start");
    132    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1);
    133    await changeDomSelection(browser, "p1", 0, "p1", 0);
    134    await caretMoved;
    135    testSelectionRange(browser, editable, p1, 0, p1, 0);
    136    is(editable.selectionCount, 0, "editable selectionCount is 0");
    137    is(p1.selectionCount, 0, "p1 selectionCount is 0");
    138    is(docAcc.selectionCount, 0, "document selectionCount is 0");
    139 
    140    info("Selecting a in editable");
    141    selChanged = waitForSelectionChange(p1);
    142    await changeDomSelection(browser, "p1", 0, "p1", 1);
    143    await selChanged;
    144    testSelectionRange(browser, editable, p1, 0, p1, 1);
    145    testTextGetSelection(editable, 0, 1, 0);
    146    testTextGetSelection(p1, 0, 1, 0);
    147    const p2 = findAccessibleChildByID(docAcc, "p2", [nsIAccessibleText]);
    148    if (browser.isRemoteBrowser) {
    149      is(p2.selectionCount, 0, "p2 selectionCount is 0");
    150    } else {
    151      todo(
    152        false,
    153        "Siblings report wrong selection in non-cache implementation"
    154      );
    155    }
    156 
    157    // Selecting across two Accessibles with only a partial selection in the
    158    // second.
    159    info("Selecting ab in editable");
    160    selChanged = waitForSelectionChange(editable, p2);
    161    await changeDomSelection(browser, "p1", 0, "p2", 1);
    162    await selChanged;
    163    testSelectionRange(browser, editable, p1, 0, p2, 1);
    164    testTextGetSelection(editable, 0, 2, 0);
    165    testTextGetSelection(p1, 0, 1, 0);
    166    testTextGetSelection(p2, 0, 1, 0);
    167 
    168    const pWithLink = findAccessibleChildByID(docAcc, "pWithLink", [
    169      nsIAccessibleText,
    170    ]);
    171    const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
    172    // Selecting both text and a link.
    173    info("Selecting de in editable");
    174    selChanged = waitForSelectionChange(pWithLink, link);
    175    await changeDomSelection(browser, "pWithLink", 0, "link", 1);
    176    await selChanged;
    177    testSelectionRange(browser, editable, pWithLink, 0, link, 1);
    178    testTextGetSelection(editable, 2, 3, 0);
    179    testTextGetSelection(pWithLink, 0, 2, 0);
    180    testTextGetSelection(link, 0, 1, 0);
    181 
    182    // Selecting a link and text on either side.
    183    info("Selecting def in editable");
    184    selChanged = waitForSelectionChange(pWithLink, pWithLink);
    185    await changeDomSelection(browser, "pWithLink", 0, "textAfterLink", 1);
    186    await selChanged;
    187    testSelectionRange(browser, editable, pWithLink, 0, pWithLink, 3);
    188    testTextGetSelection(editable, 2, 3, 0);
    189    testTextGetSelection(pWithLink, 0, 3, 0);
    190    testTextGetSelection(link, 0, 1, 0);
    191 
    192    // Noncontiguous selection.
    193    info("Selecting a in editable");
    194    selChanged = waitForSelectionChange(p1);
    195    await changeDomSelection(browser, "p1", 0, "p1", 1);
    196    await selChanged;
    197    info("Adding c to selection in editable");
    198    selChanged = waitForSelectionChange(p2);
    199    await invokeContentTask(browser, [], () => {
    200      const r = content.document.createRange();
    201      const p2text = content.document.getElementById("p2").firstChild;
    202      r.setStart(p2text, 0);
    203      r.setEnd(p2text, 1);
    204      content.window.getSelection().addRange(r);
    205    });
    206    await selChanged;
    207    let selRanges = editable.selectionRanges;
    208    is(selRanges.length, 2, "2 selection ranges");
    209    testTextRange(
    210      selRanges.queryElementAt(0, nsIAccessibleTextRange),
    211      "range 0",
    212      p1,
    213      0,
    214      p1,
    215      1
    216    );
    217    testTextRange(
    218      selRanges.queryElementAt(1, nsIAccessibleTextRange),
    219      "range 1",
    220      p2,
    221      0,
    222      p2,
    223      1
    224    );
    225    is(editable.selectionCount, 2, "editable selectionCount is 2");
    226    testTextGetSelection(editable, 0, 1, 0);
    227    testTextGetSelection(editable, 1, 2, 1);
    228    if (browser.isRemoteBrowser) {
    229      is(p1.selectionCount, 1, "p1 selectionCount is 1");
    230      testTextGetSelection(p1, 0, 1, 0);
    231      is(p2.selectionCount, 1, "p2 selectionCount is 1");
    232      testTextGetSelection(p2, 0, 1, 0);
    233    } else {
    234      todo(
    235        false,
    236        "Siblings report wrong selection in non-cache implementation"
    237      );
    238    }
    239  },
    240  {
    241    chrome: true,
    242    topLevel: true,
    243    iframe: true,
    244    remoteIframe: true,
    245  }
    246 );
    247 
    248 /**
    249 * Tabbing to an input selects all its text. Test that the cached selection
    250 *reflects this. This has to be done separately from the other selection tests
    251 * because prior contentEditable selection changes the events that get fired.
    252 */
    253 addAccessibleTask(
    254  `
    255 <button id="before">Before</button>
    256 <input id="input" value="test">
    257  `,
    258  async function (browser, docAcc) {
    259    // The tab order is different when there's an iframe, so focus a control
    260    // before the input to make tab consistent.
    261    info("Focusing before");
    262    const before = findAccessibleChildByID(docAcc, "before");
    263    // Focusing a button fires a selection event. We must swallow this to
    264    // avoid confusing the later test.
    265    let events = waitForOrderedEvents([
    266      [EVENT_FOCUS, before],
    267      [EVENT_TEXT_SELECTION_CHANGED, docAcc],
    268    ]);
    269    before.takeFocus();
    270    await events;
    271 
    272    const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
    273    info("Tabbing to input");
    274    events = waitForEvents(
    275      {
    276        expected: [
    277          [EVENT_FOCUS, input],
    278          [EVENT_TEXT_SELECTION_CHANGED, input],
    279        ],
    280        unexpected: [[EVENT_TEXT_SELECTION_CHANGED, docAcc]],
    281      },
    282      "input",
    283      false,
    284      (args, task) => invokeContentTask(browser, args, task)
    285    );
    286    EventUtils.synthesizeKey("KEY_Tab");
    287    await events;
    288    testSelectionRange(browser, input, input, 0, input, 4);
    289    testTextGetSelection(input, 0, 4, 0);
    290  },
    291  {
    292    chrome: true,
    293    topLevel: true,
    294    iframe: true,
    295    remoteIframe: true,
    296  }
    297 );
    298 
    299 /**
    300 * Test text selection via API.
    301 */
    302 addAccessibleTask(
    303  `
    304  <p id="paragraph">hello world</p>
    305  <ol>
    306    <li id="li">Number one</li>
    307  </ol>
    308  `,
    309  async function (browser, docAcc) {
    310    const paragraph = findAccessibleChildByID(docAcc, "paragraph", [
    311      nsIAccessibleText,
    312    ]);
    313 
    314    let selChanged = waitForSelectionChange(paragraph);
    315    paragraph.setSelectionBounds(0, 2, 4);
    316    await selChanged;
    317    testTextGetSelection(paragraph, 2, 4, 0);
    318 
    319    selChanged = waitForSelectionChange(paragraph);
    320    paragraph.addSelection(6, 10);
    321    await selChanged;
    322    testTextGetSelection(paragraph, 6, 10, 1);
    323    is(paragraph.selectionCount, 2, "paragraph selectionCount is 2");
    324 
    325    selChanged = waitForSelectionChange(paragraph);
    326    paragraph.removeSelection(0);
    327    await selChanged;
    328    testTextGetSelection(paragraph, 6, 10, 0);
    329    is(paragraph.selectionCount, 1, "paragraph selectionCount is 1");
    330 
    331    const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
    332 
    333    selChanged = waitForSelectionChange(li);
    334    li.setSelectionBounds(0, 1, 8);
    335    await selChanged;
    336    testTextGetSelection(li, 3, 8, 0);
    337  },
    338  {
    339    chrome: true,
    340    topLevel: true,
    341    iframe: true,
    342    remoteIframe: true,
    343  }
    344 );
    345 
    346 /**
    347 * Test selections which start or end in an empty container.
    348 */
    349 addAccessibleTask(
    350  `<div id="div" contenteditable>a<p id="p"></p>b</div>`,
    351  async function testEmptyContainer(browser, docAcc) {
    352    const div = findAccessibleChildByID(docAcc, "div", [nsIAccessibleText]);
    353    info('Selecting from the empty<p> to after the text "b"');
    354    let selected = waitForSelectionChange(div);
    355    await invokeContentTask(browser, [], () => {
    356      const divDom = content.document.getElementById("div");
    357      content
    358        .getSelection()
    359        .setBaseAndExtent(divDom.childNodes[1], 0, divDom.childNodes[2], 1);
    360    });
    361    await selected;
    362    const p = findAccessibleChildByID(docAcc, "p");
    363    testSelectionRange(browser, div, p, 0, div, 3);
    364    testTextGetSelection(div, 1, 3, 0);
    365    info('Selecting from the text "a" to after the empty<p>');
    366    selected = waitForSelectionChange(div, p);
    367    await invokeContentTask(browser, [], () => {
    368      const divDom = content.document.getElementById("div");
    369      content
    370        .getSelection()
    371        .setBaseAndExtent(divDom.childNodes[0], 0, divDom.childNodes[1], 0);
    372    });
    373    await selected;
    374    testSelectionRange(browser, div, div, 0, p, 0);
    375    // XXX Bug 1973166: This should perhaps be (0, 2), indicating that the
    376    // selection includes the empty paragraph. However, offset 1 isn't valid in
    377    // an empty container, which means we use offset 0 in the paragraph. Since
    378    // the end is exclusive, that causes us to exclude the paragraph when we
    379    // transform to the div.
    380    testTextGetSelection(div, 0, 1, 0);
    381  },
    382  { chrome: true, toplevel: true }
    383 );