tor-browser

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

browser_caching_text_bounds.js (25159B)


      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 requestLongerTimeout(3);
      7 
      8 /* import-globals-from ../../mochitest/layout.js */
      9 loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
     10 
     11 // Note that testTextNode, testChar and testTextRange currently don't handle
     12 // white space in the code that doesn't get rendered on screen. To work around
     13 // this, ensure that containers you want to test are all on a single line in the
     14 // test snippet.
     15 
     16 async function testTextNode(accDoc, browser, id) {
     17  await testTextRange(accDoc, browser, id, 0, -1);
     18 }
     19 
     20 async function testChar(accDoc, browser, id, idx) {
     21  await testTextRange(accDoc, browser, id, idx, idx + 1);
     22 }
     23 
     24 async function testTextRange(accDoc, browser, id, start, end) {
     25  const r = await invokeContentTask(
     26    browser,
     27    [id, start, end],
     28    (_id, _start, _end) => {
     29      const htNode = content.document.getElementById(_id);
     30      let [eX, eY, eW, eH] = [
     31        Number.MAX_SAFE_INTEGER,
     32        Number.MAX_SAFE_INTEGER,
     33        0,
     34        0,
     35      ];
     36      let traversed = 0;
     37      let localStart = _start;
     38      let endTraversal = false;
     39      for (let element of htNode.childNodes) {
     40        // ignore whitespace, but not embedded elements
     41        let isEmbeddedElement = false;
     42        if (element.length == undefined) {
     43          let potentialTextContainer = element;
     44          while (
     45            potentialTextContainer &&
     46            potentialTextContainer.length == undefined
     47          ) {
     48            potentialTextContainer = element.firstChild;
     49          }
     50          if (potentialTextContainer && potentialTextContainer.length) {
     51            // If we can reach some text from this container, use that as part
     52            // of our range. This is important when testing with intervening inline
     53            // elements. ie. <pre><code>ab%0acd
     54            element = potentialTextContainer;
     55          } else if (element.firstChild) {
     56            isEmbeddedElement = true;
     57          } else {
     58            continue;
     59          }
     60        }
     61        if (element.length + traversed < _start) {
     62          // If our start index is not within this
     63          // node, keep looking.
     64          traversed += element.length;
     65          localStart -= element.length;
     66          continue;
     67        }
     68 
     69        let rect;
     70        if (isEmbeddedElement) {
     71          rect = element.getBoundingClientRect();
     72        } else {
     73          const range = content.document.createRange();
     74          range.setStart(element, localStart);
     75 
     76          if (_end != -1 && _end - traversed <= element.length) {
     77            // If the current node contains
     78            // our end index, stop here.
     79            endTraversal = true;
     80            range.setEnd(element, _end - traversed);
     81          } else {
     82            range.setEnd(element, element.length);
     83          }
     84 
     85          rect = range.getBoundingClientRect();
     86        }
     87 
     88        const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX;
     89        const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY;
     90        eX = Math.min(eX, rect.x);
     91        eY = Math.min(eY, rect.y);
     92        eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX);
     93        eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY);
     94 
     95        if (endTraversal) {
     96          break;
     97        }
     98        localStart = 0;
     99        traversed += element.length;
    100      }
    101      return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)];
    102    }
    103  );
    104  let hyperTextNode = findAccessibleChildByID(accDoc, id);
    105 
    106  // Add in the doc's screen coords because getBoundingClientRect
    107  // is relative to the document, not the screen. This assumes the doc's
    108  // screen coords are correct. We use getBoundsInCSSPixels to avoid factoring
    109  // in the DPR ourselves.
    110  let x = {};
    111  let y = {};
    112  let w = {};
    113  let h = {};
    114  accDoc.getBoundsInCSSPixels(x, y, w, h);
    115  r[0] += x.value;
    116  r[1] += y.value;
    117  if (end != -1 && end - start == 1) {
    118    // If we're only testing a character, use this function because it calls
    119    // CharBounds() directly instead of TextBounds().
    120    testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE);
    121  } else {
    122    testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE);
    123  }
    124 }
    125 
    126 /**
    127 * Since testTextChar can't handle non-rendered white space, this function first
    128 * uses testTextChar to verify the first character and then ensures all
    129 * characters thereafter have an incrementing x and a non-0 width.
    130 */
    131 async function testLineWithNonRenderedSpace(docAcc, browser, id, length) {
    132  await testChar(docAcc, browser, id, 0);
    133  const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
    134  let prevX = -1;
    135  for (let offset = 0; offset < length; ++offset) {
    136    const x = {};
    137    const y = {};
    138    const w = {};
    139    const h = {};
    140    acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
    141    Assert.greater(
    142      x.value,
    143      prevX,
    144      `${id}: offset ${offset} x is larger (${x.value})`
    145    );
    146    prevX = x.value;
    147    Assert.greater(w.value, 0, `${id}: offset ${offset} width > 0`);
    148  }
    149 }
    150 
    151 add_setup(async function () {
    152  await SpecialPowers.pushPrefEnv({
    153    set: [["test.wait300msAfterTabSwitch", true]],
    154  });
    155 });
    156 
    157 /**
    158 * Test the text range boundary for simple LtR text
    159 */
    160 addAccessibleTask(
    161  `
    162  <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
    163  <p id='p2' style='font-family: monospace;'>ل</p>
    164  <p id='p3' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
    165  <pre id='p4' style='font-family: monospace;'>a%0abcdef</pre>
    166  `,
    167  async function (browser, accDoc) {
    168    info("Testing simple LtR text");
    169    await testTextNode(accDoc, browser, "p1");
    170    await testTextNode(accDoc, browser, "p2");
    171    await testTextNode(accDoc, browser, "p3");
    172    await testTextNode(accDoc, browser, "p4");
    173  },
    174  {
    175    iframe: true,
    176  }
    177 );
    178 
    179 /**
    180 * Test the partial text range boundary for LtR text
    181 */
    182 addAccessibleTask(
    183  `
    184  <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
    185  <p id='p2' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
    186  `,
    187  async function (browser, accDoc) {
    188    info("Testing partial ranges in LtR text");
    189    await testTextRange(accDoc, browser, "p1", 0, 4);
    190    await testTextRange(accDoc, browser, "p1", 2, 8);
    191    await testTextRange(accDoc, browser, "p1", 12, 17);
    192    await testTextRange(accDoc, browser, "p2", 0, 4);
    193    await testTextRange(accDoc, browser, "p2", 2, 8);
    194    await testTextRange(accDoc, browser, "p2", 6, 11);
    195  },
    196  {
    197    topLevel: true,
    198    iframe: true,
    199  }
    200 );
    201 
    202 /**
    203 * Test the text boundary for multiline LtR text
    204 */
    205 addAccessibleTask(
    206  `
    207  <p id='p4' dir='ltr' style='font-family: monospace;'>Привіт Світ<br>Привіт Світ</p>
    208  <p id='p5' dir='ltr' style='font-family: monospace;'>Привіт Світ<br> Я ще трохи тексту в другому рядку</p>
    209  <p id='p6' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
    210  <p id='p7' style='font-family: monospace;'>hello world<br>hello world</p>
    211  `,
    212  async function (browser, accDoc) {
    213    info("Testing multiline LtR text");
    214    await testTextNode(accDoc, browser, "p4");
    215    await testTextNode(accDoc, browser, "p5");
    216    // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping)
    217    await testTextNode(accDoc, browser, "p7");
    218  },
    219  {
    220    topLevel: true,
    221    iframe: true,
    222  }
    223 );
    224 
    225 /**
    226 * Test the text boundary for simple RtL text
    227 */
    228 addAccessibleTask(
    229  `
    230  <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
    231  <p id='p2' dir='rtl' style='font-family: monospace;'>ل</p>
    232  <p id='p3' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
    233  <pre id='p4' dir='rtl' style='font-family: monospace;'>a%0abcdef</pre>
    234  `,
    235  async function (browser, accDoc) {
    236    info("Testing simple RtL text");
    237    await testTextNode(accDoc, browser, "p1");
    238    await testTextNode(accDoc, browser, "p2");
    239    await testTextNode(accDoc, browser, "p3");
    240    await testTextNode(accDoc, browser, "p4");
    241  },
    242  {
    243    topLevel: true,
    244    iframe: true,
    245  }
    246 );
    247 
    248 /**
    249 * Test the text boundary for multiline RtL text
    250 */
    251 addAccessibleTask(
    252  `
    253  <p id='p4' dir='rtl' style='font-family: monospace;'>لل لللل لل<br>لل لللل لل</p>
    254  <p id='p5' dir='rtl' style='font-family: monospace;'>لل لللل لل<br> لل لل  لل لل ل لل لل لل</p>
    255  <p id='p6' dir='rtl' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
    256  <p id='p7' dir='rtl' style='font-family: monospace;'>hello world<br>hello world</p>
    257  `,
    258  async function (browser, accDoc) {
    259    info("Testing multiline RtL text");
    260    await testTextNode(accDoc, browser, "p4");
    261    //await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char
    262    // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping)
    263    await testTextNode(accDoc, browser, "p7");
    264  },
    265  {
    266    topLevel: true,
    267    iframe: true,
    268  }
    269 );
    270 
    271 /**
    272 * Test the partial text range boundary for RtL text
    273 */
    274 addAccessibleTask(
    275  `
    276  <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
    277  <p id='p2' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
    278  `,
    279  async function (browser, accDoc) {
    280    info("Testing partial ranges in RtL text");
    281    await testTextRange(accDoc, browser, "p1", 0, 4);
    282    await testTextRange(accDoc, browser, "p1", 2, 8);
    283    await testTextRange(accDoc, browser, "p1", 12, 17);
    284    await testTextRange(accDoc, browser, "p2", 0, 4);
    285    await testTextRange(accDoc, browser, "p2", 2, 8);
    286    await testTextRange(accDoc, browser, "p2", 6, 10);
    287  },
    288  {
    289    topLevel: true,
    290    iframe: true,
    291  }
    292 );
    293 
    294 /**
    295 * Test simple vertical text in rl and lr layouts
    296 */
    297 addAccessibleTask(
    298  `
    299  <div style="writing-mode: vertical-rl;">
    300    <p id='p1'>你好世界</p>
    301    <p id='p2'>hello world</p>
    302    <br>
    303    <p id='p3'>こんにちは世界</p>
    304  </div>
    305  <div style="writing-mode: vertical-lr;">
    306    <p id='p4'>你好世界</p>
    307    <p id='p5'>hello world</p>
    308    <br>
    309    <p id='p6'>こんにちは世界</p>
    310  </div>
    311  `,
    312  async function (browser, accDoc) {
    313    info("Testing vertical-rl");
    314    await testTextNode(accDoc, browser, "p1");
    315    await testTextNode(accDoc, browser, "p2");
    316    await testTextNode(accDoc, browser, "p3");
    317    info("Testing vertical-lr");
    318    await testTextNode(accDoc, browser, "p4");
    319    await testTextNode(accDoc, browser, "p5");
    320    await testTextNode(accDoc, browser, "p6");
    321  },
    322  {
    323    topLevel: true,
    324    iframe: true,
    325  }
    326 );
    327 
    328 /**
    329 * Test multiline vertical-rl text
    330 */
    331 addAccessibleTask(
    332  `
    333  <p id='p1' style='writing-mode: vertical-rl;'>你好世界<br>你好世界</p>
    334  <p id='p2' style='writing-mode: vertical-rl;'>hello world<br>hello world</p>
    335  <br>
    336  <p id='p3' style='writing-mode: vertical-rl;'>你好世界<br> 你好世界 你好世界</p>
    337  <p id='p4' style='writing-mode: vertical-rl;'>hello world<br> hello world hello world</p>
    338  `,
    339  async function (browser, accDoc) {
    340    info("Testing vertical-rl multiline");
    341    await testTextNode(accDoc, browser, "p1");
    342    await testTextNode(accDoc, browser, "p2");
    343    await testTextNode(accDoc, browser, "p3");
    344    // await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe
    345  },
    346  {
    347    topLevel: true,
    348    iframe: true,
    349  }
    350 );
    351 
    352 /**
    353 * Test text with embedded chars
    354 */
    355 addAccessibleTask(
    356  `<p id='p1' style='font-family: monospace;'>hello <a href="google.com">world</a></p>
    357   <p id='p2' style='font-family: monospace;'>hello<br><a href="google.com">world</a></p>
    358   <div id='d3'><p></p>hello world</div>
    359   <div id='d4'>hello world<p></p></div>
    360   <div id='d5'>oh<p></p>hello world</div>`,
    361  async function (browser, accDoc) {
    362    info("Testing embedded chars");
    363    await testTextNode(accDoc, browser, "p1");
    364    await testTextNode(accDoc, browser, "p2");
    365    await testTextNode(accDoc, browser, "d3");
    366    await testTextNode(accDoc, browser, "d4");
    367    await testTextNode(accDoc, browser, "d5");
    368  },
    369  {
    370    topLevel: true,
    371    iframe: true,
    372  }
    373 );
    374 
    375 /**
    376 * Test bounds after text mutations.
    377 */
    378 addAccessibleTask(
    379  `<p id="p">a</p>`,
    380  async function (browser, docAcc) {
    381    await testTextNode(docAcc, browser, "p");
    382    const p = findAccessibleChildByID(docAcc, "p");
    383    info("Appending a character to text leaf");
    384    let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p);
    385    await invokeContentTask(browser, [], () => {
    386      content.document.getElementById("p").firstChild.data = "ab";
    387    });
    388    await textInserted;
    389    await testTextNode(docAcc, browser, "p");
    390  },
    391  {
    392    chrome: true,
    393    topLevel: true,
    394    iframe: true,
    395  }
    396 );
    397 
    398 /**
    399 * Test character bounds on the insertion point at the end of a text box.
    400 */
    401 addAccessibleTask(
    402  `<input id="input" value="a">`,
    403  async function (browser, docAcc) {
    404    const input = findAccessibleChildByID(docAcc, "input");
    405    testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE);
    406  },
    407  {
    408    chrome: true,
    409    topLevel: true,
    410    iframe: true,
    411  }
    412 );
    413 
    414 /**
    415 * Test character bounds after non-br line break.
    416 */
    417 addAccessibleTask(
    418  `
    419  <style>
    420    @font-face {
    421      font-family: Ahem;
    422      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    423    }
    424    pre {
    425      font: 20px/20px Ahem;
    426    }
    427  </style>
    428  <pre id="t">XX
    429 XXX</pre>`,
    430  async function (browser, docAcc) {
    431    await testChar(docAcc, browser, "t", 3);
    432  },
    433  {
    434    chrome: true,
    435    topLevel: true,
    436    iframe: true,
    437  }
    438 );
    439 
    440 /**
    441 * Test character bounds in a pre with padding.
    442 */
    443 addAccessibleTask(
    444  `
    445  <style>
    446    @font-face {
    447      font-family: Ahem;
    448      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    449    }
    450    pre {
    451      font: 20px/20px Ahem;
    452      padding: 20px;
    453    }
    454  </style>
    455  <pre id="t">XX
    456 XXX</pre>`,
    457  async function (browser, docAcc) {
    458    await testTextNode(docAcc, browser, "t");
    459    await testChar(docAcc, browser, "t", 3);
    460  },
    461  {
    462    chrome: true,
    463    topLevel: true,
    464    iframe: true,
    465  }
    466 );
    467 
    468 /**
    469 * Test text bounds with an invalid end offset.
    470 */
    471 addAccessibleTask(
    472  `<p id="p">a</p>`,
    473  async function (browser, docAcc) {
    474    const p = findAccessibleChildByID(docAcc, "p");
    475    testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE);
    476  },
    477  { chrome: true, topLevel: !true }
    478 );
    479 
    480 /**
    481 * Test character bounds in an intervening inline element with non-br line breaks
    482 */
    483 addAccessibleTask(
    484  `
    485  <style>
    486    @font-face {
    487      font-family: Ahem;
    488      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    489    }
    490    pre {
    491      font: 20px/20px Ahem;
    492    }
    493  </style>
    494  <pre id="t"><code role="none">XX
    495 XXX
    496 XX
    497 X</pre>`,
    498  async function (browser, docAcc) {
    499    await testChar(docAcc, browser, "t", 0);
    500    await testChar(docAcc, browser, "t", 3);
    501    await testChar(docAcc, browser, "t", 7);
    502    await testChar(docAcc, browser, "t", 10);
    503  },
    504  {
    505    chrome: true,
    506    topLevel: true,
    507    iframe: true,
    508  }
    509 );
    510 
    511 /**
    512 * Test character bounds in an intervening inline element with margins
    513 * and with non-br line breaks
    514 */
    515 addAccessibleTask(
    516  `
    517  <style>
    518    @font-face {
    519      font-family: Ahem;
    520      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    521    }
    522  </style>
    523  <div>hello<pre id="t" style="margin-left:100px;margin-top:30px;background-color:blue;">XX
    524 XXX
    525 XX
    526 X</pre></div>`,
    527  async function (browser, docAcc) {
    528    await testChar(docAcc, browser, "t", 0);
    529    await testChar(docAcc, browser, "t", 3);
    530    await testChar(docAcc, browser, "t", 7);
    531    await testChar(docAcc, browser, "t", 10);
    532  },
    533  {
    534    chrome: true,
    535    topLevel: true,
    536    iframe: true,
    537  }
    538 );
    539 
    540 /**
    541 * Test text bounds in a textarea after scrolling.
    542 */
    543 addAccessibleTask(
    544  `
    545 <textarea id="textarea" rows="1">a
    546 b
    547 c</textarea>
    548  `,
    549  async function (browser, docAcc) {
    550    // We can't use testChar because Range.getBoundingClientRect isn't supported
    551    // inside textareas.
    552    const textarea = findAccessibleChildByID(docAcc, "textarea");
    553    textarea.QueryInterface(nsIAccessibleText);
    554    const oldY = {};
    555    textarea.getCharacterExtents(
    556      4,
    557      {},
    558      oldY,
    559      {},
    560      {},
    561      COORDTYPE_SCREEN_RELATIVE
    562    );
    563    info("Moving textarea caret to c");
    564    await invokeContentTask(browser, [], () => {
    565      const textareaDom = content.document.getElementById("textarea");
    566      textareaDom.focus();
    567      textareaDom.selectionStart = 4;
    568    });
    569    await waitForContentPaint(browser);
    570    const newY = {};
    571    textarea.getCharacterExtents(
    572      4,
    573      {},
    574      newY,
    575      {},
    576      {},
    577      COORDTYPE_SCREEN_RELATIVE
    578    );
    579    Assert.less(
    580      newY.value,
    581      oldY.value,
    582      "y coordinate smaller after scrolling down"
    583    );
    584  },
    585  { chrome: true, topLevel: true, iframe: !true }
    586 );
    587 
    588 /**
    589 * Test magic offsets with GetCharacter/RangeExtents.
    590 */
    591 addAccessibleTask(
    592  `<input id="input" value="abc">`,
    593  async function (browser, docAcc) {
    594    const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
    595    info("Setting caret and focusing input");
    596    let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    597    await invokeContentTask(browser, [], () => {
    598      const inputDom = content.document.getElementById("input");
    599      inputDom.selectionStart = inputDom.selectionEnd = 1;
    600      inputDom.focus();
    601    });
    602    await caretMoved;
    603    is(input.caretOffset, 1, "input caretOffset is 1");
    604    let expectedX = {};
    605    let expectedY = {};
    606    let expectedW = {};
    607    let expectedH = {};
    608    let magicX = {};
    609    let magicY = {};
    610    let magicW = {};
    611    let magicH = {};
    612    input.getCharacterExtents(
    613      1,
    614      expectedX,
    615      expectedY,
    616      expectedW,
    617      expectedH,
    618      COORDTYPE_SCREEN_RELATIVE
    619    );
    620    input.getCharacterExtents(
    621      nsIAccessibleText.TEXT_OFFSET_CARET,
    622      magicX,
    623      magicY,
    624      magicW,
    625      magicH,
    626      COORDTYPE_SCREEN_RELATIVE
    627    );
    628    Assert.deepEqual(
    629      [magicX.value, magicY.value, magicW.value, magicH.value],
    630      [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
    631      "GetCharacterExtents correct with TEXT_OFFSET_CARET"
    632    );
    633    input.getRangeExtents(
    634      1,
    635      3,
    636      expectedX,
    637      expectedY,
    638      expectedW,
    639      expectedH,
    640      COORDTYPE_SCREEN_RELATIVE
    641    );
    642    input.getRangeExtents(
    643      nsIAccessibleText.TEXT_OFFSET_CARET,
    644      nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT,
    645      magicX,
    646      magicY,
    647      magicW,
    648      magicH,
    649      COORDTYPE_SCREEN_RELATIVE
    650    );
    651    Assert.deepEqual(
    652      [magicX.value, magicY.value, magicW.value, magicH.value],
    653      [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
    654      "GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT"
    655    );
    656  },
    657  { chrome: true, topLevel: true, remoteIframe: !true }
    658 );
    659 
    660 /**
    661 * Test wrapped text and pre-formatted text beginning with an empty line.
    662 */
    663 addAccessibleTask(
    664  `
    665 <style>
    666  #wrappedText {
    667    width: 3ch;
    668    font-family: monospace;
    669  }
    670 </style>
    671 <p id="wrappedText"><a href="https://example.com/">a</a>b cd</p>
    672 <p id="emptyFirstLine" style="white-space: pre-line;">
    673 foo</p>
    674  `,
    675  async function (browser, docAcc) {
    676    await testChar(docAcc, browser, "wrappedText", 0);
    677    await testChar(docAcc, browser, "wrappedText", 1);
    678    await testChar(docAcc, browser, "wrappedText", 2);
    679    await testChar(docAcc, browser, "wrappedText", 3);
    680    await testChar(docAcc, browser, "wrappedText", 4);
    681 
    682    // We can't use testChar for emptyFirstLine because it doesn't handle white
    683    // space properly. Instead, verify that the first character is at the top
    684    // left of the text leaf.
    685    const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [
    686      nsIAccessibleText,
    687    ]);
    688    const emptyFirstLineLeaf = emptyFirstLine.firstChild;
    689    const leafX = {};
    690    const leafY = {};
    691    emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {});
    692    testTextPos(
    693      emptyFirstLine,
    694      0,
    695      [leafX.value, leafY.value],
    696      COORDTYPE_SCREEN_RELATIVE
    697    );
    698  },
    699  { chrome: true, topLevel: true, remoteIframe: !true }
    700 );
    701 
    702 /**
    703 * Test character bounds in an intervening inline element with non-br line breaks
    704 */
    705 addAccessibleTask(
    706  `
    707  <style>
    708    @font-face {
    709      font-family: Ahem;
    710      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    711    }
    712    pre {
    713      font: 20px/20px Ahem;
    714    }
    715  </style>
    716  <pre><code id="t" role="group">XX
    717 XXX
    718 XX
    719 X</pre>`,
    720  async function (browser, docAcc) {
    721    await testChar(docAcc, browser, "t", 0);
    722    await testChar(docAcc, browser, "t", 3);
    723    await testChar(docAcc, browser, "t", 7);
    724    await testChar(docAcc, browser, "t", 10);
    725  },
    726  {
    727    chrome: true,
    728    topLevel: true,
    729    iframe: true,
    730  }
    731 );
    732 
    733 /**
    734 * Test character bounds where content white space isn't rendered.
    735 */
    736 addAccessibleTask(
    737  `
    738 <p id="single">a  b</p>
    739 <p id="multi"><ins>a </ins>
    740 b</p>
    741 <pre id="pre">a  b</pre>
    742  `,
    743  async function (browser, docAcc) {
    744    await testLineWithNonRenderedSpace(docAcc, browser, "single", 3);
    745    await testLineWithNonRenderedSpace(docAcc, browser, "multi", 2);
    746    for (let offset = 0; offset < 4; ++offset) {
    747      await testChar(docAcc, browser, "pre", offset);
    748    }
    749  },
    750  { chrome: true, topLevel: true }
    751 );
    752 
    753 function getCharacterExtents(acc, offset) {
    754  const x = {};
    755  const y = {};
    756  const w = {};
    757  const h = {};
    758  acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
    759  return [x.value, y.value, w.value, h.value];
    760 }
    761 
    762 /**
    763 * Test character bounds of line feed characters in a textarea.
    764 */
    765 addAccessibleTask(
    766  `
    767 <textarea id="textarea">a
    768 
    769 b</textarea>
    770  `,
    771  async function testLineFeedTextarea(browser, docAcc) {
    772    // We can't use testChar because it doesn't know how to handle line feeds.
    773    // We check relative to other characters instead.
    774    const textarea = findAccessibleChildByID(docAcc, "textarea", [
    775      nsIAccessibleText,
    776    ]);
    777    const [x0, y0, ,] = getCharacterExtents(textarea, 0);
    778    const [x1, y1, w1, h1] = getCharacterExtents(textarea, 1);
    779    const [x2, y2, w2, h2] = getCharacterExtents(textarea, 2);
    780    const [x3, y3, ,] = getCharacterExtents(textarea, 3);
    781    // Character 0 is a letter on the first line.
    782    // Character 1 is a line feed at the end of the first line.
    783    Assert.greater(x1, x0, "x1 > x0");
    784    is(y1, y0, "y1 == y0");
    785    Assert.greater(w1, 0, "w1 > 0");
    786    Assert.greater(h1, 0, "h1 > 0");
    787    // Character 2 is a line feed on a blank line.
    788    is(x2, x0, "x2 == x0");
    789    Assert.greaterOrEqual(y2, y1 + h1, "y2 >= y1 + h1");
    790    Assert.greater(w2, 0, "w2 > 0");
    791    Assert.greater(h2, 0, "h2 > 0");
    792    // Character 3 is a letter on the final line.
    793    is(x3, x0, "x3 == x0");
    794    Assert.greaterOrEqual(y3, y2 + h2, "y3 >= y2 + h2");
    795  },
    796  { chrome: true, topLevel: true }
    797 );
    798 
    799 /**
    800 * Test line feed characters in a contentEditable.
    801 */
    802 addAccessibleTask(
    803  `
    804 <div contenteditable role="textbox">
    805  <div id="ce0">a</div>
    806  <div id="ce1"><br></div>
    807  <div id="ce2">b</div>
    808 </div>
    809  `,
    810  async function testLineFeedEditable(browser, docAcc) {
    811    // We can't use testChar because it doesn't know how to handle line feeds.
    812    // We check relative to other characters instead.
    813    const ce0 = findAccessibleChildByID(docAcc, "ce0", [nsIAccessibleText]);
    814    const [x0, y0, ,] = getCharacterExtents(ce0, 0);
    815    const ce1 = findAccessibleChildByID(docAcc, "ce1", [nsIAccessibleText]);
    816    const [x1, y1, w1, h1] = getCharacterExtents(ce1, 0);
    817    const ce2 = findAccessibleChildByID(docAcc, "ce2", [nsIAccessibleText]);
    818    const [x2, y2, ,] = getCharacterExtents(ce2, 0);
    819    // Character 0 is a letter on the first line.
    820    // Character 1 is a line feed on a blank line.
    821    is(x1, x0, "x1 == x0");
    822    Assert.greater(y1, y0, "y1 > y0");
    823    Assert.greater(w1, 0, "w1 > 0");
    824    Assert.greater(h1, 0, "h1 > 0");
    825    // Character 2 is a letter on the final line.
    826    is(x2, x0, "x2 == x0");
    827    Assert.greaterOrEqual(y2, y1 + h1, "y2 >= y1 + h1");
    828  },
    829  { chrome: true, topLevel: true }
    830 );
    831 
    832 /**
    833 * Test list bullets.
    834 */
    835 addAccessibleTask(
    836  `<ul><li id="li" style="font-family: monospace;">a</li></ul>`,
    837  async function testBullet(browser, docAcc) {
    838    // We can't use testChar because it doesn't know how to handle list bullets.
    839    // We check relative to other characters instead.
    840    const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
    841    const [x0, y0, w0, h0] = getCharacterExtents(li, 0);
    842    const [x1, y1, w1, h1] = getCharacterExtents(li, 1);
    843    const [x2, y2, ,] = getCharacterExtents(li, 2);
    844    // Characters 0 and 1 are the bullet.
    845    // Character 2 is a letter.
    846    Assert.less(x0, x2, "x0 < x2");
    847    isWithin(y0, y2, 5, "y0 ~ y2");
    848    Assert.greater(w0, 0, "w0 > 0");
    849    Assert.greater(h0, 0, "h0 > 0");
    850    Assert.less(x1, x2, "x1 < x2");
    851    isWithin(y1, y2, 5, "y1 ~ y2");
    852    Assert.greater(w1, 0, "w1 > 0");
    853    Assert.greater(h1, 0, "h1 > 0");
    854  },
    855  { chrome: true, topLevel: true }
    856 );