tor-browser

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

browser_caret_rect.js (18342B)


      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 function getCaretRect(docAcc, id) {
      8  const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
      9  const caretX = {};
     10  const caretY = {};
     11  const caretW = {};
     12  const caretH = {};
     13  acc.getCaretRect(caretX, caretY, caretW, caretH);
     14 
     15  const caretBounds = [caretX.value, caretY.value, caretW.value, caretH.value];
     16 
     17  info(`Caret bounds: ${caretBounds}`);
     18 
     19  return caretBounds;
     20 }
     21 
     22 function getCollapsedRangeExtents(acc, offset) {
     23  const rangeX = {};
     24  const rangeY = {};
     25  const rangeW = {};
     26  const rangeH = {};
     27  acc.getRangeExtents(
     28    offset,
     29    offset,
     30    rangeX,
     31    rangeY,
     32    rangeW,
     33    rangeH,
     34    COORDTYPE_SCREEN_RELATIVE
     35  );
     36 
     37  const rangeBounds = [rangeX.value, rangeY.value, rangeW.value, rangeH.value];
     38 
     39  info(`Range ${offset}-${offset} bounds: ${rangeBounds}`);
     40 
     41  return rangeBounds;
     42 }
     43 
     44 async function fetchCollapsedRangeBounds(docAcc, acc) {
     45  const state = {};
     46  acc.getState(state, {});
     47  if (state.value & nsIAccessibleStates.STATE_FOCUSABLE) {
     48    // This pre-scrolls the accessible into view as a focus would do.
     49    let focused = waitForEvent(EVENT_FOCUS, acc);
     50    acc.takeFocus();
     51    await focused;
     52 
     53    // We need to blur the accessible so the caret does not interfere with
     54    // the bounds fetch.
     55    focused = waitForEvent(EVENT_FOCUS, docAcc);
     56    docAcc.takeFocus();
     57    await focused;
     58  }
     59 
     60  acc.QueryInterface(nsIAccessibleHyperText);
     61 
     62  // If this is a 0 length text accessible, we need to ensure that
     63  // characterCount is at least 1.
     64  const characterCount = acc.characterCount ? acc.characterCount : 1;
     65  const bounds = [];
     66  for (let offset = 0; offset < characterCount; offset++) {
     67    let linkIndex = acc.getLinkIndexAtOffset(offset);
     68    if (linkIndex != -1) {
     69      const link = acc
     70        .getLinkAt(linkIndex)
     71        .QueryInterface(nsIAccessibleText)
     72        .QueryInterface(nsIAccessible);
     73      bounds.push(...(await fetchCollapsedRangeBounds(docAcc, link)));
     74    } else {
     75      let rect = getCollapsedRangeExtents(acc, offset);
     76      bounds.push(rect);
     77    }
     78  }
     79 
     80  return bounds;
     81 }
     82 
     83 function testCaretRect(
     84  docAcc,
     85  id,
     86  offset,
     87  fetchedBounds,
     88  atLineEnd = false,
     89  isVertical = false
     90 ) {
     91  const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
     92  is(acc.caretOffset, offset, `Caret at offset ${offset}`);
     93  const atEnd = offset == acc.characterCount;
     94  const empty = offset == 0 && atEnd;
     95  let queryOffset = atEnd && !empty ? offset - 1 : offset;
     96  const atEndInNewLine = atEnd && acc.getCharacterAtOffset(queryOffset) == "\n";
     97 
     98  const [rangeX, rangeY, , rangeH] =
     99    fetchedBounds.length > queryOffset
    100      ? fetchedBounds[queryOffset]
    101      : [0, 0, 0, 0];
    102 
    103  const [caretX, caretY, caretW, caretH] = getCaretRect(docAcc, id);
    104 
    105  if (!empty) {
    106    // In case of an empty input `getRangeExtents()` will return the full accessible bounds.
    107    let [currRangeX, currRangeY, currRangeW] = getCollapsedRangeExtents(
    108      acc,
    109      acc.caretOffset
    110    );
    111    Assert.deepEqual(
    112      [caretX, caretY, caretW],
    113      [currRangeX, currRangeY, currRangeW],
    114      "Collapsed range extents at caret position should be identical caret rect"
    115    );
    116  }
    117 
    118  if (atEndInNewLine) {
    119    Assert.lessOrEqual(caretX, rangeX, "Caret x before range x");
    120  } else if (atEnd || atLineEnd) {
    121    Assert.greater(caretX, rangeX, "Caret x after last range x");
    122  } else {
    123    // Caret width changes depending on device pixel ratio. In RTL
    124    // text that would change the x where the caret is drawn by a pixel or two.
    125    isWithin(caretX, rangeX, 3, "Caret x similar to range x");
    126  }
    127 
    128  if (isVertical && atEnd) {
    129    Assert.greaterOrEqual(caretY, rangeY, "Caret y below range y");
    130  } else if (atEndInNewLine) {
    131    Assert.greater(caretY, rangeY, "Caret y below range y");
    132  } else if (atLineEnd) {
    133    Assert.less(caretY, rangeY, "Caret y above start line range.");
    134  } else {
    135    isWithin(caretY, rangeY, 3, "Caret y similar to range y");
    136  }
    137 
    138  ok(caretW, "Caret width is greater than 0");
    139 
    140  if (!empty) {
    141    // Depending on glyph, the range can be taller.
    142    isWithin(caretH, rangeH, 2, "Caret height similar to range height");
    143  }
    144 }
    145 
    146 function getAccBounds(acc) {
    147  const x = {};
    148  const y = {};
    149  const w = {};
    150  const h = {};
    151  acc.getBounds(x, y, w, h);
    152  return [x.value, y.value, w.value, h.value];
    153 }
    154 
    155 /**
    156 * Test the caret rect in content documents.
    157 */
    158 addAccessibleTask(
    159  `
    160 <input id="input" value="ab">
    161 <input id="emptyInput">
    162  `,
    163  async function (browser, docAcc) {
    164    async function runTests() {
    165      const input = findAccessibleChildByID(docAcc, "input", [
    166        nsIAccessibleText,
    167      ]);
    168 
    169      let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
    170 
    171      info("Focusing input");
    172      let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    173      input.takeFocus();
    174      await caretMoved;
    175      testCaretRect(docAcc, "input", 0, fetchedBounds);
    176      info("Setting caretOffset to 1");
    177      caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    178      input.caretOffset = 1;
    179      await caretMoved;
    180      testCaretRect(docAcc, "input", 1, fetchedBounds);
    181      info("Setting caretOffset to 2");
    182      caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    183      input.caretOffset = 2;
    184      await caretMoved;
    185      testCaretRect(docAcc, "input", 2, fetchedBounds);
    186      info("Resetting caretOffset to 0");
    187      input.caretOffset = 0;
    188 
    189      const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
    190        nsIAccessibleText,
    191      ]);
    192 
    193      fetchedBounds = await fetchCollapsedRangeBounds(docAcc, emptyInput);
    194 
    195      info("Focusing emptyInput");
    196      caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
    197      emptyInput.takeFocus();
    198      await caretMoved;
    199      testCaretRect(docAcc, "emptyInput", 0, fetchedBounds);
    200    }
    201 
    202    await runTests();
    203 
    204    // Check that the caret rect is correct when the title bar is shown.
    205    if (LINUX || Services.env.get("MOZ_HEADLESS")) {
    206      // Disabling tabs in title bar doesn't change the bounds on Linux or in
    207      // headless mode.
    208      info("Skipping title bar tests");
    209      return;
    210    }
    211    const [, origDocY] = getAccBounds(docAcc);
    212    info("Showing title bar");
    213    let titleBarChanged = BrowserTestUtils.waitForMutationCondition(
    214      document.documentElement,
    215      { attributes: true, attributeFilter: ["customtitlebar"] },
    216      () => !document.documentElement.hasAttribute("customtitlebar")
    217    );
    218    await SpecialPowers.pushPrefEnv({
    219      set: [["browser.tabs.inTitlebar", false]],
    220    });
    221    await titleBarChanged;
    222    const [, newDocY] = getAccBounds(docAcc);
    223    Assert.greater(
    224      newDocY,
    225      origDocY,
    226      "Doc has larger y after title bar change"
    227    );
    228    await runTests();
    229    await SpecialPowers.popPrefEnv();
    230  },
    231  { chrome: true, topLevel: true }
    232 );
    233 
    234 /**
    235 * Test the caret rect in multiline content.
    236 */
    237 addAccessibleTask(
    238  `<style>
    239    @font-face {
    240      font-family: Ahem;
    241      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    242    }
    243   textarea {
    244      font: 10px/10px Ahem;
    245      width: 30px;
    246      height: 80px;
    247    }
    248  </style>
    249  <textarea id="textarea">1234
    250 56789
    251 </textarea>
    252  `,
    253  async function testMultiline(browser, docAcc) {
    254    async function moveCaret(key, keyopts = {}) {
    255      let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textarea");
    256      if (key) {
    257        EventUtils.synthesizeKey(key, keyopts);
    258      } else {
    259        // If no key is provided, just focus the textarea.
    260        findAccessibleChildByID(docAcc, "textarea").takeFocus();
    261      }
    262 
    263      let evt = await caretMoved;
    264      evt.QueryInterface(nsIAccessibleCaretMoveEvent);
    265      return [evt.caretOffset, evt.isAtEndOfLine];
    266    }
    267 
    268    const textarea = findAccessibleChildByID(docAcc, "textarea", [
    269      nsIAccessibleText,
    270    ]);
    271    let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, textarea);
    272 
    273    info("Focusing textarea");
    274    let [offset, isAtLineEnd] = await moveCaret();
    275    is(offset, 0, "Caret at offset 0");
    276    is(isAtLineEnd, false, "Caret not at end of line");
    277    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    278 
    279    info("Moving caret right");
    280    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    281    is(offset, 1, "Caret at offset 1");
    282    is(isAtLineEnd, false, "Caret not at end of line");
    283    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    284 
    285    info("Moving caret right again");
    286    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    287    is(offset, 2, "Caret at offset 2");
    288    is(isAtLineEnd, false, "Caret not at end of line");
    289    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    290 
    291    info("Moving caret right again again");
    292    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    293    is(offset, 3, "Caret at offset 3");
    294    is(isAtLineEnd, true, "Caret at end of line");
    295    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    296 
    297    info("Moving caret right stays at same offset, but on new line");
    298    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    299    is(offset, 3, "Caret at offset 3");
    300    is(isAtLineEnd, false, "Caret not at end of line");
    301    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    302 
    303    info("Moving caret right in second line");
    304    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    305    is(offset, 4, "Caret at offset 4");
    306    is(isAtLineEnd, false, "Caret not at end of line");
    307    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    308 
    309    info("Move caret right to new line");
    310    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
    311    is(offset, 5, "Caret at offset 5");
    312    is(isAtLineEnd, false, "Caret not at end of line");
    313    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    314 
    315    info("Move caret to end of previous line");
    316    [offset, isAtLineEnd] = await moveCaret("KEY_ArrowLeft");
    317    is(offset, 4, "Caret at offset 4");
    318    is(isAtLineEnd, false, "Caret at end line break");
    319    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    320 
    321    info("Move caret to end of text");
    322    if (AppConstants.platform == "macosx") {
    323      [offset, isAtLineEnd] = await moveCaret("KEY_PageDown", {
    324        altKey: true,
    325      });
    326    } else {
    327      [offset, isAtLineEnd] = await moveCaret("KEY_End", {
    328        ctrlKey: true,
    329      });
    330    }
    331    is(offset, 11, "Caret at offset 11");
    332    is(isAtLineEnd, false, "Caret at end line break");
    333    testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
    334  }
    335 );
    336 
    337 function todoIsWithin(expected, got, within, msg) {
    338  if (Math.abs(got - expected) <= within) {
    339    todo(true, `${msg} - Got ${got}`);
    340  } else {
    341    todo(
    342      false,
    343      `${msg} - Got ${got}, expected ${expected} with error of ${within}`
    344    );
    345  }
    346 }
    347 
    348 /**
    349 * Test the caret rect and collapsed range in bidi text
    350 */
    351 addAccessibleTask(
    352  `
    353 <input id="input1" value="hello שלום world">
    354 <input id="input2" dir="rtl" value="שלום hello עולם">
    355 <textarea id="textarea" dir="rtl">שלום
    356 עולם</textarea>
    357  `,
    358  async function testRTL(browser, docAcc) {
    359    const input1 = findAccessibleChildByID(docAcc, "input1", [
    360      nsIAccessibleText,
    361    ]);
    362 
    363    let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input1);
    364 
    365    info("Focusing input1");
    366    let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
    367    input1.takeFocus();
    368    await caretMoved;
    369    testCaretRect(docAcc, "input1", 0, fetchedBounds);
    370 
    371    info("Setting caretOffset to 1");
    372    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
    373    input1.caretOffset = 1;
    374    await caretMoved;
    375    testCaretRect(docAcc, "input1", 1, fetchedBounds);
    376 
    377    info("Setting caretOffset to 6 (first in embedded RTL)");
    378    // Retrieving rangeX before caret goes there
    379    let [rangeX] = getCollapsedRangeExtents(input1, 6);
    380    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
    381    input1.caretOffset = 6;
    382    await caretMoved;
    383    let [caretX] = getCaretRect(docAcc, "input1");
    384    todoIsWithin(caretX, rangeX, 2, "Caret x similar to range x");
    385 
    386    info("Setting caretOffset to 7 (in embedded RTL)");
    387    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
    388    input1.caretOffset = 7;
    389    await caretMoved;
    390    testCaretRect(docAcc, "input1", 7, fetchedBounds);
    391 
    392    info("Resetting caretOffset to 0");
    393    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
    394    input1.caretOffset = 0;
    395    await caretMoved;
    396 
    397    const input2 = findAccessibleChildByID(docAcc, "input2", [
    398      nsIAccessibleText,
    399    ]);
    400 
    401    fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input2);
    402 
    403    info("Focusing input2");
    404    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
    405    input2.takeFocus();
    406    await caretMoved;
    407    testCaretRect(docAcc, "input2", 0, fetchedBounds);
    408 
    409    info("Setting caretOffset to 1");
    410    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
    411    input2.caretOffset = 1;
    412    await caretMoved;
    413    testCaretRect(docAcc, "input2", 1, fetchedBounds);
    414 
    415    info("Setting caretOffset to 5 (first in embedded LTR)");
    416    // Retrieving rangeX before caret goes there
    417    [rangeX] = getCollapsedRangeExtents(input2, 5);
    418    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
    419    input2.caretOffset = 5;
    420    await caretMoved;
    421    [caretX] = getCaretRect(docAcc, "input1");
    422    todoIsWithin(caretX, rangeX, 2, "Caret x similar to range x");
    423 
    424    info("Setting caretOffset to 7 (in embedded LTR)");
    425    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
    426    input2.caretOffset = 7;
    427    await caretMoved;
    428    testCaretRect(docAcc, "input2", 7, fetchedBounds);
    429 
    430    const textarea = findAccessibleChildByID(docAcc, "textarea", [
    431      nsIAccessibleText,
    432    ]);
    433 
    434    fetchedBounds = await fetchCollapsedRangeBounds(docAcc, textarea);
    435 
    436    info("Focusing textarea");
    437    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
    438    textarea.takeFocus();
    439    await caretMoved;
    440    testCaretRect(docAcc, "textarea", 0, fetchedBounds);
    441 
    442    info("Setting caretOffset to 1");
    443    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
    444    textarea.caretOffset = 1;
    445    await caretMoved;
    446    testCaretRect(docAcc, "textarea", 1, fetchedBounds);
    447 
    448    info("Setting caretOffset to 4 (before newline)");
    449    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
    450    textarea.caretOffset = 4;
    451    await caretMoved;
    452    testCaretRect(docAcc, "textarea", 4, fetchedBounds);
    453 
    454    info("Setting caretOffset to 5 (after newline)");
    455    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
    456    textarea.caretOffset = 5;
    457    await caretMoved;
    458    testCaretRect(docAcc, "textarea", 5, fetchedBounds);
    459  },
    460  { chrome: true, topLevel: true }
    461 );
    462 
    463 /**
    464 * Test the caret rect in vertical text.
    465 */
    466 addAccessibleTask(
    467  `
    468 <input id="input" value="ab" style="writing-mode: vertical-lr;">
    469 <input id="emptyInput" style="writing-mode: vertical-lr;">
    470  `,
    471  async function testVerticalInputs(browser, docAcc) {
    472    const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
    473    let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
    474 
    475    info("Focusing input");
    476    let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    477    input.takeFocus();
    478    await caretMoved;
    479    testCaretRect(docAcc, "input", 0, fetchedBounds, false, true);
    480    info("Setting caretOffset to 1");
    481    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    482    input.caretOffset = 1;
    483    await caretMoved;
    484    testCaretRect(docAcc, "input", 1, fetchedBounds, false, true);
    485    info("Setting caretOffset to 2");
    486    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    487    input.caretOffset = 2;
    488    await caretMoved;
    489    testCaretRect(docAcc, "input", 2, fetchedBounds, false, true);
    490    info("Resetting caretOffset to 0");
    491    input.caretOffset = 0;
    492 
    493    const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
    494      nsIAccessibleText,
    495    ]);
    496    fetchedBounds = await fetchCollapsedRangeBounds(docAcc, emptyInput);
    497 
    498    info("Focusing emptyInput");
    499    caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
    500    emptyInput.takeFocus();
    501    await caretMoved;
    502    testCaretRect(docAcc, "emptyInput", 0, fetchedBounds, false, true);
    503  }
    504 );
    505 
    506 /**
    507 * Test contenteditable caret
    508 */
    509 addAccessibleTask(
    510  `
    511 <div contenteditable="true" id="input" role="textbox">one <strong>two</strong> <i>three</i></div>
    512  `,
    513  async function testRichTextEdit(browser, docAcc) {
    514    const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
    515    let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
    516 
    517    let focused = waitForEvent(EVENT_FOCUS, input);
    518    input.takeFocus();
    519    info("Focusing input");
    520    await focused;
    521    testCaretRect(docAcc, "input", 0, fetchedBounds);
    522 
    523    for (let offset = 1; offset < fetchedBounds.length - 1; offset++) {
    524      info("Pressing ArrowRight to move caret to index " + offset);
    525      let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED);
    526      EventUtils.synthesizeKey("KEY_ArrowRight");
    527 
    528      let evt = (await caretMoved).QueryInterface(nsIAccessibleCaretMoveEvent);
    529 
    530      let acc = evt.accessible.QueryInterface(nsIAccessibleHyperText);
    531      let caretOffset = evt.caretOffset;
    532      let linkIndex = acc.getLinkIndexAtOffset(evt.caretOffset);
    533      if (linkIndex != -1) {
    534        acc = acc
    535          .getLinkAt(linkIndex)
    536          .QueryInterface(nsIAccessibleText)
    537          .QueryInterface(nsIAccessible);
    538        caretOffset = 0;
    539      }
    540 
    541      const [rangeX, rangeY, rangeW, rangeH] =
    542        fetchedBounds.length > offset ? fetchedBounds[offset] : [0, 0, 0, 0];
    543      const [caretX, caretY, caretW, caretH] = getCollapsedRangeExtents(
    544        acc,
    545        caretOffset
    546      );
    547 
    548      info(
    549        `${offset}: Caret rect: ${caretX}, ${caretY}, ${caretW}, ${caretH}, Prefetched rect: ${rangeX}, ${rangeY}, ${rangeW}, ${rangeH}`
    550      );
    551 
    552      isWithin(rangeX, caretX, 2, "Caret x similar to range x");
    553      isWithin(rangeY, caretY, 2, "Caret y similar to range y");
    554      isWithin(rangeW, caretW, 2, "Caret width similar to range width");
    555    }
    556  }
    557 );