diff options
Diffstat (limited to 'accessible/tests/browser/text')
-rw-r--r-- | accessible/tests/browser/text/browser.ini | 18 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_editabletext.js | 173 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_text.js | 326 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_text_caret.js | 452 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_text_paragraph_boundary.js | 22 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_text_selection.js | 344 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_text_spelling.js | 151 | ||||
-rw-r--r-- | accessible/tests/browser/text/browser_textleafpoint.js | 485 | ||||
-rw-r--r-- | accessible/tests/browser/text/head.js | 276 |
9 files changed, 2247 insertions, 0 deletions
diff --git a/accessible/tests/browser/text/browser.ini b/accessible/tests/browser/text/browser.ini new file mode 100644 index 0000000000..1b0c5a3033 --- /dev/null +++ b/accessible/tests/browser/text/browser.ini @@ -0,0 +1,18 @@ +[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..894e982142 --- /dev/null +++ b/accessible/tests/browser/text/browser_textleafpoint.js @@ -0,0 +1,485 @@ +/* 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 } +); 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); +} |