summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser/text
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/tests/browser/text')
-rw-r--r--accessible/tests/browser/text/browser.toml24
-rw-r--r--accessible/tests/browser/text/browser_editabletext.js173
-rw-r--r--accessible/tests/browser/text/browser_text.js326
-rw-r--r--accessible/tests/browser/text/browser_text_caret.js452
-rw-r--r--accessible/tests/browser/text/browser_text_paragraph_boundary.js22
-rw-r--r--accessible/tests/browser/text/browser_text_selection.js344
-rw-r--r--accessible/tests/browser/text/browser_text_spelling.js151
-rw-r--r--accessible/tests/browser/text/browser_textleafpoint.js524
-rw-r--r--accessible/tests/browser/text/head.js276
9 files changed, 2292 insertions, 0 deletions
diff --git a/accessible/tests/browser/text/browser.toml b/accessible/tests/browser/text/browser.toml
new file mode 100644
index 0000000000..c04531b126
--- /dev/null
+++ b/accessible/tests/browser/text/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_editabletext.js"]
+
+["browser_text.js"]
+
+["browser_text_caret.js"]
+
+["browser_text_paragraph_boundary.js"]
+
+["browser_text_selection.js"]
+
+["browser_text_spelling.js"]
+skip-if = ["true"] # Bug 1800400
+
+["browser_textleafpoint.js"]
diff --git a/accessible/tests/browser/text/browser_editabletext.js b/accessible/tests/browser/text/browser_editabletext.js
new file mode 100644
index 0000000000..0310122deb
--- /dev/null
+++ b/accessible/tests/browser/text/browser_editabletext.js
@@ -0,0 +1,173 @@
+/* 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";
+
+async function testEditable(browser, acc, aBefore = "", aAfter = "") {
+ async function resetInput() {
+ if (acc.childCount <= 1) {
+ return;
+ }
+
+ let emptyInputEvent = waitForEvent(EVENT_TEXT_VALUE_CHANGE, "input");
+ await invokeContentTask(browser, [], async () => {
+ content.document.getElementById("input").innerHTML = "";
+ });
+
+ await emptyInputEvent;
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // insertText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testInsertText(acc, "ma ", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ma hello", aAfter]);
+ await testInsertText(acc, "ma", 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "mama hello", aAfter]);
+ await testInsertText(acc, " hello", 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [
+ aBefore,
+ "mama hello hello",
+ aAfter,
+ ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // deleteText
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello hello", aAfter]);
+ await testDeleteText(acc, 5, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hellohello", aAfter]);
+ await testDeleteText(acc, 5, 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ // XXX: clipboard operation tests don't work well with editable documents.
+ if (acc.role == ROLE_DOCUMENT) {
+ return;
+ }
+
+ await resetInput();
+
+ // copyText and pasteText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+
+ await testCopyText(acc, 0, 1, aBefore.length, browser, "h");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhello", aAfter]);
+
+ await testCopyText(acc, 5, 6, aBefore.length, browser, "o");
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhelloo", aAfter]);
+
+ await testCopyText(acc, 2, 3, aBefore.length, browser, "e");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hehelloo", aAfter]);
+
+ // cut & paste
+ await testCutText(acc, 0, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehelloo", aAfter]);
+ await testPasteText(acc, 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhelloo", aAfter]);
+
+ await testCutText(acc, 3, 4, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloo", aAfter]);
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloeo", aAfter]);
+
+ await testCutText(acc, 0, 8, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ await resetInput();
+
+ // ////////////////////////////////////////////////////////////////////////
+ // setTextContents
+ await testSetTextContents(acc, "hello", aBefore.length, [
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testSetTextContents(acc, "katze", aBefore.length, [
+ EVENT_TEXT_REMOVED,
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "katze", aAfter]);
+}
+
+addAccessibleTask(
+ `<input id="input"/>`,
+ async function (browser, docAcc) {
+ await testEditable(browser, findAccessibleChildByID(docAcc, "input"));
+ },
+ { chrome: true, topLevel: true }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::after {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "",
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "before";
+ }
+ #input::after {
+ content: "after";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "before",
+ "after"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ ``,
+ async function (browser, docAcc) {
+ await testEditable(browser, docAcc);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ contentDocBodyAttrs: { contentEditable: "true" },
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text.js b/accessible/tests/browser/text/browser_text.js
new file mode 100644
index 0000000000..79909ee412
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text.js
@@ -0,0 +1,326 @@
+/* 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";
+
+/* import-globals-from ../../mochitest/attributes.js */
+/* import-globals-from ../../mochitest/text.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test line and word offsets for various cases for both local and remote
+ * Accessibles. There is more extensive coverage in ../../mochitest/text. These
+ * tests don't need to duplicate all of that, since much of the underlying code
+ * is unified. They should ensure that the cache works as expected and that
+ * there is consistency between local and remote.
+ */
+addAccessibleTask(
+ `
+<p id="br">ab cd<br>ef gh</p>
+<pre id="pre">ab cd
+ef gh</pre>
+<p id="linksStartEnd"><a href="https://example.com/">a</a>b<a href="https://example.com/">c</a></p>
+<p id="linksBreaking">a<a href="https://example.com/">b<br>c</a>d</p>
+<p id="p">a<br role="presentation">b</p>
+<p id="leafThenWrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span>a</span>bc</p>
+ `,
+ async function (browser, docAcc) {
+ for (const id of ["br", "pre"]) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ testCharacterCount([acc], 11);
+ testTextAtOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd\n", 0, 6],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ef gh", 6, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "ab cd", 0, 5],
+ [6, 11, "\nef gh", 5, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd", 0, 5],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "\nef gh", 5, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "ab ", 0, 3],
+ [3, 5, "cd\n", 3, 6],
+ [6, 8, "ef ", 6, 9],
+ [9, 11, "gh", 9, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab ", 0, 3],
+ [6, 8, "cd\n", 3, 6],
+ [9, 11, "ef ", 6, 9],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "cd\n", 3, 6],
+ [3, 5, "ef ", 6, 9],
+ [6, 8, "gh", 9, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_END, [
+ [0, 1, "ab", 0, 2],
+ [2, 4, " cd", 2, 5],
+ [5, 7, "\nef", 5, 8],
+ [8, 11, " gh", 8, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab", 0, 2],
+ // See below for offset 6.
+ [7, 8, " cd", 2, 5],
+ [9, 11, "\nef", 5, 8],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [[6, 6, " cd", 2, 5]]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, " cd", 2, 5],
+ [3, 5, "\nef", 5, 8],
+ [6, 8, " gh", 8, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_PARAGRAPH, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ }
+ const linksStartEnd = findAccessibleChildByID(docAcc, "linksStartEnd");
+ testTextAtOffset(linksStartEnd, BOUNDARY_LINE_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ testTextAtOffset(linksStartEnd, BOUNDARY_WORD_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ const linksBreaking = findAccessibleChildByID(docAcc, "linksBreaking");
+ testTextAtOffset(linksBreaking, BOUNDARY_LINE_START, [
+ [0, 0, `a${kEmbedChar}`, 0, 2],
+ [1, 1, `a${kEmbedChar}d`, 0, 3],
+ [2, 3, `${kEmbedChar}d`, 1, 3],
+ ]);
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [
+ [0, 0, "a", 0, 1],
+ [1, 2, "b", 1, 2],
+ ]);
+ testTextAtOffset(p, BOUNDARY_PARAGRAPH, [[0, 2, "ab", 0, 2]]);
+ const leafThenWrap = findAccessibleChildByID(docAcc, "leafThenWrap");
+ testTextAtOffset(leafThenWrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "c", 2, 3],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test line offsets after text mutation.
+ */
+addAccessibleTask(
+ `
+<p id="initBr"><br></p>
+<p id="rewrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span id="rewrap1">ac</span>def</p>
+ `,
+ async function (browser, docAcc) {
+ const initBr = findAccessibleChildByID(docAcc, "initBr");
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 0, "\n", 0, 1],
+ [1, 1, "", 1, 1],
+ ]);
+ info("initBr: Inserting text before br");
+ let reordered = waitForEvent(EVENT_REORDER, initBr);
+ await invokeContentTask(browser, [], () => {
+ const initBrNode = content.document.getElementById("initBr");
+ initBrNode.insertBefore(
+ content.document.createTextNode("a"),
+ initBrNode.firstElementChild
+ );
+ });
+ await reordered;
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 1, "a\n", 0, 2],
+ [2, 2, "", 2, 2],
+ ]);
+
+ const rewrap = findAccessibleChildByID(docAcc, "rewrap");
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ac", 0, 2],
+ [2, 3, "de", 2, 4],
+ [4, 5, "f", 4, 5],
+ ]);
+ info("rewrap: Changing ac to abc");
+ reordered = waitForEvent(EVENT_REORDER, rewrap);
+ await invokeContentTask(browser, [], () => {
+ const rewrap1 = content.document.getElementById("rewrap1");
+ rewrap1.textContent = "abc";
+ });
+ await reordered;
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "cd", 2, 4],
+ [4, 6, "ef", 4, 6],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test retrieval of text offsets when an invalid offset is given.
+ */
+addAccessibleTask(
+ `<p id="p">test</p>`,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextBeforeOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextAfterOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ },
+ {
+ // The old HyperTextAccessible implementation doesn't crash, but it returns
+ // different offsets. This doesn't matter because they're invalid either
+ // way. Since the new HyperTextAccessibleBase implementation is all we will
+ // have soon, just test that.
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods.
+ */
+addAccessibleTask(
+ `<div id="container">a<a id="link" href="https://example.com/">b</a>c</div>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container", [
+ nsIAccessibleHyperText,
+ ]);
+ is(container.linkCount, 1, "container linkCount is 1");
+ let link = container.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible, nsIAccessibleHyperText]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(container.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 1, "link's startIndex is 1");
+ is(link.endIndex, 2, "link's endIndex is 2");
+ is(container.getLinkIndexAtOffset(1), 0, "getLinkIndexAtOffset(1) is 0");
+ is(container.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ is(link.linkCount, 0, "link linkCount is 0");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods near a list bullet.
+ */
+addAccessibleTask(
+ `<ul><li id="li"><a id="link" href="https://example.com/">a</a></li></ul>`,
+ async function (browser, docAcc) {
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleHyperText]);
+ let link = li.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(li.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 2, "link's startIndex is 2");
+ is(li.getLinkIndexAtOffset(2), 0, "getLinkIndexAtOffset(2) is 0");
+ is(li.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/**
+ * Test text attribute methods.
+ */
+addAccessibleTask(
+ `
+<p id="plain">ab</p>
+<p id="bold" style="font-weight: bold;">ab</p>
+<p id="partialBold">ab<b>cd</b>ef</p>
+<p id="consecutiveBold">ab<b>cd</b><b>ef</b>gh</p>
+<p id="embeddedObjs">ab<a href="https://example.com/">cd</a><a href="https://example.com/">ef</a><a href="https://example.com/">gh</a>ij</p>
+<p id="empty"></p>
+<p id="fontFamilies" style="font-family: sans-serif;">ab<span style="font-family: monospace;">cd</span><span style="font-family: monospace;">ef</span>gh</p>
+ `,
+ async function (browser, docAcc) {
+ let defAttrs = {
+ "text-position": "baseline",
+ "font-style": "normal",
+ "font-weight": "400",
+ };
+
+ const plain = findAccessibleChildByID(docAcc, "plain");
+ testDefaultTextAttrs(plain, defAttrs, true);
+ for (let offset = 0; offset <= 2; ++offset) {
+ testTextAttrs(plain, offset, {}, defAttrs, 0, 2, true);
+ }
+
+ const bold = findAccessibleChildByID(docAcc, "bold");
+ defAttrs["font-weight"] = "700";
+ testDefaultTextAttrs(bold, defAttrs, true);
+ testTextAttrs(bold, 0, {}, defAttrs, 0, 2, true);
+
+ const partialBold = findAccessibleChildByID(docAcc, "partialBold");
+ defAttrs["font-weight"] = "400";
+ testDefaultTextAttrs(partialBold, defAttrs, true);
+ testTextAttrs(partialBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(partialBold, 2, boldAttrs, defAttrs, 2, 4, true);
+ testTextAttrs(partialBold, 4, {}, defAttrs, 4, 6, true);
+
+ const consecutiveBold = findAccessibleChildByID(docAcc, "consecutiveBold");
+ testDefaultTextAttrs(consecutiveBold, defAttrs, true);
+ testTextAttrs(consecutiveBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(consecutiveBold, 2, boldAttrs, defAttrs, 2, 6, true);
+ testTextAttrs(consecutiveBold, 6, {}, defAttrs, 6, 8, true);
+
+ const embeddedObjs = findAccessibleChildByID(docAcc, "embeddedObjs");
+ testDefaultTextAttrs(embeddedObjs, defAttrs, true);
+ testTextAttrs(embeddedObjs, 0, {}, defAttrs, 0, 2, true);
+ for (let offset = 2; offset <= 4; ++offset) {
+ // attrs and defAttrs should be completely empty, so we pass
+ // false for aSkipUnexpectedAttrs.
+ testTextAttrs(embeddedObjs, offset, {}, {}, 2, 5, false);
+ }
+ testTextAttrs(embeddedObjs, 5, {}, defAttrs, 5, 7, true);
+
+ const empty = findAccessibleChildByID(docAcc, "empty");
+ testDefaultTextAttrs(empty, defAttrs, true);
+ testTextAttrs(empty, 0, {}, defAttrs, 0, 0, true);
+
+ const fontFamilies = findAccessibleChildByID(docAcc, "fontFamilies", [
+ nsIAccessibleHyperText,
+ ]);
+ testDefaultTextAttrs(fontFamilies, defAttrs, true);
+ testTextAttrs(fontFamilies, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(fontFamilies, 2, {}, defAttrs, 2, 6, true);
+ testTextAttrs(fontFamilies, 6, {}, defAttrs, 6, 8, true);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_caret.js b/accessible/tests/browser/text/browser_text_caret.js
new file mode 100644
index 0000000000..e0cea334d6
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_caret.js
@@ -0,0 +1,452 @@
+/* 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";
+
+/* import-globals-from ../../mochitest/text.js */
+
+/**
+ * Test caret retrieval.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea"
+ spellcheck="false"
+ style="scrollbar-width: none; font-family: 'Liberation Mono', monospace;"
+ cols="6">ab cd e</textarea>
+<textarea id="empty"></textarea>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ let evt = await caretMoved;
+ is(textarea.caretOffset, 0, "Initial caret offset is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "a",
+ 0,
+ 1,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 1, "Caret offset is 1 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "b",
+ 1,
+ 2,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 2, "Caret offset is 2 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 2,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 3, "Caret offset is 3 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "c",
+ 3,
+ 4,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 4, "Caret offset is 4 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "d",
+ 4,
+ 5,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 5, "Caret offset is 5 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 5,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset is 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 6,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset remains 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ // Caret is at start of second line.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 7, "Caret offset is 7 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ // Caret is at end of textarea.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 7,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ const empty = findAccessibleChildByID(docAcc, "empty", [nsIAccessibleText]);
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, empty);
+ empty.takeFocus();
+ evt = await caretMoved;
+ is(empty.caretOffset, 0, "Caret offset in empty textarea is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test setting the caret.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab\nc</textarea>
+<div id="editable" contenteditable>
+ <p id="p">a<a id="link" href="https://example.com/">b</a></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("textarea: Set caret offset to 0");
+ let focused = waitForEvent(EVENT_FOCUS, textarea);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(textarea.caretOffset, 0, "textarea caret correct");
+ // Test setting caret to another line.
+ info("textarea: Set caret offset to 3");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 3;
+ await caretMoved;
+ is(textarea.caretOffset, 3, "textarea caret correct");
+ // Test setting caret to the end.
+ info("textarea: Set caret offset to 4 (end)");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 4;
+ await caretMoved;
+ is(textarea.caretOffset, 4, "textarea caret correct");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ focused = waitForEvent(EVENT_FOCUS, editable);
+ editable.takeFocus();
+ await focused;
+ const p = findAccessibleChildByID(docAcc, "p", [nsIAccessibleText]);
+ info("p: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p);
+ p.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(p.caretOffset, 0, "p caret correct");
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ info("link: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, link);
+ link.caretOffset = 0;
+ await caretMoved;
+ is(link.caretOffset, 0, "link caret correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/text/browser_text_paragraph_boundary.js b/accessible/tests/browser/text/browser_text_paragraph_boundary.js
new file mode 100644
index 0000000000..04e64520e8
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_paragraph_boundary.js
@@ -0,0 +1,22 @@
+/* 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";
+
+// Test that we don't crash the parent process when querying the paragraph
+// boundary on an Accessible which has remote ProxyAccessible descendants.
+addAccessibleTask(
+ `test`,
+ async function testParagraphBoundaryWithRemoteDescendants(browser, accDoc) {
+ const root = getRootAccessible(document).QueryInterface(
+ Ci.nsIAccessibleText
+ );
+ let start = {};
+ let end = {};
+ // The offsets will change as the Firefox UI changes. We don't really care
+ // what they are, just that we don't crash.
+ root.getTextAtOffset(0, nsIAccessibleText.BOUNDARY_PARAGRAPH, start, end);
+ ok(true, "Getting paragraph boundary succeeded");
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_selection.js b/accessible/tests/browser/text/browser_text_selection.js
new file mode 100644
index 0000000000..3b47d5f36e
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_selection.js
@@ -0,0 +1,344 @@
+/* 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";
+
+/* import-globals-from ../../mochitest/text.js */
+
+function waitForSelectionChange(selectionAcc, caretAcc) {
+ if (!caretAcc) {
+ caretAcc = selectionAcc;
+ }
+ return waitForEvents(
+ [
+ [EVENT_TEXT_SELECTION_CHANGED, selectionAcc],
+ // We must swallow the caret events as well to avoid confusion with later,
+ // unrelated caret events.
+ [EVENT_TEXT_CARET_MOVED, caretAcc],
+ ],
+ true
+ );
+}
+
+function changeDomSelection(
+ browser,
+ anchorId,
+ anchorOffset,
+ focusId,
+ focusOffset
+) {
+ return invokeContentTask(
+ browser,
+ [anchorId, anchorOffset, focusId, focusOffset],
+ (
+ contentAnchorId,
+ contentAnchorOffset,
+ contentFocusId,
+ contentFocusOffset
+ ) => {
+ // We want the text node, so we use firstChild.
+ content.window
+ .getSelection()
+ .setBaseAndExtent(
+ content.document.getElementById(contentAnchorId).firstChild,
+ contentAnchorOffset,
+ content.document.getElementById(contentFocusId).firstChild,
+ contentFocusOffset
+ );
+ }
+ );
+}
+
+function testSelectionRange(
+ browser,
+ root,
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+) {
+ let selRange = root.selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
+ testTextRange(
+ selRange,
+ getAccessibleDOMNodeID(root),
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+ );
+}
+
+/**
+ * Test text selection via keyboard.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab</textarea>
+<div id="editable" contenteditable>
+ <p id="p1">a</p>
+ <p id="p2">bc</p>
+ <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ queryInterfaces(docAcc, [nsIAccessibleText]);
+
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in textarea");
+ let selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Selecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 2);
+ testTextGetSelection(textarea, 0, 2, 0);
+
+ info("Unselecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Unselecting a in textarea");
+ // We don't fire selection changed when the selection collapses.
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]);
+ info("Focusing editable, caret to start");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 0);
+ await caretMoved;
+ testSelectionRange(browser, editable, p1, 0, p1, 0);
+ is(editable.selectionCount, 0, "editable selectionCount is 0");
+ is(p1.selectionCount, 0, "p1 selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p1, 1);
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ const p2 = findAccessibleChildByID(docAcc, "p2", [nsIAccessibleText]);
+ if (browser.isRemoteBrowser) {
+ is(p2.selectionCount, 0, "p2 selectionCount is 0");
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+
+ // Selecting across two Accessibles with only a partial selection in the
+ // second.
+ info("Selecting ab in editable");
+ selChanged = waitForSelectionChange(editable, p2);
+ await changeDomSelection(browser, "p1", 0, "p2", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p2, 1);
+ testTextGetSelection(editable, 0, 2, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ testTextGetSelection(p2, 0, 1, 0);
+
+ const pWithLink = findAccessibleChildByID(docAcc, "pWithLink", [
+ nsIAccessibleText,
+ ]);
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ // Selecting both text and a link.
+ info("Selecting de in editable");
+ selChanged = waitForSelectionChange(pWithLink, link);
+ await changeDomSelection(browser, "pWithLink", 0, "link", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, link, 1);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 2, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Selecting a link and text on either side.
+ info("Selecting def in editable");
+ selChanged = waitForSelectionChange(pWithLink, pWithLink);
+ await changeDomSelection(browser, "pWithLink", 0, "textAfterLink", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, pWithLink, 3);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 3, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Noncontiguous selection.
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ info("Adding c to selection in editable");
+ selChanged = waitForSelectionChange(p2);
+ await invokeContentTask(browser, [], () => {
+ const r = content.document.createRange();
+ const p2text = content.document.getElementById("p2").firstChild;
+ r.setStart(p2text, 0);
+ r.setEnd(p2text, 1);
+ content.window.getSelection().addRange(r);
+ });
+ await selChanged;
+ let selRanges = editable.selectionRanges;
+ is(selRanges.length, 2, "2 selection ranges");
+ testTextRange(
+ selRanges.queryElementAt(0, nsIAccessibleTextRange),
+ "range 0",
+ p1,
+ 0,
+ p1,
+ 1
+ );
+ testTextRange(
+ selRanges.queryElementAt(1, nsIAccessibleTextRange),
+ "range 1",
+ p2,
+ 0,
+ p2,
+ 1
+ );
+ is(editable.selectionCount, 2, "editable selectionCount is 2");
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(editable, 1, 2, 1);
+ if (browser.isRemoteBrowser) {
+ is(p1.selectionCount, 1, "p1 selectionCount is 1");
+ testTextGetSelection(p1, 0, 1, 0);
+ is(p2.selectionCount, 1, "p2 selectionCount is 1");
+ testTextGetSelection(p2, 0, 1, 0);
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Tabbing to an input selects all its text. Test that the cached selection
+ *reflects this. This has to be done separately from the other selection tests
+ * because prior contentEditable selection changes the events that get fired.
+ */
+addAccessibleTask(
+ `
+<button id="before">Before</button>
+<input id="input" value="test">
+ `,
+ async function (browser, docAcc) {
+ // The tab order is different when there's an iframe, so focus a control
+ // before the input to make tab consistent.
+ info("Focusing before");
+ const before = findAccessibleChildByID(docAcc, "before");
+ // Focusing a button fires a selection event. We must swallow this to
+ // avoid confusing the later test.
+ let events = waitForOrderedEvents([
+ [EVENT_FOCUS, before],
+ [EVENT_TEXT_SELECTION_CHANGED, docAcc],
+ ]);
+ before.takeFocus();
+ await events;
+
+ const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
+ info("Tabbing to input");
+ events = waitForEvents(
+ {
+ expected: [
+ [EVENT_FOCUS, input],
+ [EVENT_TEXT_SELECTION_CHANGED, input],
+ ],
+ unexpected: [[EVENT_TEXT_SELECTION_CHANGED, docAcc]],
+ },
+ "input",
+ false,
+ (args, task) => invokeContentTask(browser, args, task)
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ await events;
+ testSelectionRange(browser, input, input, 0, input, 4);
+ testTextGetSelection(input, 0, 4, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test text selection via API.
+ */
+addAccessibleTask(
+ `
+ <p id="paragraph">hello world</p>
+ <ol>
+ <li id="li">Number one</li>
+ </ol>
+ `,
+ async function (browser, docAcc) {
+ const paragraph = findAccessibleChildByID(docAcc, "paragraph", [
+ nsIAccessibleText,
+ ]);
+
+ let selChanged = waitForSelectionChange(paragraph);
+ paragraph.setSelectionBounds(0, 2, 4);
+ await selChanged;
+ testTextGetSelection(paragraph, 2, 4, 0);
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.addSelection(6, 10);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 1);
+ is(paragraph.selectionCount, 2, "paragraph selectionCount is 2");
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.removeSelection(0);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 0);
+ is(paragraph.selectionCount, 1, "paragraph selectionCount is 1");
+
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
+
+ selChanged = waitForSelectionChange(li);
+ li.setSelectionBounds(0, 1, 8);
+ await selChanged;
+ testTextGetSelection(li, 3, 8, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_spelling.js b/accessible/tests/browser/text/browser_text_spelling.js
new file mode 100644
index 0000000000..14c5c16be4
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_spelling.js
@@ -0,0 +1,151 @@
+/* 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";
+
+/* import-globals-from ../../mochitest/text.js */
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts(
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/*
+ * Given a text accessible and a list of ranges
+ * check if those ranges match the misspelled ranges in the accessible.
+ */
+function misspelledRangesMatch(acc, ranges) {
+ let offset = 0;
+ let expectedRanges = [...ranges];
+ let charCount = acc.characterCount;
+ while (offset < charCount) {
+ let start = {};
+ let end = {};
+ let attributes = acc.getTextAttributes(false, offset, start, end);
+ offset = end.value;
+ try {
+ if (attributes.getStringProperty("invalid") == "spelling") {
+ let expected = expectedRanges.shift();
+ if (
+ !expected ||
+ expected[0] != start.value ||
+ expected[1] != end.value
+ ) {
+ return false;
+ }
+ }
+ } catch (err) {}
+ }
+
+ return !expectedRanges.length;
+}
+
+/*
+ * Returns a promise that resolves after a text attribute changed event
+ * brings us to a state where the misspelled ranges match.
+ */
+async function waitForMisspelledRanges(acc, ranges) {
+ await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED);
+ await untilCacheOk(
+ () => misspelledRangesMatch(acc, ranges),
+ `Misspelled ranges match: ${JSON.stringify(ranges)}`
+ );
+}
+
+/**
+ * Test spelling errors.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea" spellcheck="true">test tset tset test</textarea>
+<div contenteditable id="editable" spellcheck="true">plain<span> ts</span>et <b>bold</b></div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ textarea.takeFocus();
+ await spellingChanged;
+
+ // Test removal of a spelling error.
+ info('textarea: Changing first "tset" to "test"');
+ // setTextRange fires multiple EVENT_TEXT_ATTRIBUTE_CHANGED, so replace by
+ // selecting and typing instead.
+ spellingChanged = waitForMisspelledRanges(textarea, [[10, 14]]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("test");
+ // Move the cursor to trigger spell check.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Test addition of a spelling error.
+ info('textarea: Changing it back to "tset"');
+ spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("tset");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Ensure that changing the text without changing any spelling errors
+ // correctly updates offsets.
+ info('textarea: Changing first "test" to "the"');
+ // Spelling errors don't change, so we won't get
+ // EVENT_TEXT_ATTRIBUTE_CHANGED. We change the text, wait for the insertion
+ // and then select a character so we know when the change is done.
+ let inserted = waitForEvent(EVENT_TEXT_INSERTED, textarea);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(0, 4);
+ });
+ EventUtils.sendString("the");
+ await inserted;
+ let selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selected;
+ const expectedRanges = [
+ [4, 8],
+ [9, 13],
+ ];
+ await untilCacheOk(
+ () => misspelledRangesMatch(textarea, expectedRanges),
+ `Misspelled ranges match: ${JSON.stringify(expectedRanges)}`
+ );
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing editable");
+ spellingChanged = waitForMisspelledRanges(editable, [[6, 10]]);
+ editable.takeFocus();
+ await spellingChanged;
+ // Test normal text and spelling errors crossing text nodes.
+ testTextAttrs(editable, 0, {}, {}, 0, 6, true); // "plain "
+ // Ensure we detect the spelling error even though there is a style change
+ // after it.
+ testTextAttrs(editable, 6, { invalid: "spelling" }, {}, 6, 10, true); // "tset"
+ testTextAttrs(editable, 10, {}, {}, 10, 11, true); // " "
+ // Ensure a style change is still detected in the presence of a spelling
+ // error.
+ testTextAttrs(editable, 11, boldAttrs, {}, 11, 15, true); // "bold"
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_textleafpoint.js b/accessible/tests/browser/text/browser_textleafpoint.js
new file mode 100644
index 0000000000..344a01e2d3
--- /dev/null
+++ b/accessible/tests/browser/text/browser_textleafpoint.js
@@ -0,0 +1,524 @@
+/* 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";
+
+/* import-globals-from ../../mochitest/text.js */
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+h<a href="#">id i</a>n
+the <strong>big</strong> rug.</p>
+`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ let charSequence = [
+ ...textBoundaryGenerator(firstPoint, BOUNDARY_CHAR, DIRECTION_NEXT),
+ ];
+
+ testPointEqual(
+ firstPoint,
+ charSequence[0],
+ "Point constructed via container and offset 0 is first character point."
+ );
+ testPointEqual(
+ lastPoint,
+ charSequence[charSequence.length - 1],
+ "Point constructed via container and kTextEndOffset is last character point."
+ );
+
+ const expectedCharSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 1],
+ ["A bug\nh", 2],
+ ["A bug\nh", 3],
+ ["A bug\nh", 4],
+ ["A bug\nh", 5],
+ ["A bug\nh", 6],
+ ["id i", 0],
+ ["id i", 1],
+ ["id i", 2],
+ ["id i", 3],
+ ["n\nthe ", 0],
+ ["n\nthe ", 1],
+ ["n\nthe ", 2],
+ ["n\nthe ", 3],
+ ["n\nthe ", 4],
+ ["n\nthe ", 5],
+ ["big", 0],
+ ["big", 1],
+ ["big", 2],
+ [" rug.", 0],
+ [" rug.", 1],
+ [" rug.", 2],
+ [" rug.", 3],
+ [" rug.", 4],
+ [" rug.", 5],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR sequence is correct"
+ );
+
+ const expectedWordStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 2],
+ ["A bug\nh", 6],
+ ["id i", 3],
+ ["n\nthe ", 2],
+ ["big", 0],
+ [" rug.", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ const expectedWordEndSequence = [
+ ["A bug\nh", 1],
+ ["A bug\nh", 5],
+ ["id i", 2],
+ ["n\nthe ", 1],
+ ["n\nthe ", 5],
+ [" rug.", 0],
+ [" rug.", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedWordEndSequence].reverse(),
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ const expectedLineStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 6],
+ ["n\nthe ", 2],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ const expectedLineEndSequence = [
+ ["A bug\nh", 5],
+ ["n\nthe ", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineEndSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_END sequence is correct",
+ { todo: true }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedLineEndSequence].reverse(),
+ "Backward BOUNDARY_LINE_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `<p id="p">
+ Rob ca<input id="i1" value="n m">op up.
+ </p>`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 1],
+ ["Rob ca", 2],
+ ["Rob ca", 3],
+ ["Rob ca", 4],
+ ["Rob ca", 5],
+ ["n m", 0],
+ ["n m", 1],
+ ["n m", 2],
+ ["n m", 3],
+ ],
+ "Forward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [
+ ["op up. ", 7],
+ ["op up. ", 6],
+ ["op up. ", 5],
+ ["op up. ", 4],
+ ["op up. ", 3],
+ ["op up. ", 2],
+ ["op up. ", 1],
+ ["op up. ", 0],
+ ["n m", 2],
+ ["n m", 1],
+ ["n m", 0],
+ ],
+ "Backward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 4],
+ ["n m", 2],
+ ],
+ "Forward BOUNDARY_WORD_START sequence when stopping in editable is correct",
+ {
+ flags: BOUNDARY_FLAG_STOP_IN_EDITABLE,
+ todo: true, // Shouldn't consider end of input a word start
+ }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+on a <span style="display: block;">rug</span></p>
+ <p id="p2">
+ Numbers:
+ </p>
+ <ul>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedParagraphStart = [
+ ["A bug\non a ", 0],
+ ["A bug\non a ", 6],
+ ["rug", 0],
+ ["Numbers: ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+
+ const paragraphStart = createTextLeafPoint(
+ findAccessibleChildByID(docAcc, "p2").firstChild,
+ 0
+ );
+ const wordEnd = paragraphStart.findBoundary(
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ testPointEqual(
+ wordEnd,
+ paragraphStart,
+ "The word end from the previous block is the first point in this block"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+// Test for skipping list item bullets.
+addAccessibleTask(
+ `<ul>
+ <li>One</li>
+ <li>Two</li>
+ <li style="white-space: pre-line;">Three
+Four</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const firstNonMarkerPoint = firstPoint.findBoundary(
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ Assert.deepEqual(
+ readablePoint(firstNonMarkerPoint),
+ ["One", 0],
+ "First non-marker point is correct"
+ );
+
+ const expectedParagraphStart = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_PREVIOUS,
+ [...expectedParagraphStart].reverse(),
+ "Backward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedCharSequence = [
+ ["One", 0],
+ ["One", 1],
+ ["One", 2],
+ ["Two", 0],
+ ["Two", 1],
+ ["Two", 2],
+ ["Three\nFour", 0],
+ ["Three\nFour", 1],
+ ["Three\nFour", 2],
+ ["Three\nFour", 3],
+ ["Three\nFour", 4],
+ ["Three\nFour", 5],
+ ["Three\nFour", 6],
+ ["Three\nFour", 7],
+ ["Three\nFour", 8],
+ ["Three\nFour", 9],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordEndSequence = [
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 5],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [
+ readablePoint(firstNonMarkerPoint),
+ ...expectedWordEndSequence,
+ ].reverse(),
+ "Backward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedLineStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ // Add last point in doc
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test the paragraph boundary on tables.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th>a</th><td>b</td></tr>
+ <tr><td>c</td><td>d</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [["a", 0], ["b", 0], ["c", 0], ["d", 0], readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/*
+ * Test the word boundary with punctuation character
+ */
+addAccessibleTask(
+ `
+<p>ab'cd</p>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedWordStartSequence = [
+ ["ab'cd", 0],
+ ["ab'cd", 3],
+ ["ab'cd", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ expectedWordStartSequence,
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+ const expectedWordEndSequence = [
+ ["ab'cd", 5],
+ ["ab'cd", 3],
+ ["ab'cd", 0],
+ ];
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ expectedWordEndSequence,
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
diff --git a/accessible/tests/browser/text/head.js b/accessible/tests/browser/text/head.js
new file mode 100644
index 0000000000..fa4b095892
--- /dev/null
+++ b/accessible/tests/browser/text/head.js
@@ -0,0 +1,276 @@
+/* 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";
+
+/* exported createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS,
+ BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN,
+ BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER,
+ readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence,
+ isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText,
+ testCopyText, testPasteText, testCutText, testSetTextContents */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+
+/* import-globals-from ../../mochitest/role.js */
+
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+const DIRECTION_NEXT = Ci.nsIAccessibleTextLeafPoint.DIRECTION_NEXT;
+const DIRECTION_PREVIOUS = Ci.nsIAccessibleTextLeafPoint.DIRECTION_PREVIOUS;
+
+const BOUNDARY_FLAG_DEFAULT =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_DEFAULT;
+const BOUNDARY_FLAG_INCLUDE_ORIGIN =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_INCLUDE_ORIGIN;
+const BOUNDARY_FLAG_STOP_IN_EDITABLE =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_STOP_IN_EDITABLE;
+const BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER;
+
+function createTextLeafPoint(acc, offset) {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ nsIAccessibilityService
+ );
+
+ return accService.createTextLeafPoint(acc, offset);
+}
+
+// Converts an nsIAccessibleTextLeafPoint into a human/machine
+// readable tuple with a readable accessible and the offset within it.
+// For a point text leaf it would look like this: ["hello", 2],
+// For a point in an empty input it would look like this ["input#name", 0]
+function readablePoint(point) {
+ const readableLeaf = acc => {
+ let tagName = getAccessibleTagName(acc);
+ if (tagName && !tagName.startsWith("_moz_generated")) {
+ let domNodeID = getAccessibleDOMNodeID(acc);
+ if (domNodeID) {
+ return `${tagName}#${domNodeID}`;
+ }
+ return tagName;
+ }
+
+ return acc.name;
+ };
+
+ return [readableLeaf(point.accessible), point.offset];
+}
+
+function sequenceEqual(val, expected, msg) {
+ Assert.deepEqual(val, expected, msg);
+}
+
+// eslint-disable-next-line camelcase
+function sequenceEqualTodo(val, expected, msg) {
+ todo_is(JSON.stringify(val), JSON.stringify(expected), msg);
+}
+
+function pointsEqual(pointA, pointB) {
+ return (
+ pointA.offset == pointB.offset && pointA.accessible == pointB.accessible
+ );
+}
+
+function testPointEqual(pointA, pointB, msg) {
+ is(pointA.offset, pointB.offset, `Offset mismatch - ${msg}`);
+ is(pointA.accessible, pointB.accessible, `Accessible mismatch - ${msg}`);
+}
+
+function* textBoundaryGenerator(
+ firstPoint,
+ boundaryType,
+ direction,
+ flags = BOUNDARY_FLAG_DEFAULT
+) {
+ // Our start point should be inclusive of the given point.
+ let nextLeafPoint = firstPoint.findBoundary(
+ boundaryType,
+ direction,
+ flags | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ let textLeafPoint = null;
+
+ do {
+ textLeafPoint = nextLeafPoint;
+ yield textLeafPoint;
+ nextLeafPoint = textLeafPoint.findBoundary(boundaryType, direction, flags);
+ } while (!pointsEqual(textLeafPoint, nextLeafPoint));
+}
+
+// This function takes FindBoundary arguments and an expected sequence
+// of boundary points formatted with readablePoint.
+// For example, word starts would look like this:
+// [["one two", 0], ["one two", 4], ["one two", 7]]
+function testBoundarySequence(
+ startPoint,
+ boundaryType,
+ direction,
+ expectedSequence,
+ msg,
+ options = {}
+) {
+ let sequence = [
+ ...textBoundaryGenerator(
+ startPoint,
+ boundaryType,
+ direction,
+ options.flags ? options.flags : BOUNDARY_FLAG_DEFAULT
+ ),
+ ];
+ (options.todo ? sequenceEqualTodo : sequenceEqual)(
+ sequence.map(readablePoint),
+ expectedSequence,
+ msg
+ );
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Editable text
+
+async function waitForCopy(browser) {
+ await BrowserTestUtils.waitForContentEvent(browser, "copy", false, evt => {
+ return true;
+ });
+
+ let clipboardData = await invokeContentTask(browser, [], async () => {
+ let text = await content.navigator.clipboard.readText();
+ return text;
+ });
+
+ return clipboardData;
+}
+
+async function isFinalValueCorrect(
+ browser,
+ acc,
+ expectedTextLeafs,
+ msg = "Value is correct"
+) {
+ let value =
+ acc.role == ROLE_ENTRY
+ ? acc.value
+ : await invokeContentTask(browser, [], () => {
+ return content.document.body.textContent;
+ });
+
+ let [before, text, after] = expectedTextLeafs;
+ let finalValue =
+ before && after && !text
+ ? [before, after].join(" ")
+ : [before, text, after].join("");
+
+ is(value.replace("\xa0", " "), finalValue, msg);
+}
+
+function waitForTextChangeEvents(acc, eventSeq) {
+ let events = eventSeq.map(eventType => {
+ return [eventType, acc];
+ });
+
+ if (acc.role == ROLE_ENTRY) {
+ events.push([EVENT_TEXT_VALUE_CHANGE, acc]);
+ }
+
+ return waitForEvents(events);
+}
+
+async function testSetTextContents(acc, text, staticContentOffset, events) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, events);
+ acc.setTextContents(text);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset);
+}
+
+async function testInsertText(
+ acc,
+ textToInsert,
+ insertOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.insertText(textToInsert, staticContentOffset + insertOffset);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testDeleteText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.deleteText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}
+
+async function testCopyText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset,
+ browser,
+ aExpectedClipboard = null
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let copied = waitForCopy(browser);
+ acc.copyText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let clipboardText = await copied;
+ if (aExpectedClipboard != null) {
+ is(clipboardText, aExpectedClipboard, "Correct text in clipboard");
+ }
+}
+
+async function testPasteText(acc, insertOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.pasteText(staticContentOffset + insertOffset);
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ // XXX: In non-headless mode pasting text produces several text leaves
+ // and the offset is not what we expect.
+ // is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testCutText(acc, startOffset, endOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.cutText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}