/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";
requestLongerTimeout(3);

/* import-globals-from ../../mochitest/layout.js */
loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });

// Note that testTextNode, testChar and testTextRange currently don't handle
// white space in the code that doesn't get rendered on screen. To work around
// this, ensure that containers you want to test are all on a single line in the
// test snippet.

async function testTextNode(accDoc, browser, id) {
  await testTextRange(accDoc, browser, id, 0, -1);
}

async function testChar(accDoc, browser, id, idx) {
  await testTextRange(accDoc, browser, id, idx, idx + 1);
}

async function testTextRange(accDoc, browser, id, start, end) {
  const r = await invokeContentTask(
    browser,
    [id, start, end],
    (_id, _start, _end) => {
      const htNode = content.document.getElementById(_id);
      let [eX, eY, eW, eH] = [
        Number.MAX_SAFE_INTEGER,
        Number.MAX_SAFE_INTEGER,
        0,
        0,
      ];
      let traversed = 0;
      let localStart = _start;
      let endTraversal = false;
      for (let element of htNode.childNodes) {
        // ignore whitespace, but not embedded elements
        let isEmbeddedElement = false;
        if (element.length == undefined) {
          let potentialTextContainer = element;
          while (
            potentialTextContainer &&
            potentialTextContainer.length == undefined
          ) {
            potentialTextContainer = element.firstChild;
          }
          if (potentialTextContainer && potentialTextContainer.length) {
            // If we can reach some text from this container, use that as part
            // of our range. This is important when testing with intervening inline
            // elements. ie. <pre><code>ab%0acd
            element = potentialTextContainer;
          } else if (element.firstChild) {
            isEmbeddedElement = true;
          } else {
            continue;
          }
        }
        if (element.length + traversed < _start) {
          // If our start index is not within this
          // node, keep looking.
          traversed += element.length;
          localStart -= element.length;
          continue;
        }

        let rect;
        if (isEmbeddedElement) {
          rect = element.getBoundingClientRect();
        } else {
          const range = content.document.createRange();
          range.setStart(element, localStart);

          if (_end != -1 && _end - traversed <= element.length) {
            // If the current node contains
            // our end index, stop here.
            endTraversal = true;
            range.setEnd(element, _end - traversed);
          } else {
            range.setEnd(element, element.length);
          }

          rect = range.getBoundingClientRect();
        }

        const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX;
        const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY;
        eX = Math.min(eX, rect.x);
        eY = Math.min(eY, rect.y);
        eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX);
        eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY);

        if (endTraversal) {
          break;
        }
        localStart = 0;
        traversed += element.length;
      }
      return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)];
    }
  );
  let hyperTextNode = findAccessibleChildByID(accDoc, id);

  // Add in the doc's screen coords because getBoundingClientRect
  // is relative to the document, not the screen. This assumes the doc's
  // screen coords are correct. We use getBoundsInCSSPixels to avoid factoring
  // in the DPR ourselves.
  let x = {};
  let y = {};
  let w = {};
  let h = {};
  accDoc.getBoundsInCSSPixels(x, y, w, h);
  r[0] += x.value;
  r[1] += y.value;
  if (end != -1 && end - start == 1) {
    // If we're only testing a character, use this function because it calls
    // CharBounds() directly instead of TextBounds().
    testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE);
  } else {
    testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE);
  }
}

/**
 * Since testTextChar can't handle non-rendered white space, this function first
 * uses testTextChar to verify the first character and then ensures all
 * characters thereafter have an incrementing x and a non-0 width.
 */
async function testLineWithNonRenderedSpace(docAcc, browser, id, length) {
  await testChar(docAcc, browser, id, 0);
  const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
  let prevX = -1;
  for (let offset = 0; offset < length; ++offset) {
    const x = {};
    const y = {};
    const w = {};
    const h = {};
    acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
    Assert.greater(
      x.value,
      prevX,
      `${id}: offset ${offset} x is larger (${x.value})`
    );
    prevX = x.value;
    Assert.greater(w.value, 0, `${id}: offset ${offset} width > 0`);
  }
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["test.wait300msAfterTabSwitch", true]],
  });
});

/**
 * Test the text range boundary for simple LtR text
 */
addAccessibleTask(
  `
  <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
  <p id='p2' style='font-family: monospace;'>ل</p>
  <p id='p3' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
  <pre id='p4' style='font-family: monospace;'>a%0abcdef</pre>
  `,
  async function (browser, accDoc) {
    info("Testing simple LtR text");
    await testTextNode(accDoc, browser, "p1");
    await testTextNode(accDoc, browser, "p2");
    await testTextNode(accDoc, browser, "p3");
    await testTextNode(accDoc, browser, "p4");
  },
  {
    iframe: true,
  }
);

/**
 * Test the partial text range boundary for LtR text
 */
addAccessibleTask(
  `
  <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
  <p id='p2' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
  `,
  async function (browser, accDoc) {
    info("Testing partial ranges in LtR text");
    await testTextRange(accDoc, browser, "p1", 0, 4);
    await testTextRange(accDoc, browser, "p1", 2, 8);
    await testTextRange(accDoc, browser, "p1", 12, 17);
    await testTextRange(accDoc, browser, "p2", 0, 4);
    await testTextRange(accDoc, browser, "p2", 2, 8);
    await testTextRange(accDoc, browser, "p2", 6, 11);
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test the text boundary for multiline LtR text
 */
addAccessibleTask(
  `
  <p id='p4' dir='ltr' style='font-family: monospace;'>Привіт Світ<br>Привіт Світ</p>
  <p id='p5' dir='ltr' style='font-family: monospace;'>Привіт Світ<br> Я ще трохи тексту в другому рядку</p>
  <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>
  <p id='p7' style='font-family: monospace;'>hello world<br>hello world</p>
  `,
  async function (browser, accDoc) {
    info("Testing multiline LtR text");
    await testTextNode(accDoc, browser, "p4");
    await testTextNode(accDoc, browser, "p5");
    // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping)
    await testTextNode(accDoc, browser, "p7");
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test the text boundary for simple RtL text
 */
addAccessibleTask(
  `
  <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
  <p id='p2' dir='rtl' style='font-family: monospace;'>ل</p>
  <p id='p3' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
  <pre id='p4' dir='rtl' style='font-family: monospace;'>a%0abcdef</pre>
  `,
  async function (browser, accDoc) {
    info("Testing simple RtL text");
    await testTextNode(accDoc, browser, "p1");
    await testTextNode(accDoc, browser, "p2");
    await testTextNode(accDoc, browser, "p3");
    await testTextNode(accDoc, browser, "p4");
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test the text boundary for multiline RtL text
 */
addAccessibleTask(
  `
  <p id='p4' dir='rtl' style='font-family: monospace;'>لل لللل لل<br>لل لللل لل</p>
  <p id='p5' dir='rtl' style='font-family: monospace;'>لل لللل لل<br> لل لل  لل لل ل لل لل لل</p>
  <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>
  <p id='p7' dir='rtl' style='font-family: monospace;'>hello world<br>hello world</p>
  `,
  async function (browser, accDoc) {
    info("Testing multiline RtL text");
    await testTextNode(accDoc, browser, "p4");
    //await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char
    // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping)
    await testTextNode(accDoc, browser, "p7");
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test the partial text range boundary for RtL text
 */
addAccessibleTask(
  `
  <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
  <p id='p2' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
  `,
  async function (browser, accDoc) {
    info("Testing partial ranges in RtL text");
    await testTextRange(accDoc, browser, "p1", 0, 4);
    await testTextRange(accDoc, browser, "p1", 2, 8);
    await testTextRange(accDoc, browser, "p1", 12, 17);
    await testTextRange(accDoc, browser, "p2", 0, 4);
    await testTextRange(accDoc, browser, "p2", 2, 8);
    await testTextRange(accDoc, browser, "p2", 6, 10);
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test simple vertical text in rl and lr layouts
 */
addAccessibleTask(
  `
  <div style="writing-mode: vertical-rl;">
    <p id='p1'>你好世界</p>
    <p id='p2'>hello world</p>
    <br>
    <p id='p3'>こんにちは世界</p>
  </div>
  <div style="writing-mode: vertical-lr;">
    <p id='p4'>你好世界</p>
    <p id='p5'>hello world</p>
    <br>
    <p id='p6'>こんにちは世界</p>
  </div>
  `,
  async function (browser, accDoc) {
    info("Testing vertical-rl");
    await testTextNode(accDoc, browser, "p1");
    await testTextNode(accDoc, browser, "p2");
    await testTextNode(accDoc, browser, "p3");
    info("Testing vertical-lr");
    await testTextNode(accDoc, browser, "p4");
    await testTextNode(accDoc, browser, "p5");
    await testTextNode(accDoc, browser, "p6");
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test multiline vertical-rl text
 */
addAccessibleTask(
  `
  <p id='p1' style='writing-mode: vertical-rl;'>你好世界<br>你好世界</p>
  <p id='p2' style='writing-mode: vertical-rl;'>hello world<br>hello world</p>
  <br>
  <p id='p3' style='writing-mode: vertical-rl;'>你好世界<br> 你好世界 你好世界</p>
  <p id='p4' style='writing-mode: vertical-rl;'>hello world<br> hello world hello world</p>
  `,
  async function (browser, accDoc) {
    info("Testing vertical-rl multiline");
    await testTextNode(accDoc, browser, "p1");
    await testTextNode(accDoc, browser, "p2");
    await testTextNode(accDoc, browser, "p3");
    // await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test text with embedded chars
 */
addAccessibleTask(
  `<p id='p1' style='font-family: monospace;'>hello <a href="google.com">world</a></p>
   <p id='p2' style='font-family: monospace;'>hello<br><a href="google.com">world</a></p>
   <div id='d3'><p></p>hello world</div>
   <div id='d4'>hello world<p></p></div>
   <div id='d5'>oh<p></p>hello world</div>`,
  async function (browser, accDoc) {
    info("Testing embedded chars");
    await testTextNode(accDoc, browser, "p1");
    await testTextNode(accDoc, browser, "p2");
    await testTextNode(accDoc, browser, "d3");
    await testTextNode(accDoc, browser, "d4");
    await testTextNode(accDoc, browser, "d5");
  },
  {
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test bounds after text mutations.
 */
addAccessibleTask(
  `<p id="p">a</p>`,
  async function (browser, docAcc) {
    await testTextNode(docAcc, browser, "p");
    const p = findAccessibleChildByID(docAcc, "p");
    info("Appending a character to text leaf");
    let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("p").firstChild.data = "ab";
    });
    await textInserted;
    await testTextNode(docAcc, browser, "p");
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test character bounds on the insertion point at the end of a text box.
 */
addAccessibleTask(
  `<input id="input" value="a">`,
  async function (browser, docAcc) {
    const input = findAccessibleChildByID(docAcc, "input");
    testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test character bounds after non-br line break.
 */
addAccessibleTask(
  `
  <style>
    @font-face {
      font-family: Ahem;
      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    }
    pre {
      font: 20px/20px Ahem;
    }
  </style>
  <pre id="t">XX
XXX</pre>`,
  async function (browser, docAcc) {
    await testChar(docAcc, browser, "t", 3);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test character bounds in a pre with padding.
 */
addAccessibleTask(
  `
  <style>
    @font-face {
      font-family: Ahem;
      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    }
    pre {
      font: 20px/20px Ahem;
      padding: 20px;
    }
  </style>
  <pre id="t">XX
XXX</pre>`,
  async function (browser, docAcc) {
    await testTextNode(docAcc, browser, "t");
    await testChar(docAcc, browser, "t", 3);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test text bounds with an invalid end offset.
 */
addAccessibleTask(
  `<p id="p">a</p>`,
  async function (browser, docAcc) {
    const p = findAccessibleChildByID(docAcc, "p");
    testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE);
  },
  { chrome: true, topLevel: !true }
);

/**
 * Test character bounds in an intervening inline element with non-br line breaks
 */
addAccessibleTask(
  `
  <style>
    @font-face {
      font-family: Ahem;
      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    }
    pre {
      font: 20px/20px Ahem;
    }
  </style>
  <pre id="t"><code role="none">XX
XXX
XX
X</pre>`,
  async function (browser, docAcc) {
    await testChar(docAcc, browser, "t", 0);
    await testChar(docAcc, browser, "t", 3);
    await testChar(docAcc, browser, "t", 7);
    await testChar(docAcc, browser, "t", 10);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test character bounds in an intervening inline element with margins
 * and with non-br line breaks
 */
addAccessibleTask(
  `
  <style>
    @font-face {
      font-family: Ahem;
      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    }
  </style>
  <div>hello<pre id="t" style="margin-left:100px;margin-top:30px;background-color:blue;">XX
XXX
XX
X</pre></div>`,
  async function (browser, docAcc) {
    await testChar(docAcc, browser, "t", 0);
    await testChar(docAcc, browser, "t", 3);
    await testChar(docAcc, browser, "t", 7);
    await testChar(docAcc, browser, "t", 10);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test text bounds in a textarea after scrolling.
 */
addAccessibleTask(
  `
<textarea id="textarea" rows="1">a
b
c</textarea>
  `,
  async function (browser, docAcc) {
    // We can't use testChar because Range.getBoundingClientRect isn't supported
    // inside textareas.
    const textarea = findAccessibleChildByID(docAcc, "textarea");
    textarea.QueryInterface(nsIAccessibleText);
    const oldY = {};
    textarea.getCharacterExtents(
      4,
      {},
      oldY,
      {},
      {},
      COORDTYPE_SCREEN_RELATIVE
    );
    info("Moving textarea caret to c");
    await invokeContentTask(browser, [], () => {
      const textareaDom = content.document.getElementById("textarea");
      textareaDom.focus();
      textareaDom.selectionStart = 4;
    });
    await waitForContentPaint(browser);
    const newY = {};
    textarea.getCharacterExtents(
      4,
      {},
      newY,
      {},
      {},
      COORDTYPE_SCREEN_RELATIVE
    );
    Assert.less(
      newY.value,
      oldY.value,
      "y coordinate smaller after scrolling down"
    );
  },
  { chrome: true, topLevel: true, iframe: !true }
);

/**
 * Test magic offsets with GetCharacter/RangeExtents.
 */
addAccessibleTask(
  `<input id="input" value="abc">`,
  async function (browser, docAcc) {
    const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
    info("Setting caret and focusing input");
    let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
    await invokeContentTask(browser, [], () => {
      const inputDom = content.document.getElementById("input");
      inputDom.selectionStart = inputDom.selectionEnd = 1;
      inputDom.focus();
    });
    await caretMoved;
    is(input.caretOffset, 1, "input caretOffset is 1");
    let expectedX = {};
    let expectedY = {};
    let expectedW = {};
    let expectedH = {};
    let magicX = {};
    let magicY = {};
    let magicW = {};
    let magicH = {};
    input.getCharacterExtents(
      1,
      expectedX,
      expectedY,
      expectedW,
      expectedH,
      COORDTYPE_SCREEN_RELATIVE
    );
    input.getCharacterExtents(
      nsIAccessibleText.TEXT_OFFSET_CARET,
      magicX,
      magicY,
      magicW,
      magicH,
      COORDTYPE_SCREEN_RELATIVE
    );
    Assert.deepEqual(
      [magicX.value, magicY.value, magicW.value, magicH.value],
      [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
      "GetCharacterExtents correct with TEXT_OFFSET_CARET"
    );
    input.getRangeExtents(
      1,
      3,
      expectedX,
      expectedY,
      expectedW,
      expectedH,
      COORDTYPE_SCREEN_RELATIVE
    );
    input.getRangeExtents(
      nsIAccessibleText.TEXT_OFFSET_CARET,
      nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT,
      magicX,
      magicY,
      magicW,
      magicH,
      COORDTYPE_SCREEN_RELATIVE
    );
    Assert.deepEqual(
      [magicX.value, magicY.value, magicW.value, magicH.value],
      [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
      "GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT"
    );
  },
  { chrome: true, topLevel: true, remoteIframe: !true }
);

/**
 * Test wrapped text and pre-formatted text beginning with an empty line.
 */
addAccessibleTask(
  `
<style>
  #wrappedText {
    width: 3ch;
    font-family: monospace;
  }
</style>
<p id="wrappedText"><a href="https://example.com/">a</a>b cd</p>
<p id="emptyFirstLine" style="white-space: pre-line;">
foo</p>
  `,
  async function (browser, docAcc) {
    await testChar(docAcc, browser, "wrappedText", 0);
    await testChar(docAcc, browser, "wrappedText", 1);
    await testChar(docAcc, browser, "wrappedText", 2);
    await testChar(docAcc, browser, "wrappedText", 3);
    await testChar(docAcc, browser, "wrappedText", 4);

    // We can't use testChar for emptyFirstLine because it doesn't handle white
    // space properly. Instead, verify that the first character is at the top
    // left of the text leaf.
    const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [
      nsIAccessibleText,
    ]);
    const emptyFirstLineLeaf = emptyFirstLine.firstChild;
    const leafX = {};
    const leafY = {};
    emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {});
    testTextPos(
      emptyFirstLine,
      0,
      [leafX.value, leafY.value],
      COORDTYPE_SCREEN_RELATIVE
    );
  },
  { chrome: true, topLevel: true, remoteIframe: !true }
);

/**
 * Test character bounds in an intervening inline element with non-br line breaks
 */
addAccessibleTask(
  `
  <style>
    @font-face {
      font-family: Ahem;
      src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
    }
    pre {
      font: 20px/20px Ahem;
    }
  </style>
  <pre><code id="t" role="group">XX
XXX
XX
X</pre>`,
  async function (browser, docAcc) {
    await testChar(docAcc, browser, "t", 0);
    await testChar(docAcc, browser, "t", 3);
    await testChar(docAcc, browser, "t", 7);
    await testChar(docAcc, browser, "t", 10);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: true,
  }
);

/**
 * Test character bounds where content white space isn't rendered.
 */
addAccessibleTask(
  `
<p id="single">a  b</p>
<p id="multi"><ins>a </ins>
b</p>
<pre id="pre">a  b</pre>
  `,
  async function (browser, docAcc) {
    await testLineWithNonRenderedSpace(docAcc, browser, "single", 3);
    await testLineWithNonRenderedSpace(docAcc, browser, "multi", 2);
    for (let offset = 0; offset < 4; ++offset) {
      await testChar(docAcc, browser, "pre", offset);
    }
  },
  { chrome: true, topLevel: true }
);

function getCharacterExtents(acc, offset) {
  const x = {};
  const y = {};
  const w = {};
  const h = {};
  acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
  return [x.value, y.value, w.value, h.value];
}

/**
 * Test character bounds of line feed characters in a textarea.
 */
addAccessibleTask(
  `
<textarea id="textarea">a

b</textarea>
  `,
  async function testLineFeedTextarea(browser, docAcc) {
    // We can't use testChar because it doesn't know how to handle line feeds.
    // We check relative to other characters instead.
    const textarea = findAccessibleChildByID(docAcc, "textarea", [
      nsIAccessibleText,
    ]);
    const [x0, y0, ,] = getCharacterExtents(textarea, 0);
    const [x1, y1, w1, h1] = getCharacterExtents(textarea, 1);
    const [x2, y2, w2, h2] = getCharacterExtents(textarea, 2);
    const [x3, y3, ,] = getCharacterExtents(textarea, 3);
    // Character 0 is a letter on the first line.
    // Character 1 is a line feed at the end of the first line.
    Assert.greater(x1, x0, "x1 > x0");
    is(y1, y0, "y1 == y0");
    Assert.greater(w1, 0, "w1 > 0");
    Assert.greater(h1, 0, "h1 > 0");
    // Character 2 is a line feed on a blank line.
    is(x2, x0, "x2 == x0");
    Assert.greaterOrEqual(y2, y1 + h1, "y2 >= y1 + h1");
    Assert.greater(w2, 0, "w2 > 0");
    Assert.greater(h2, 0, "h2 > 0");
    // Character 3 is a letter on the final line.
    is(x3, x0, "x3 == x0");
    Assert.greaterOrEqual(y3, y2 + h2, "y3 >= y2 + h2");
  },
  { chrome: true, topLevel: true }
);

/**
 * Test line feed characters in a contentEditable.
 */
addAccessibleTask(
  `
<div contenteditable role="textbox">
  <div id="ce0">a</div>
  <div id="ce1"><br></div>
  <div id="ce2">b</div>
</div>
  `,
  async function testLineFeedEditable(browser, docAcc) {
    // We can't use testChar because it doesn't know how to handle line feeds.
    // We check relative to other characters instead.
    const ce0 = findAccessibleChildByID(docAcc, "ce0", [nsIAccessibleText]);
    const [x0, y0, ,] = getCharacterExtents(ce0, 0);
    const ce1 = findAccessibleChildByID(docAcc, "ce1", [nsIAccessibleText]);
    const [x1, y1, w1, h1] = getCharacterExtents(ce1, 0);
    const ce2 = findAccessibleChildByID(docAcc, "ce2", [nsIAccessibleText]);
    const [x2, y2, ,] = getCharacterExtents(ce2, 0);
    // Character 0 is a letter on the first line.
    // Character 1 is a line feed on a blank line.
    is(x1, x0, "x1 == x0");
    Assert.greater(y1, y0, "y1 > y0");
    Assert.greater(w1, 0, "w1 > 0");
    Assert.greater(h1, 0, "h1 > 0");
    // Character 2 is a letter on the final line.
    is(x2, x0, "x2 == x0");
    Assert.greaterOrEqual(y2, y1 + h1, "y2 >= y1 + h1");
  },
  { chrome: true, topLevel: true }
);

/**
 * Test list bullets.
 */
addAccessibleTask(
  `<ul><li id="li" style="font-family: monospace;">a</li></ul>`,
  async function testBullet(browser, docAcc) {
    // We can't use testChar because it doesn't know how to handle list bullets.
    // We check relative to other characters instead.
    const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
    const [x0, y0, w0, h0] = getCharacterExtents(li, 0);
    const [x1, y1, w1, h1] = getCharacterExtents(li, 1);
    const [x2, y2, ,] = getCharacterExtents(li, 2);
    // Characters 0 and 1 are the bullet.
    // Character 2 is a letter.
    Assert.less(x0, x2, "x0 < x2");
    isWithin(y0, y2, 5, "y0 ~ y2");
    Assert.greater(w0, 0, "w0 > 0");
    Assert.greater(h0, 0, "h0 > 0");
    Assert.less(x1, x2, "x1 < x2");
    isWithin(y1, y2, 5, "y1 ~ y2");
    Assert.greater(w1, 0, "w1 > 0");
    Assert.greater(h1, 0, "h1 > 0");
  },
  { chrome: true, topLevel: true }
);
