diff options
Diffstat (limited to '')
-rw-r--r-- | widget/tests/window_composition_text_querycontent.xhtml | 10977 |
1 files changed, 10977 insertions, 0 deletions
diff --git a/widget/tests/window_composition_text_querycontent.xhtml b/widget/tests/window_composition_text_querycontent.xhtml new file mode 100644 index 0000000000..7cdbf28568 --- /dev/null +++ b/widget/tests/window_composition_text_querycontent.xhtml @@ -0,0 +1,10977 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Testing composition, text and query content events" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js" /> + + <panel id="panel" hidden="true" orient="vertical"> + <vbox id="vbox"> + <html:textarea id="textbox" cols="20" rows="4" style="font-size: 36px;"/> + </vbox> + </panel> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="display"> +<div id="div" style="margin: 0; padding: 0; font-size: 36px;">Here is a text frame.</div> +<textarea style="margin: 0; font-family: -moz-fixed;" id="textarea" cols="20" rows="4"></textarea><br/> +<iframe id="iframe" width="300" height="150" + src="data:text/html,<textarea id='textarea' cols='20' rows='4'></textarea>"></iframe><br/> +<iframe id="iframe2" width="300" height="150" + src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/> +<iframe id="iframe3" width="300" height="150" + src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/> +<iframe id="iframe4" width="300" height="150" + src="data:text/html,<div contenteditable id='contenteditable'></div>"></iframe><br/> +<!-- + NOTE: the width for the next two iframes is chosen to be small enough to make + the Show Password button (for type=password) be outside the viewport so that + it doesn't affect the rendering compared to the type=text control. + But still large enough to comfortably fit the input values we test. +--> +<iframe id="iframe5" style="width:10ch" height="50" src="data:text/html,<input id='input'>"></iframe> +<iframe id="iframe6" style="width:10ch" height="50" src="data:text/html,<input id='password' type='password'>"></iframe><br/> +<iframe id="iframe7" width="300" height="150" + src="data:text/html,<span contenteditable id='contenteditable'></span>"></iframe><br/> +<input id="input" type="text"/><br/> +<input id="password" type="password"/><br/> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function isfuzzy(aLeft, aRight, aEpsilon, aMessage) { + window.arguments[0].SimpleTest.isfuzzy(aLeft, aRight, aEpsilon, aMessage); +} + +function todo(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.todo(aCondition, aMessage); +} + +function todo_is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage); +} + +function todo_isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.todo_isnot(aLeft, aRight, aMessage); +} + +function isSimilarTo(aLeft, aRight, aAllowedDifference, aMessage) +{ + if (Math.abs(aLeft - aRight) <= aAllowedDifference) { + ok(true, aMessage); + } else { + ok(false, aMessage + ", got=" + aLeft + ", expected=" + (aRight - aAllowedDifference) + "~" + (aRight + aAllowedDifference)); + } +} + +function isGreaterThan(aLeft, aRight, aMessage) +{ + ok(aLeft > aRight, aMessage + ", got=" + aLeft + ", expected minimum value=" + aRight); +} + +/** + * synthesizeSimpleCompositionChange synthesizes a composition which has only + * one clause and put caret end of it. + * + * @param aComposition string or object. If string, it's treated as + * composition string whose attribute is + * COMPOSITION_ATTR_RAW_CLAUSE. + * If object, it must have .string whose type is "string". + * Additionally, .attr can be specified if you'd like to + * use the other attribute instead of + * COMPOSITION_ATTR_RAW_CLAUSE. + */ +function synthesizeSimpleCompositionChange(aComposition, aWindow, aCallback) { + const comp = (() => { + if (typeof aComposition == "string") { + return { string: aComposition, attr: COMPOSITION_ATTR_RAW_CLAUSE }; + } + return { + string: aComposition.string, + attr: aComposition.attr === undefined + ? COMPOSITION_ATTR_RAW_CLAUSE + : aComposition.attr + }; + })(); + synthesizeCompositionChange( + { + composition: { + string: comp.string, + clauses: [ + { length: comp.string.length, attr: comp.attr }, + ], + }, + caret: { start: comp.string.length, length: 0 }, + }, + aWindow, + aCallback + ); +} + + +var div = document.getElementById("div"); +var textarea = document.getElementById("textarea"); +var panel = document.getElementById("panel"); +var textbox = document.getElementById("textbox"); +var iframe = document.getElementById("iframe"); +var iframe2 = document.getElementById("iframe2"); +var iframe3 = document.getElementById("iframe3"); +var contenteditable; +var windowOfContenteditable; +var contenteditableBySpan; +var windowOfContenteditableBySpan; +var input = document.getElementById("input"); +var password = document.getElementById("password"); +var textareaInFrame; + +const nsITextInputProcessorCallback = Ci.nsITextInputProcessorCallback; +const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor; +const nsIWebNavigation = Ci.nsIWebNavigation; +const nsIDocShell = Ci.nsIDocShell; + +function waitForTick() { + return new Promise(resolve => { SimpleTest.executeSoon(resolve); }); +} + +async function waitForEventLoops(aTimes) +{ + for (let i = 1; i < aTimes; i++) { + await waitForTick(); + } + await new Promise(resolve => { setTimeout(resolve, 20); }); +} + +function getEditor(aNode) +{ + return aNode.editor; +} + +function getHTMLEditorIMESupport(aWindow) +{ + return aWindow.docShell.editor; +} + +const kIsWin = (navigator.platform.indexOf("Win") == 0); +const kIsMac = (navigator.platform.indexOf("Mac") == 0); + +const kLFLen = kIsWin ? 2 : 1; +const kLF = kIsWin ? "\r\n" : "\n"; + +function checkQueryContentResult(aResult, aMessage) +{ + ok(aResult, aMessage + ": the result is null"); + if (!aResult) { + return false; + } + ok(aResult.succeeded, aMessage + ": the query content failed"); + return aResult.succeeded; +} + +function checkContent(aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + let textContent = synthesizeQueryTextContent(0, 100); + if (!checkQueryContentResult(textContent, aMessage + + ": synthesizeQueryTextContent " + aID)) { + return false; + } + is(textContent.text, aExpectedText, + aMessage + ": composition string is wrong " + aID); + return textContent.text == aExpectedText; +} + +function checkContentRelativeToSelection(aRelativeOffset, aLength, aExpectedOffset, aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + aMessage += " (aRelativeOffset=" + aRelativeOffset + "): " + let textContent = synthesizeQueryTextContent(aRelativeOffset, aLength, true); + if (!checkQueryContentResult(textContent, aMessage + + "synthesizeQueryTextContent " + aID)) { + return false; + } + is(textContent.offset, aExpectedOffset, + aMessage + "offset is wrong " + aID); + is(textContent.text, aExpectedText, + aMessage + "text is wrong " + aID); + return textContent.offset == aExpectedOffset && + textContent.text == aExpectedText; +} + +function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + let selectedText = synthesizeQuerySelectedText(); + if (!checkQueryContentResult(selectedText, aMessage + + ": synthesizeQuerySelectedText " + aID)) { + return false; + } + if (aExpectedOffset === null) { + is( + selectedText.notFound, + true, + `${aMessage}: selection should not be found ${aID}` + ); + return selectedText.notFound; + } + + is( + selectedText.notFound, + false, + `${aMessage}: selection should be found ${aID}` + ); + if (selectedText.notFound) { + return false; + } + is( + selectedText.offset, + aExpectedOffset, + `${aMessage}: selection offset should be ${aExpectedOffset} ${aID}` + ); + is( + selectedText.text, + aExpectedText, + `${aMessage}: selected text should be "${aExpectedText}" ${aID}` + ); + return selectedText.offset == aExpectedOffset && + selectedText.text == aExpectedText; +} + +function checkIMESelection( + aSelectionType, + aExpectedFound, + aExpectedOffset, + aExpectedText, + aMessage, + aID, + aToDo = {} +) { + if (!aID) { + aID = ""; + } + aMessage += " (" + aSelectionType + ")"; + let { + notFound = is, + offset = is, + text = is, + } = aToDo; + let selectionType = 0; + switch (aSelectionType) { + case "RawClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT; + break; + case "SelectedRawClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT; + break; + case "ConvertedClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT; + break; + case "SelectedClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT; + break; + default: + ok(false, aMessage + ": invalid selection type, " + aSelectionType); + } + isnot(selectionType, 0, aMessage + ": wrong value"); + let selectedText = synthesizeQuerySelectedText(selectionType); + if (!checkQueryContentResult(selectedText, aMessage + + ": synthesizeQuerySelectedText " + aID)) { + return false; + } + notFound( + selectedText.notFound, + !aExpectedFound, + `${aMessage}: selection should ${ + aExpectedFound ? "" : "not" + } be found ${aID}`); + if (selectedText.notFound) { + return selectedText.notFound == !aExpectedFound; + } + + offset( + selectedText.offset, + aExpectedOffset, + `${aMessage}: selection offset is wrong ${aID}` + ); + text( + selectedText.text, + aExpectedText, + `${aMessage}: selected text is wrong ${aID}` + ); + return selectedText.offset == aExpectedOffset && + selectedText.text == aExpectedText; +} + +function checkRect(aRect, aExpectedRect, aMessage) +{ + is(aRect.left, aExpectedRect.left, aMessage + ": left is wrong"); + is(aRect.top, aExpectedRect.top, aMessage + " top is wrong"); + is(aRect.width, aExpectedRect.width, aMessage + ": width is wrong"); + is(aRect.height, aExpectedRect.height, aMessage + ": height is wrong"); + return aRect.left == aExpectedRect.left && + aRect.top == aExpectedRect.top && + aRect.width == aExpectedRect.width && + aRect.height == aExpectedRect.height; +} + +function checkRectFuzzy(aRect, aExpectedRect, aEpsilon, aMessage) { + isfuzzy(aRect.left, aExpectedRect.left, aEpsilon.left, aMessage + ": left is wrong"); + isfuzzy(aRect.top, aExpectedRect.top, aEpsilon.top, aMessage + " top is wrong"); + isfuzzy(aRect.width, aExpectedRect.width, aEpsilon.width, aMessage + ": width is wrong"); + isfuzzy(aRect.height, aExpectedRect.height, aEpsilon.height, aMessage + ": height is wrong"); + return (aRect.left >= aExpectedRect.left - aEpsilon.left && + aRect.left <= aExpectedRect.left + aEpsilon.left) && + (aRect.top >= aExpectedRect.top - aEpsilon.top && + aRect.top <= aExpectedRect.top + aEpsilon.top) && + (aRect.width >= aExpectedRect.width - aEpsilon.width && + aRect.width <= aExpectedRect.width + aEpsilon.width) && + (aRect.height >= aExpectedRect.height - aEpsilon.height && + aRect.height <= aExpectedRect.height + aEpsilon.height); +} + +function getRectArray(aQueryTextRectArrayResult) { + let rects = []; + for (let i = 0; ; i++) { + let rect = { left: {}, top: {}, width: {}, height: {} }; + try { + aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height); + } catch (e) { + break; + } + rects.push({ + left: rect.left.value, + top: rect.top.value, + width: rect.width.value, + height: rect.height.value, + }); + } + return rects; +} + +function checkRectArray(aQueryTextRectArrayResult, aExpectedTextRectArray, aMessage) +{ + for (let i = 1; i < aExpectedTextRectArray.length; ++i) { + let rect = { left: {}, top: {}, width: {}, height: {} }; + try { + aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height); + } catch (e) { + ok(false, aMessage + ": failed to retrieve " + i + "th rect (" + e + ")"); + return false; + } + function toRect(aRect) + { + return { left: aRect.left.value, top: aRect.top.value, width: aRect.width.value, height: aRect.height.value }; + } + if (!checkRect(toRect(rect), aExpectedTextRectArray[i], aMessage + " " + i + "th rect")) { + return false; + } + } + return true; +} + +function checkRectContainsRect(aRect, aContainer, aMessage) +{ + let container = { left: Math.ceil(aContainer.left), + top: Math.ceil(aContainer.top), + width: Math.floor(aContainer.width), + height: Math.floor(aContainer.height) }; + + let ret = container.left <= aRect.left && + container.top <= aRect.top && + container.left + container.width >= aRect.left + aRect.width && + container.top + container.height >= aRect.top + aRect.height; + ret = ret && aMessage; + ok(ret, aMessage + " container={ left=" + container.left + ", top=" + + container.top + ", width=" + container.width + ", height=" + + container.height + " } rect={ left=" + aRect.left + ", top=" + aRect.top + + ", width=" + aRect.width + ", height=" + aRect.height + " }"); + return ret; +} + +// eslint-disable-next-line complexity +function runUndoRedoTest() +{ + textarea.value = ""; + textarea.focus(); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "," }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u306D\u3053", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "b" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u732B", + "clauses": + [ + { "length": 1, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u307E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "j" }, + }); + + // cancel the composition + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3080", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "]" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3080\u3059", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "r" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3080\u3059\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "/" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u5A18", + "clauses": + [ + { "length": 1, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + sendString(" meant"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("s \"cat-girl\". She is a "); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3088", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "9" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "4" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046\u304b", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "t" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046\u304b\u3044", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "e" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u5996\u602a", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } }); + + synthesizeKey("KEY_Backspace", {repeat: 12}); + + let i = 0; + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(32, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(30, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 mean", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(7, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 meant", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(8, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(2, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(0, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(0, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(2, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 meant", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(8, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 mean", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(7, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(30, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(32, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aTargetRanges, aDescription) { + if (aEvent.type !== "input" && aEvent.type !== "beforeinput") { + throw new Error(`${aDescription}: "${aEvent.type}" is not InputEvent`); + } + ok(InputEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with InputEvent interface: ${aDescription}`); + let cancelable = aEvent.type === "beforeinput" && + aInputType !== "insertCompositionText" && + aInputType !== "deleteCompositionText"; + is(aEvent.cancelable, cancelable, `"${aEvent.type}" event should ${cancelable ? "be" : "be never"} cancelable: ${aDescription}`); + is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`); + is(aEvent.isComposing, aIsComposing, `isComposing of "${aEvent.type}" event should be ${aIsComposing}: ${aDescription}`); + is(aEvent.inputType, aInputType, `inputType of "${aEvent.type}" event should be "${aInputType}": ${aDescription}`); + is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`); + is(aEvent.dataTransfer, null, `dataTransfer of "${aEvent.type}" event should be null: ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`); + } else { + is(targetRanges.length, aTargetRanges.length, + `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + } + } + } +} + +function runCompositionCommitAsIsTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + // compositioncommitasis with composing string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } }); + + is(result.length, 4, + "runCompositionCommitAsIsTest: 4 events should be fired after dispatching compositioncommitasis #1"); + is(result[0].type, "text", + "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1"); + is(result[1].type, "beforeinput", + "runCompositionCommitAsIsTest: beforeinput should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1"); + is(result[2].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1"); + is(result[3].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1"); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1"); + + // compositioncommitasis with committed string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #2"); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #2"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2"); + is(result[1].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2"); + checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #2"); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2"); + + // compositioncommitasis with committed string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #3"); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Escape", type: "keydown" }, + }); + is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #3"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend shouldn't be fired after dispatching compositioncommitasis #3"); + is(result[1].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3"); + checkInputEvent(result[1], false, "insertCompositionText", "", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #3"); + is(textarea.value, "", "runCompositionCommitAsIsTest: textarea doesn't have committed string #3"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runCompositionCommitTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + // compositioncommit with different composing string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #1"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #1"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #1"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #1"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1"); + + // compositioncommit with different committed string when there is already committed string + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #2"); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #2"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #2"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #2"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #2"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2"); + + // compositioncommit with empty composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #3"); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #3"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #3"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #3"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #3"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3"); + + // inserting empty string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(3, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired when inserting empty string with composition"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired when inserting empty string with composition"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when inserting empty string with composition"); + checkInputEvent(result[1], true, "insertCompositionText", "", [], + "runCompositionCommitTest: when inserting empty string with composition"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when inserting empty string with composition"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired when inserting empty string with composition"); + checkInputEvent(result[3], false, "insertCompositionText", "", [], + "runCompositionCommitTest: when inserting empty string with composition"); + is(textarea.value, "abc", + "runCompositionCommitTest: textarea should keep original value when inserting empty string with composition"); + + // replacing selection with empty string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(0, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired when replacing with empty string with composition"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired when replacing with empty string with composition"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when replacing with empty string with composition"); + checkInputEvent(result[1], true, "insertCompositionText", "", [], + "runCompositionCommitTest: when replacing with empty string with composition"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired when replacing with empty string with composition"); + checkInputEvent(result[3], false, "insertCompositionText", "", [], + "runCompositionCommitTest: when replacing with empty string with composition"); + is(textarea.value, "", + "runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition"); + + // replacing selection with same string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(0, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "abc" }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired when replacing selection with same string with composition"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired when replacing selection with same string with composition"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when replacing selection with same string with composition"); + checkInputEvent(result[2], true, "insertCompositionText", "abc", [], + "runCompositionCommitTest: when replacing selection with same string with composition"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired when replacing selection with same string with composition"); + checkInputEvent(result[4], false, "insertCompositionText", "abc", [], + "runCompositionCommitTest: when replacing selection with same string with composition"); + is(textarea.value, "abc", + "runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition"); + + // compositioncommit with non-empty composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #4"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #4"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #4"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runCompositionCommitTest: after dispatching compositioncommit #4"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4"); + checkInputEvent(result[4], false, "insertCompositionText", "", [], + "runCompositionCommitTest: after dispatching compositioncommit #4"); + is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4"); + + // compositioncommit immediately without compositionstart + textarea.value = ""; + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #5"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #5"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #5"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #5"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5"); + + // compositioncommit with same composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #5"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #6"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #6"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #6"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #6"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #6"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6"); + + // compositioncommit with same composition string when there is committed string + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitTest: 2 events should be fired after dispatching compositioncommit #7"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #7"); + is(result[1].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #7"); + checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #7"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +// eslint-disable-next-line complexity +async function runCompositionTest() +{ + textarea.value = ""; + textarea.focus(); + let caretRects = []; + + let caretRect = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #0")) { + return; + } + caretRects[0] = caretRect; + + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + if (!checkContent("\u3089", "runCompositionTest", "#1-1") || + !checkSelection(1, "", "runCompositionTest", "#1-1")) { + return; + } + + caretRect = synthesizeQueryCaretRect(1); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-1")) { + return; + } + caretRects[1] = caretRect; + + // input second character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + if (!checkContent("\u3089\u30FC", "runCompositionTest", "#1-2") || + !checkSelection(2, "", "runCompositionTest", "#1-2")) { + return; + } + + caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-2")) { + return; + } + caretRects[2] = caretRect; + + isnot(caretRects[2].left, caretRects[1].left, + "runCompositionTest: caret isn't moved (#1-2)"); + is(caretRects[2].top, caretRects[1].top, + "runCompositionTest: caret is moved to another line (#1-2)"); + is(caretRects[2].width, caretRects[1].width, + "runCompositionTest: caret width is wrong (#1-2)"); + is(caretRects[2].height, caretRects[1].height, + "runCompositionTest: caret width is wrong (#1-2)"); + + // input third character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "/" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3") || + !checkSelection(3, "", "runCompositionTest", "#1-3")) { + return; + } + + caretRect = synthesizeQueryCaretRect(3); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3")) { + return; + } + caretRects[3] = caretRect; + + isnot(caretRects[3].left, caretRects[2].left, + "runCompositionTest: caret isn't moved (#1-3)"); + is(caretRects[3].top, caretRects[2].top, + "runCompositionTest: caret is moved to another line (#1-3)"); + is(caretRects[3].width, caretRects[2].width, + "runCompositionTest: caret width is wrong (#1-3)"); + is(caretRects[3].height, caretRects[2].height, + "runCompositionTest: caret height is wrong (#1-3)"); + + // moves the caret left + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "KEY_ArrowLeft" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-1") || + !checkSelection(2, "", "runCompositionTest", "#1-3-1")) { + return; + } + + + caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3-1")) { + return; + } + + is(caretRect.left, caretRects[2].left, + "runCompositionTest: caret rects are different (#1-3-1, left)"); + is(caretRect.top, caretRects[2].top, + "runCompositionTest: caret rects are different (#1-3-1, top)"); + // by bug 335359, the caret width depends on the right side's character. + is(caretRect.width, caretRects[2].width + Math.round(window.devicePixelRatio), + "runCompositionTest: caret rects are different (#1-3-1, width)"); + is(caretRect.height, caretRects[2].height, + "runCompositionTest: caret rects are different (#1-3-1, height)"); + + // moves the caret left + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_ArrowLeft" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-2") || + !checkSelection(1, "", "runCompositionTest", "#1-3-2")) { + return; + } + + + caretRect = synthesizeQueryCaretRect(1); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3-2")) { + return; + } + + is(caretRect.left, caretRects[1].left, + "runCompositionTest: caret rects are different (#1-3-2, left)"); + is(caretRect.top, caretRects[1].top, + "runCompositionTest: caret rects are different (#1-3-2, top)"); + // by bug 335359, the caret width depends on the right side's character. + is(caretRect.width, caretRects[1].width + Math.round(window.devicePixelRatio), + "runCompositionTest: caret rects are different (#1-3-2, width)"); + is(caretRect.height, caretRects[1].height, + "runCompositionTest: caret rects are different (#1-3-2, height)"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "y" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-4") || + !checkSelection(4, "", "runCompositionTest", "#1-4")) { + return; + } + + + // backspace + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "KEY_Backspace" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-5") || + !checkSelection(3, "", "runCompositionTest", "#1-5")) { + return; + } + + // re-input + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "y" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-6") || + !checkSelection(4, "", "runCompositionTest", "#1-6")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 }, + "key": { key: "x" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", "runCompositionTest", "#1-7") || + !checkSelection(5, "", "runCompositionTest", "#1-7")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044", + "clauses": + [ + { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 }, + "key": { key: "e" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", "runCompositionTest", "#1-8") || + !checkSelection(6, "", "runCompositionTest", "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + "clauses": + [ + { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 7, "length": 0 }, + "key": { key: "b" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", "runCompositionTest", "#1-8") || + !checkSelection(7, "", "runCompositionTest", "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "runCompositionTest", "#1-9") || + !checkSelection(8, "", "runCompositionTest", "#1-9")) { + return; + } + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: " " }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "runCompositionTest", "#1-10") || + !checkSelection(4, "", "runCompositionTest", "#1-10")) { + return; + } + + // change the selected clause + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 }, + "key": { key: "KEY_ArrowLeft", shiftKey: true }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "runCompositionTest", "#1-11") || + !checkSelection(6, "", "runCompositionTest", "#1-11")) { + return; + } + + // reset clauses + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "clauses": + [ + { "length": 5, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 }, + "key": { key: "KEY_ArrowRight" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "runCompositionTest", "#1-12") || + !checkSelection(5, "", "runCompositionTest", "#1-12")) { + return; + } + + + let textRect1 = synthesizeQueryTextRect(0, 1); + let textRect2 = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(textRect1, + "runCompositionTest: synthesizeQueryTextRect #1-12-1") || + !checkQueryContentResult(textRect2, + "runCompositionTest: synthesizeQueryTextRect #1-12-2")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "runCompositionTest", "#1-13") || + !checkSelection(8, "", "runCompositionTest", "#1-13")) { + return; + } + + let textRect3 = synthesizeQueryTextRect(0, 1); + let textRect4 = synthesizeQueryTextRect(1, 1); + + if (!checkQueryContentResult(textRect3, + "runCompositionTest: synthesizeQueryTextRect #1-13-1") || + !checkQueryContentResult(textRect4, + "runCompositionTest: synthesizeQueryTextRect #1-13-2")) { + return; + } + + checkRect(textRect3, textRect1, "runCompositionTest: textRect #1-13-1"); + checkRect(textRect4, textRect2, "runCompositionTest: textRect #1-13-2"); + + // restart composition and input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3057", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "d" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3057", + "runCompositionTest", "#2-1") || + !checkSelection(8 + 1, "", "runCompositionTest", "#2-1")) { + return; + } + + let textRect3QueriedWithRelativeOffset = synthesizeQueryTextRect(-8, 1, true); + let textRect4QueriedWithRelativeOffset = synthesizeQueryTextRect(-8 + 1, 1, true); + checkRect(textRect3QueriedWithRelativeOffset, textRect3, "runCompositionTest: textRect #2-1-2"); + checkRect(textRect4QueriedWithRelativeOffset, textRect4, "runCompositionTest: textRect #2-1-3"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "r" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058", + "runCompositionTest", "#2-2") || + !checkSelection(8 + 1, "", "runCompositionTest", "#2-2")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058\u3087", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: ")", code: "Digit9", keyCode: KeyboardEvent.DOM_VK_9, shiftKey: true }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087", + "runCompositionTest", "#2-3") || + !checkSelection(8 + 2, "", "runCompositionTest", "#2-3")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058\u3087\u3046", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046", + "runCompositionTest", "#2-4") || + !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046", + "runCompositionTest", "#2-4") || + !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) { + return; + } + + // set selection + const selectionSetTest = await synthesizeSelectionSet(4, 7, false); + ok(selectionSetTest, "runCompositionTest: selectionSetTest failed"); + + if (!checkSelection(4, "\u3055\u884C\u3053\u3046\u3058\u3087\u3046", "runCompositionTest", "#3-1")) { + return; + } + + // start composition with selection + synthesizeCompositionChange( + { "composition": + { "string": "\u304A", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "6" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u304A", + "runCompositionTest", "#3-2") || + !checkSelection(4 + 1, "", "runCompositionTest", "#3-2")) { + return; + } + + // remove the composition string + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Backspace" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#3-3") || + !checkSelection(4, "", "runCompositionTest", "#3-3")) { + return; + } + + // re-input the composition string + synthesizeCompositionChange( + { "composition": + { "string": "\u3046", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3046", + "runCompositionTest", "#3-4") || + !checkSelection(4 + 1, "", "runCompositionTest", "#3-4")) { + return; + } + + // cancel the composition + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#3-5") || + !checkSelection(4, "", "runCompositionTest", "#3-5")) { + return; + } + + // bug 271815, some Chinese IMEs for Linux make empty composition string + // and compty clause information when it lists up Chinese characters on + // its candidate window. + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-1") || + !checkSelection(4, "", "runCompositionTest", "#4-1")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "b" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-2") || + !checkSelection(4, "", "runCompositionTest", "#4-2")) { + return; + } + + synthesizeComposition({ type: "compositioncommit", data: "\u6700", key: { key: "KEY_Enter" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-3") || + !checkSelection(5, "", "runCompositionTest", "#4-3")) { + return; + } + + // testing the canceling case + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-5") || + !checkSelection(5, "", "runCompositionTest", "#4-5")) { + return; + } + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-6") || + !checkSelection(5, "", "runCompositionTest", "#4-6")) { + return; + } + + // testing whether the empty composition string deletes selected string. + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-8") || + !checkSelection(4, "", "runCompositionTest", "#4-8")) { + return; + } + + synthesizeComposition({ type: "compositioncommit", data: "\u9AD8", key: { key: "KEY_Enter" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8", + "runCompositionTest", "#4-9") || + !checkSelection(5, "", "runCompositionTest", "#4-9")) { + return; + } + + synthesizeKey("KEY_Backspace"); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-11") || + !checkSelection(4, "", "runCompositionTest", "#4-11")) { + return; + } + + // bug 23558, ancient Japanese IMEs on Window may send empty text event + // twice at canceling composition. + synthesizeCompositionChange( + { "composition": + { "string": "\u6700", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#5-1") || + !checkSelection(4 + 1, "", "runCompositionTest", "#5-1")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Backspace", type: "keydown" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#5-2") || + !checkSelection(4, "", "runCompositionTest", "#5-2")) { + return; + } + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Backspace", type: "keyup" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#5-3") || + !checkSelection(4, "", "runCompositionTest", "#5-3")) { + return; + } + + // Undo tests for the testcases for bug 23558 and bug 271815 + synthesizeKey("z", { accelKey: true }); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#6-1") || + !checkSelection(4, "", "runCompositionTest", "#6-1")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8", + "runCompositionTest", "#6-2") || + !checkSelection(5, "", "runCompositionTest", "#6-2")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#6-3") || + !checkSelection(4, "", "runCompositionTest", "#6-3")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#6-4") || + !checkSelection(5, "", "runCompositionTest", "#6-4")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#6-5") || + !checkSelection(4, "", "runCompositionTest", "#6-5")) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function runCompositionEventTest() +{ + const kDescription = "runCompositionEventTest: "; + const kEvents = ["compositionstart", "compositionupdate", "compositionend", + "input"]; + + input.value = ""; + input.focus(); + + let windowEventCounts = [], windowEventData = [], windowEventLocale = []; + let inputEventCounts = [], inputEventData = [], inputEventLocale = []; + let preventDefault = false; + let stopPropagation = false; + + function initResults() + { + for (let i = 0; i < kEvents.length; i++) { + windowEventCounts[kEvents[i]] = 0; + windowEventData[kEvents[i]] = ""; + windowEventLocale[kEvents[i]] = ""; + inputEventCounts[kEvents[i]] = 0; + inputEventData[kEvents[i]] = ""; + inputEventLocale[kEvents[i]] = ""; + } + } + + function compositionEventHandlerForWindow(aEvent) + { + windowEventCounts[aEvent.type]++; + windowEventData[aEvent.type] = aEvent.data; + windowEventLocale[aEvent.type] = aEvent.locale; + if (preventDefault) { + aEvent.preventDefault(); + } + if (stopPropagation) { + aEvent.stopPropagation(); + } + } + + function formEventHandlerForWindow(aEvent) + { + ok(aEvent.isTrusted, "input events must be trusted events"); + windowEventCounts[aEvent.type]++; + windowEventData[aEvent.type] = input.value; + } + + function compositionEventHandlerForInput(aEvent) + { + inputEventCounts[aEvent.type]++; + inputEventData[aEvent.type] = aEvent.data; + inputEventLocale[aEvent.type] = aEvent.locale; + if (preventDefault) { + aEvent.preventDefault(); + } + if (stopPropagation) { + aEvent.stopPropagation(); + } + } + + function formEventHandlerForInput(aEvent) + { + inputEventCounts[aEvent.type]++; + inputEventData[aEvent.type] = input.value; + } + + window.addEventListener("compositionstart", compositionEventHandlerForWindow, + true, true); + window.addEventListener("compositionend", compositionEventHandlerForWindow, + true, true); + window.addEventListener("compositionupdate", compositionEventHandlerForWindow, + true, true); + window.addEventListener("input", formEventHandlerForWindow, + true, true); + + input.addEventListener("compositionstart", compositionEventHandlerForInput, + true, true); + input.addEventListener("compositionend", compositionEventHandlerForInput, + true, true); + input.addEventListener("compositionupdate", compositionEventHandlerForInput, + true, true); + input.addEventListener("input", formEventHandlerForInput, + true, true); + + // test for normal case + initResults(); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #1"); + is(windowEventData.compositionstart, "", + kDescription + "data of compositionstart isn't empty (window) #1"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #1"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #1"); + is(inputEventData.compositionstart, "", + kDescription + "data of compositionstart isn't empty (input) #1"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #1"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #1"); + is(windowEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (window) #1"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #1"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #1"); + is(inputEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (input) #1"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #1"); + + is(windowEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by window #1"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by input #1"); + + is(windowEventCounts.input, 1, + kDescription + "input hasn't been handled by window #1"); + is(windowEventData.input, "\u3089", + kDescription + "value of input element wasn't modified (window) #1"); + is(inputEventCounts.input, 1, + kDescription + "input hasn't been handled by input #1"); + is(inputEventData.input, "\u3089", + kDescription + "value of input element wasn't modified (input) #1"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by window #2"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by input #2"); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by window #2"); + is(windowEventData.compositionupdate, "\u3089\u30FC", + kDescription + "data of compositionupdate doesn't match (window) #2"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #2"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by input #2"); + is(inputEventData.compositionupdate, "\u3089\u30FC", + kDescription + "data of compositionupdate doesn't match (input) #2"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #2"); + + is(windowEventCounts.compositionend, 0, + kDescription + "compositionend has been handled during composition by window #2"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled during composition by input #2"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #2"); + is(windowEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (window) #2"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #2"); + is(inputEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (input) #2"); + + // text event shouldn't cause composition update, e.g., at committing. + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by window #3"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by input #3"); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate has been fired unexpectedly on window #3"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate has been fired unexpectedly on input #3"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #3"); + is(windowEventData.compositionend, "\u3089\u30FC", + kDescription + "data of compositionend doesn't match (window) #3"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #3"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #3"); + is(inputEventData.compositionend, "\u3089\u30FC", + kDescription + "data of compositionend doesn't match (input) #3"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #3"); + + is(windowEventCounts.input, 3, + kDescription + "input hasn't been handled by window #3"); + is(windowEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (window) #3"); + is(inputEventCounts.input, 3, + kDescription + "input hasn't been handled by input #3"); + is(inputEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (input) #3"); + + // select the second character, then, data of composition start should be + // the selected character. + initResults(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #4"); + is(windowEventData.compositionstart, "\u30FC", + kDescription + "data of compositionstart is empty (window) #4"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #4"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #4"); + is(inputEventData.compositionstart, "\u30FC", + kDescription + "data of compositionstart is empty (input) #4"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #4"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #4"); + is(windowEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (window) #4"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #4"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #4"); + is(inputEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (input) #4"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #4"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #4"); + is(windowEventData.compositionend, "\u3089", + kDescription + "data of compositionend doesn't match (window) #4"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #4"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #4"); + is(inputEventData.compositionend, "\u3089", + kDescription + "data of compositionend doesn't match (input) #4"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #4"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #4"); + is(windowEventData.input, "\u3089\u3089", + kDescription + "value of input element wasn't modified (window) #4"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #4"); + is(inputEventData.input, "\u3089\u3089", + kDescription + "value of input element wasn't modified (input) #4"); + + // preventDefault() should effect nothing. + preventDefault = true; + + initResults(); + synthesizeKey("a", { accelKey: true }); // Select All + + synthesizeCompositionChange( + { "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "," }, + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #5"); + is(windowEventData.compositionstart, "\u3089\u3089", + kDescription + "data of compositionstart is empty (window) #5"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #5"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #5"); + is(inputEventData.compositionstart, "\u3089\u3089", + kDescription + "data of compositionstart is empty (input) #5"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #5"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #5"); + is(windowEventData.compositionupdate, "\u306D", + kDescription + "data of compositionupdate doesn't match (window) #5"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #5"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #5"); + is(inputEventData.compositionupdate, "\u306D", + kDescription + "data of compositionupdate doesn't match (input) #5"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #5"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #5"); + is(windowEventData.compositionend, "\u306D", + kDescription + "data of compositionend doesn't match (window) #5"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #5"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #5"); + is(inputEventData.compositionend, "\u306D", + kDescription + "data of compositionend doesn't match (input) #5"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #5"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #5"); + is(windowEventData.input, "\u306D", + kDescription + "value of input element wasn't modified (window) #5"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #5"); + is(inputEventData.input, "\u306D", + kDescription + "value of input element wasn't modified (input) #5"); + + preventDefault = false; + + // stopPropagation() should effect nothing (except event count) + stopPropagation = true; + + initResults(); + synthesizeKey("a", { accelKey: true }); // Select All + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "\\", code: "IntlRo", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #6"); + is(windowEventData.compositionstart, "\u306D", + kDescription + "data of compositionstart is empty #6"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty #6"); + is(inputEventCounts.compositionstart, 0, + kDescription + "compositionstart has been handled by input #6"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #6"); + is(windowEventData.compositionupdate, "\u306E", + kDescription + "data of compositionupdate doesn't match #6"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty #6"); + is(inputEventCounts.compositionupdate, 0, + kDescription + "compositionupdate has been handled by input #6"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #6"); + is(windowEventData.compositionend, "\u306E", + kDescription + "data of compositionend doesn't match #6"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty #6"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by input #6"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #6"); + is(windowEventData.input, "\u306E", + kDescription + "value of input element wasn't modified (window) #6"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #6"); + is(inputEventData.input, "\u306E", + kDescription + "value of input element wasn't modified (input) #6"); + + stopPropagation = false; + + // create event and dispatch it. + initResults(); + + input.value = "value of input"; + synthesizeKey("a", { accelKey: true }); // Select All + + let compositionstart = document.createEvent("CompositionEvent"); + compositionstart.initCompositionEvent("compositionstart", + true, true, document.defaultView, + "start data", "start locale"); + is(compositionstart.type, "compositionstart", + kDescription + "type doesn't match #7"); + is(compositionstart.data, "start data", + kDescription + "data doesn't match #7"); + is(compositionstart.locale, "start locale", + kDescription + "locale doesn't match #7"); + is(compositionstart.detail, 0, + kDescription + "detail isn't 0 #7"); + + input.dispatchEvent(compositionstart); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #7"); + is(windowEventData.compositionstart, "start data", + kDescription + "data of compositionstart was changed (window) #7"); + is(windowEventLocale.compositionstart, "start locale", + kDescription + "locale of compositionstart was changed (window) #7"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #7"); + is(inputEventData.compositionstart, "start data", + kDescription + "data of compositionstart was changed (input) #7"); + is(inputEventLocale.compositionstart, "start locale", + kDescription + "locale of compositionstart was changed (input) #7"); + + is(input.value, "value of input", + kDescription + "input value was changed #7"); + + let compositionupdate1 = document.createEvent("compositionevent"); + compositionupdate1.initCompositionEvent("compositionupdate", + true, false, document.defaultView, + "composing string", "composing locale"); + is(compositionupdate1.type, "compositionupdate", + kDescription + "type doesn't match #8"); + is(compositionupdate1.data, "composing string", + kDescription + "data doesn't match #8"); + is(compositionupdate1.locale, "composing locale", + kDescription + "locale doesn't match #8"); + is(compositionupdate1.detail, 0, + kDescription + "detail isn't 0 #8"); + + input.dispatchEvent(compositionupdate1); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #8"); + is(windowEventData.compositionupdate, "composing string", + kDescription + "data of compositionupdate was changed (window) #8"); + is(windowEventLocale.compositionupdate, "composing locale", + kDescription + "locale of compositionupdate was changed (window) #8"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #8"); + is(inputEventData.compositionupdate, "composing string", + kDescription + "data of compositionupdate was changed (input) #8"); + is(inputEventLocale.compositionupdate, "composing locale", + kDescription + "locale of compositionupdate was changed (input) #8"); + + is(input.value, "value of input", + kDescription + "input value was changed #8"); + + let compositionupdate2 = document.createEvent("compositionEvent"); + compositionupdate2.initCompositionEvent("compositionupdate", + true, false, document.defaultView, + "commit string", "commit locale"); + is(compositionupdate2.type, "compositionupdate", + kDescription + "type doesn't match #9"); + is(compositionupdate2.data, "commit string", + kDescription + "data doesn't match #9"); + is(compositionupdate2.locale, "commit locale", + kDescription + "locale doesn't match #9"); + is(compositionupdate2.detail, 0, + kDescription + "detail isn't 0 #9"); + + input.dispatchEvent(compositionupdate2); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by window #9"); + is(windowEventData.compositionupdate, "commit string", + kDescription + "data of compositionupdate was changed (window) #9"); + is(windowEventLocale.compositionupdate, "commit locale", + kDescription + "locale of compositionupdate was changed (window) #9"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by input #9"); + is(inputEventData.compositionupdate, "commit string", + kDescription + "data of compositionupdate was changed (input) #9"); + is(inputEventLocale.compositionupdate, "commit locale", + kDescription + "locale of compositionupdate was changed (input) #9"); + + is(input.value, "value of input", + kDescription + "input value was changed #9"); + + let compositionend = document.createEvent("Compositionevent"); + compositionend.initCompositionEvent("compositionend", + true, false, document.defaultView, + "end data", "end locale"); + is(compositionend.type, "compositionend", + kDescription + "type doesn't match #10"); + is(compositionend.data, "end data", + kDescription + "data doesn't match #10"); + is(compositionend.locale, "end locale", + kDescription + "locale doesn't match #10"); + is(compositionend.detail, 0, + kDescription + "detail isn't 0 #10"); + + input.dispatchEvent(compositionend); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #10"); + is(windowEventData.compositionend, "end data", + kDescription + "data of compositionend was changed (window) #10"); + is(windowEventLocale.compositionend, "end locale", + kDescription + "locale of compositionend was changed (window) #10"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #10"); + is(inputEventData.compositionend, "end data", + kDescription + "data of compositionend was changed (input) #10"); + is(inputEventLocale.compositionend, "end locale", + kDescription + "locale of compositionend was changed (input) #10"); + + is(input.value, "value of input", + kDescription + "input value was changed #10"); + + window.removeEventListener("compositionstart", + compositionEventHandlerForWindow, true); + window.removeEventListener("compositionend", + compositionEventHandlerForWindow, true); + window.removeEventListener("compositionupdate", + compositionEventHandlerForWindow, true); + window.removeEventListener("input", + formEventHandlerForWindow, true); + + input.removeEventListener("compositionstart", + compositionEventHandlerForInput, true); + input.removeEventListener("compositionend", + compositionEventHandlerForInput, true); + input.removeEventListener("compositionupdate", + compositionEventHandlerForInput, true); + input.removeEventListener("input", + formEventHandlerForInput, true); +} + +function runCompositionTestWhoseTextNodeModified() { + const selection = windowOfContenteditable.getSelection(); + + (function testInsertTextBeforeComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextBeforeComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>def</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "def".length); + // Insert composition to the end of a text node + synthesizeSimpleCompositionChange("g"); + is( + textNode.data, + "defg", + `${description} Composition should be inserted to end of the text node` + ); + + // Insert a character before the composition string + textNode.insertData(0, "c"); + is( + textNode.data, + "cdefg", + `${ + description + } Composition should be shifted when a character is inserted before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}cdef`.length, + "g", + `${ + description + } IME selection should be shifted when a character is inserted before it` + ); + + // Update composition string (appending a character) + synthesizeSimpleCompositionChange("gh"); + is( + textNode.data, + "cdefgh", + `${ + description + } Composition should be updated correctly after inserted a character before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}cdef`.length, + "gh", + `${ + description + } IME selection should be extended correctly at updating composition after inserted a character before it` + ); + + // Insert another character before the composition + textNode.insertData(0, "b"); + is( + textNode.data, + "bcdefgh", + `${ + description + } Composition should be shifted when a character is inserted again before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}bcdef`.length, + "gh", + `${ + description + } IME selection should be shifted when a character is inserted again before it` + ); + + // Update the composition string again (appending another character) + synthesizeSimpleCompositionChange("ghi"); + is( + textNode.data, + "bcdefghi", + `${ + description + } Composition should be updated correctly after inserted 2 characters before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}bcdef`.length, + "ghi", + `${ + description + } IME selection should be extended correctly at updating composition after inserted 2 characters before it` + ); + + // Insert a new character before the composition string + textNode.insertData(0, "a"); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be shifted when a character is inserted again and again before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}abcdef`.length, + "ghi", + `${ + description + } IME selection should be shifted when a character is inserted again and again before it` + ); + + // Commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be committed as is` + ); + is( + selection.focusOffset, + "abcdefghi".length, + `${description} Selection should be collapsed at end of the commit string` + ); + + // Undo the commit + synthesizeKey("z", { accelKey: true }); + is( + textNode.data, + "abcdef", + `${ + description + } Composition should be undone correctly` + ); + is( + selection.focusOffset, + "abcdef".length, + `${ + description + } Selection should be collapsed at where the composition was after undoing` + ); + + // Redo the commit + synthesizeKey("z", { accelKey: true, shiftKey: true }); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be redone correctly` + ); + is( + selection.focusOffset, + "abcdefghi".length, + `${ + description + } focus offset of Selection should be at end of the commit string after redoing` + ); + })(); + + (function testInsertTextImmediatelyBeforeComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyBeforeComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>d</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, 0); + // Insert composition at start of the text node + synthesizeSimpleCompositionChange("b"); + is( + textNode.data, + "bd", + `${description} Composition should be inserted to start of the text node` + ); + + // Insert a character before the composition string + textNode.insertData(0, "a"); + is( + textNode.data, + "abd", + `${ + description + } Composition should be shifted when a character is inserted immediately before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "b", + `${ + description + } IME selection should be shifted when a character is inserted immediately before it`, + "", + { offset: todo_is, text: todo_is } + ); + + // Update the composition string after inserting character immediately before it + synthesizeSimpleCompositionChange("bc"); + is( + textNode.data, + "abcd", + `${description} Composition should be updated after the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bc", + `${ + description + } IME selection should be set at the composition string after the inserted character` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcd", + `${ + description + } Composition should be committed after the inserted character` + ); + is( + selection.focusOffset, + "abc".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testInsertTextImmediatelyAfterComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyAfterComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>a</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "a".length); + // Insert composition at end of the text node + synthesizeSimpleCompositionChange("b"); + is( + textNode.data, + "ab", + `${description} Composition should be inserted to start of the text node` + ); + + // Insert a character after the composition string + textNode.insertData("ab".length, "d"); + is( + textNode.data, + "abd", + `${ + description + } Composition should stay when a character is inserted immediately after it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "b", + `${ + description + } IME selection should stay when a character is inserted immediately after it` + ); + + // Update the composition string after inserting character immediately after it + synthesizeSimpleCompositionChange("bc"); + is( + textNode.data, + "abcd", + `${description} Composition should be updated before the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bc", + `${ + description + } IME selection should be set at the composition string before the inserted character` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcd", + `${ + description + } Composition should be committed before the inserted character` + ); + is( + selection.focusOffset, + "abc".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // Inserting/replacing text before the last character of composition string + // should be contained by the composition, i.e., updated by next composition + // update. This is Chrome compatible. + (function testInsertTextMiddleOfComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextMiddleOfComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>a</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "a".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("bd"); + is( + textNode.data, + "abd", + `${description} Composition should be inserted to end of the text node` + ); + + // Insert a character before the composition string + textNode.insertData("ab".length, "c"); + is( + textNode.data, + "abcd", + `${ + description + } Inserted string should inserted into the middle of composition string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bcd", + `${ + description + } IME selection should be extended when a character is inserted into middle of it` + ); + + // Update the composition string after inserting character into it + synthesizeSimpleCompositionChange("BD"); + is( + textNode.data, + "aBD", + `${ + description + } Composition should be replace the range containing the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "BD", + `${ + description + } IME selection should be set at the updated composition string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aBD", + `${ + description + } Composition should be committed without the inserted character` + ); + is( + selection.focusOffset, + "aBD".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceFirstCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceFirstCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("ab".length, "c".length, "XYZ"); + is( + textNode.data, + "abXYZdefg", + `${description} First character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "XYZde", + `${description} IME selection should contain the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEfg", + `${description} Composition should update the replace string too` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should update the replace string too` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // Although Chrome commits composition if all composition string is removed, + // let's keep composition for making TSF stable... + (function testReplaceAllCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceAllCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("ab".length, "cde".length, "XYZ"); + is( + textNode.data, + "abXYZfg", + `${description} Composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "", + `${ + description + } IME selection should be collapsed before the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be inserted again` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceCompositionStringAndSurroundedCharacters() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceCompositionStringAndSurroundedCharacters:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("a".length, "bcdef".length, "XYZ"); + is( + textNode.data, + "aXYZg", + `${description} Composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "", + `${ + description + } IME selection should be collapsed before the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "aCDEXYZg", + `${description} Composition should be inserted again` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aCDEXYZg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "aCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If start boundary characters are replaced, the replace string should be + // contained into the composition range. This is Chrome compatible. + (function testReplaceStartBoundaryOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceStartBoundaryOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace some text + textNode.replaceData("a".length, "bc".length, "XYZ"); + is( + textNode.data, + "aXYZdefg", + `${ + description + } Start of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "XYZde", + `${description} IME selection should contain the replace string` + ); + + // Update the replace string and remaining composition. + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "aCDEfg", + `${description} Composition should update the replace string too` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "CDE", + `${description} IME selection should contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aCDEfg", + `${ + description + } Composition should be committed` + ); + is( + selection.focusOffset, + "aCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If start boundary characters are replaced, the replace string should NOT + // be contained in the composition range. This is Chrome compatible. + (function testReplaceEndBoundaryOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceEndBoundaryOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("abcd".length, "ef".length, "XYZ"); + is( + textNode.data, + "abcdXYZg", + `${ + description + } End half of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cd", + `${ + description + } IME selection should be shrunken to the non-replaced part` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZg", + `${description} Only the remaining composition string should be updated` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should NOT include the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If the last character of composition is replaced, i.e., it should NOT be + // treated as a part of composition string. This is Chrome compatible. + (function testReplaceLastCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceLastCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("abcd".length, "e".length, "XYZ"); + is( + textNode.data, + "abcdXYZfg", + `${description} Last character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cd", + `${description} IME selection should be shrunken` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should NOT update the replace string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceMiddleCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceMiddleCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("abc".length, "d".length, "XYZ"); + is( + textNode.data, + "abcXYZefg", + `${ + description + } Middle character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cXYZe", + `${description} IME selection should be extended by the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEfg", + `${description} Composition should update the replace string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should be shrunken after update` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); +} + +// eslint-disable-next-line complexity +function runQueryTextRectInContentEditableTest() +{ + contenteditable.focus(); + + contenteditable.innerHTML = "<p>abc</p><p>def</p>"; + // \n 0 123 4 567 + // \r\n 01 234 56 789 + + let description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "a" + let a = synthesizeQueryTextRect(kLFLen, 1); + if (!checkQueryContentResult(a, description + "rect for 'a'")) { + return; + } + + // "b" + let b = synthesizeQueryTextRect(kLFLen + 1, 1); + if (!checkQueryContentResult(b, description + "rect for 'b'")) { + return; + } + + is(b.top, a.top, description + "'a' and 'b' should be at same top"); + isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'"); + is(b.height, a.height, description + "'a' and 'b' should be same height"); + + // "c" + let c = synthesizeQueryTextRect(kLFLen + 2, 1); + if (!checkQueryContentResult(c, description + "rect for 'c'")) { + return; + } + + is(c.top, b.top, description + "'b' and 'c' should be at same top"); + isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'"); + is(c.height, b.height, description + "'b' and 'c' should be same height"); + + // "abc" as array + let abcAsArray = synthesizeQueryTextRectArray(kLFLen, 3); + if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") || + !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) { + return; + } + + // 2nd <p> (can be computed with the rect of 'c') + let p2 = synthesizeQueryTextRect(kLFLen + 3, 1); + if (!checkQueryContentResult(p2, description + "rect for 2nd <p>")) { + return; + } + + is(p2.top, c.top, description + "'c' and a line breaker caused by 2nd <p> should be at same top"); + isSimilarTo(p2.left, c.left + c.width, 2, description + "left of a line breaker caused by 2nd <p> should be at similar to right of 'c'"); + is(p2.height, c.height, description + "'c' and a line breaker caused by 2nd <p> should be same height"); + + // 2nd <p> as array + let p2AsArray = synthesizeQueryTextRectArray(kLFLen + 3, 1); + if (!checkQueryContentResult(p2AsArray, description + "2nd <p>'s line breaker as array") || + !checkRectArray(p2AsArray, [p2], description + "query text rect array result of 2nd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p2_2 = synthesizeQueryTextRect(kLFLen + 4, 1); + if (!checkQueryContentResult(p2_2, description + "rect for \\n of \\r\\n caused by 2nd <p>")) { + return; + } + + is(p2_2.top, p2.top, description + "'\\r' and '\\n' should be at same top"); + is(p2_2.left, p2.left, description + "'\\r' and '\\n' should be at same top"); + is(p2_2.height, p2.height, description + "'\\r' and '\\n' should be same height"); + is(p2_2.width, p2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 4, 1); + if (!checkQueryContentResult(p2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <p>") || + !checkRectArray(p2_2AsArray, [p2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <p> should match with each query text rect result")) { + return; + } + } + + // "d" + let d = synthesizeQueryTextRect(kLFLen * 2 + 3, 1); + if (!checkQueryContentResult(d, description + "rect for 'd'")) { + return; + } + + isGreaterThan(d.top, a.top + a.height, description + "top of 'd' should be greater than bottom of 'a'"); + is(d.left, a.left, description + "'a' and 'd' should be same at same left"); + is(d.height, a.height, description + "'a' and 'd' should be same height"); + + // "e" + let e = synthesizeQueryTextRect(kLFLen * 2 + 4, 1); + if (!checkQueryContentResult(e, description + "rect for 'e'")) { + return; + } + + is(e.top, d.top, description + "'d' and 'd' should be at same top"); + isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'"); + is(e.height, d.height, description + "'d' and 'e' should be same height"); + + // "f" + let f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // "def" as array + let defAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 3, 3); + if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") || + !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) { + return; + } + + // next of "f" (can be computed with rect of 'f') + let next_f = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) { + return; + } + + is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top"); + isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'"); + is(next_f.height, d.height, description + "'f' and next of 'f' should be same height"); + + // next of "f" as array + let next_fAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") || + !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) { + return; + } + + // too big offset for the editor + let tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top"); + is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left"); + is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height"); + is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width"); + + // too big offset for the editors as array + let tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "<p>abc</p><p>def</p><p><br></p>"; + // \n 0 123 4 567 8 9 + // \r\n 01 234 56 789 01 23 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + + // 3rd <p> (can be computed with rect of 'f') + let p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) { + return; + } + + is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top"); + is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height"); + isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'"); + + // 3rd <p> as array + let p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") || + !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) { + return; + } + + is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height"); + is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") || + !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) { + return; + } + } + + // <br> in 3rd <p> + let br = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(br, description + "rect for <br> in 3rd <p>")) { + return; + } + + isGreaterThan(br.top, d.top + d.height, description + "a line breaker caused by <br> in 3rd <p> should be greater than bottom of 'd'"); + isSimilarTo(br.height, d.height, 2, description + "'d' and a line breaker caused by <br> in 3rd <p> should be similar height"); + is(br.left, d.left, description + "left of a line breaker caused by <br> in 3rd <p> should be same left of 'd'"); + + // <br> in 3rd <p> as array + let brAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(brAsArray, description + "<br> in 3rd <p> as array") || + !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> in 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br_2 = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(br_2, description + "rect for \\n of \\r\\n caused by <br> in 3rd <p>")) { + return; + } + + is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top"); + is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top"); + is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height"); + is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br_2AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br> in 3rd <p>") || + !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> in 3rd <p> should match with each query text rect result")) { + return; + } + } + + // next of <br> in 3rd <p> + let next_br = synthesizeQueryTextRect(kLFLen * 4 + 6, 1); + if (!checkQueryContentResult(next_br, description + "rect for next of <br> in 3rd <p>")) { + return; + } + + is(next_br.top, br.top, description + "next of <br> and <br> should be at same top"); + is(next_br.left, br.left, description + "next of <br> and <br> should be at same left"); + is(next_br.height, br.height, description + "next of <br> and <br> should be same height"); + is(next_br.width, br.width, description + "next of <br> and <br> should be same width"); + + // next of <br> in 3rd <p> as array + let next_brAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 6, 1); + if (!checkQueryContentResult(next_brAsArray, description + "rect array for next of <br> in 3rd <p>") || + !checkRectArray(next_brAsArray, [next_br], description + "query text rect array result of next of <br> in 3rd <p> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 4 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br.top, description + "too big offset and next of 3rd <p> should be at same top"); + is(tooBigOffset.left, next_br.left, description + "too big offset and next of 3rd <p> should be at same left"); + is(tooBigOffset.height, next_br.height, description + "too big offset and next of 3rd <p> should be same height"); + is(tooBigOffset.width, next_br.width, description + "too big offset and next of 3rd <p> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "<p>abc</p><p>def</p><p></p>"; + // \n 0 123 4 567 8 + // \r\n 01 234 56 789 0 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // 3rd <p> (can be computed with rect of 'f') + p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) { + return; + } + + is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top"); + is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height"); + isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'"); + + // 3rd <p> as array + p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") || + !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) { + return; + } + + is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height"); + is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") || + !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) { + return; + } + } + + // next of 3rd <p> + let next_p3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_p3, description + "rect for next of 3rd <p>")) { + return; + } + + isGreaterThan(next_p3.top, d.top + d.height, description + "top of next of 3rd <p> should equal to or be bigger than bottom of 'd'"); + isSimilarTo(next_p3.left, d.left, 2, description + "left of next of 3rd <p> should be at similar to left of 'd'"); + isSimilarTo(next_p3.height, d.height, 2, description + "next of 3rd <p> and 'd' should be similar height"); + + // next of 3rd <p> as array + let next_p3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_p3AsArray, description + "next of 3rd <p> as array") || + !checkRectArray(next_p3AsArray, [next_p3], description + "query text rect array result of next of 3rd <p> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_p3.top, description + "too big offset and next of 3rd <p> should be at same top"); + is(tooBigOffset.left, next_p3.left, description + "too big offset and next of 3rd <p> should be at same left"); + is(tooBigOffset.height, next_p3.height, description + "too big offset and next of 3rd <p> should be same height"); + is(tooBigOffset.width, next_p3.width, description + "too big offset and next of 3rd <p> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "abc<br>def"; + // \n 0123 456 + // \r\n 01234 567 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "a" + a = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(a, description + "rect for 'a'")) { + return; + } + + // "b" + b = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(b, description + "rect for 'b'")) { + return; + } + + is(b.top, a.top, description + "'a' and 'b' should be at same top"); + isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'"); + is(b.height, a.height, description + "'a' and 'b' should be same height"); + + // "c" + c = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(c, description + "rect for 'c'")) { + return; + } + + is(c.top, b.top, description + "'b' and 'c' should be at same top"); + isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'"); + is(c.height, b.height, description + "'b' and 'c' should be same height"); + + // "abc" as array + abcAsArray = synthesizeQueryTextRectArray(0, 3); + if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") || + !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) { + return; + } + + // <br> (can be computed with the rect of 'c') + br = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(br, description + "rect for <br>")) { + return; + } + + is(br.top, c.top, description + "'c' and a line breaker caused by <br> should be at same top"); + isSimilarTo(br.left, c.left + c.width, 2, description + "left of a line breaker caused by <br> should be at similar to right of 'c'"); + is(br.height, c.height, description + "'c' and a line breaker caused by <br> should be same height"); + + // <br> as array + brAsArray = synthesizeQueryTextRectArray(3, 1); + if (!checkQueryContentResult(brAsArray, description + "<br>'s line breaker as array") || + !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br_2 = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(br_2, description + "rect for \n of \r\n caused by <br>")) { + return; + } + + is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top"); + is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top"); + is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height"); + is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br_2AsArray = synthesizeQueryTextRectArray(4, 1); + if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br>") || + !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> should match with each query text rect result")) { + return; + } + } + + // "d" + d = synthesizeQueryTextRect(kLFLen + 3, 1); + if (!checkQueryContentResult(d, description + "rect for 'd'")) { + return; + } + + isSimilarTo(d.top, a.top + a.height, 2, description + "top of 'd' should be at similar to bottom of 'a'"); + is(d.left, a.left, description + "'a' and 'd' should be same at same left"); + is(d.height, a.height, description + "'a' and 'd' should be same height"); + + // "e" + e = synthesizeQueryTextRect(kLFLen + 4, 1); + if (!checkQueryContentResult(e, description + "rect for 'e'")) { + return; + } + + is(e.top, d.top, description + "'d' and 'd' should be at same top"); + isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'"); + is(e.height, d.height, description + "'d' and 'e' should be same height"); + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // "def" as array + defAsArray = synthesizeQueryTextRectArray(kLFLen + 3, 3); + if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") || + !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) { + return; + } + + // next of "f" (can be computed with rect of 'f') + next_f = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) { + return; + } + + is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top"); + isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'"); + is(next_f.height, d.height, description + "'f' and next of 'f' should be same height"); + + // next of "f" as array + next_fAsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") || + !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top"); + is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left"); + is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height"); + is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + // Note that this case does not have an empty line at the end. + contenteditable.innerHTML = "abc<br>def<br>"; + // \n 0123 4567 + // \r\n 01234 56789 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + + // 2nd <br> (can be computed with rect of 'f') + let br2 = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) { + return; + } + + is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top"); + is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height"); + isSimilarTo(br2.left, f.left + f.width, 2, description + "left of a line breaker caused by 2nd <br> should be similar to right of 'f'"); + + // 2nd <br> as array + let br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") || + !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) { + return; + } + + is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height"); + is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") || + !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) { + return; + } + } + + // next of 2nd <br> + let next_br2 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_br2, description + "rect for next of 2nd <br>")) { + return; + } + + is(next_br2.top, br2.top, description + "2nd <br> and next of 2nd <br> should be at same top"); + is(next_br2.left, br2.left, description + "2nd <br> and next of 2nd <br> should be at same top"); + is(next_br2.height, br2.height, description + "2nd <br> and next of 2nd <br> should be same height"); + is(next_br2.width, br2.width, description + "2nd <br> and next of 2nd <br> should be same width"); + + // next of 2nd <br> as array + let next_br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_br2AsArray, description + "rect array for next of 2nd <br>") || + !checkRectArray(next_br2AsArray, [next_br2], description + "query text rect array result of next of 2nd <br> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br2.top, description + "too big offset and next of 2nd <br> should be at same top"); + is(tooBigOffset.left, next_br2.left, description + "too big offset and next of 2nd <br> should be at same left"); + is(tooBigOffset.height, next_br2.height, description + "too big offset and next of 2nd <br> should be same height"); + is(tooBigOffset.width, next_br2.width, description + "too big offset and next of 2nd <br> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "abc<br>def<br><br>"; + // \n 0123 4567 8 + // \r\n 01234 56789 01 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // 2nd <br> + br2 = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) { + return; + } + + is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top"); + is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height"); + ok(f.left < br2.left, description + "left of a line breaker caused by 2nd <br> should be bigger than left of 'f', f.left=" + f.left + ", br2.left=" + br2.left); + + // 2nd <br> as array + br2AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") || + !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) { + return; + } + + is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height"); + is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") || + !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) { + return; + } + } + + // 3rd <br> + let br3 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3, description + "rect for next of 3rd <br>")) { + return; + } + + isSimilarTo(br3.top, d.top + d.height, 3, description + "top of next of 3rd <br> should at similar to bottom of 'd'"); + isSimilarTo(br3.left, d.left, 2, description + "left of next of 3rd <br> should be at similar to left of 'd'"); + isSimilarTo(br3.height, d.height, 2, description + "next of 3rd <br> and 'd' should be similar height"); + + // 3rd <br> as array + let br3AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(br3AsArray, description + "3rd <br>'s line breaker as array") || + !checkRectArray(br3AsArray, [br3], description + "query text rect array result of 3rd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3_2, description + "rect for \\n of \\r\\n caused by 3rd <br>")) { + return; + } + + is(br3_2.top, br3.top, description + "'\\r' and '\\n' should be at same top"); + is(br3_2.left, br3.left, description + "'\\r' and '\\n' should be at same left"); + is(br3_2.height, br3.height, description + "'\\r' and '\\n' should be same height"); + is(br3_2.width, br3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <br>") || + !checkRectArray(br3_2AsArray, [br3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <br> should match with each query text rect result")) { + return; + } + } + + // next of 3rd <br> + let next_br3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_br3, description + "rect for next of 3rd <br>")) { + return; + } + + is(next_br3.top, br3.top, description + "3rd <br> and next of 3rd <br> should be at same top"); + is(next_br3.left, br3.left, description + "3rd <br> and next of 3rd <br> should be at same left"); + is(next_br3.height, br3.height, description + "3rd <br> and next of 3rd <br> should be same height"); + is(next_br3.width, br3.width, description + "3rd <br> and next of 3rd <br> should be same width"); + + // next of 3rd <br> as array + let next_br3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_br3AsArray, description + "rect array for next of 3rd <br>") || + !checkRectArray(next_br3AsArray, [next_br3], description + "query text rect array result of next of 3rd <br> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br3.top, description + "too big offset and next of 3rd <br> should be at same top"); + is(tooBigOffset.left, next_br3.left, description + "too big offset and next of 3rd <br> should be at same left"); + is(tooBigOffset.height, next_br3.height, description + "too big offset and next of 3rd <br> should be same height"); + is(tooBigOffset.width, next_br3.width, description + "too big offset and next of 3rd <br> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset")) { + checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result"); + } + + if (!(function test_query_text_rects_across_invisible_text() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_across_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromStartToA = synthesizeQueryTextRectArray(0, kLFLen * 3 + 1); + if (!checkQueryContentResult(rectArrayFromStartToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromStartToARects = getRectArray(rectArrayFromStartToA); + if (!checkRect( + fromStartToARects[kLFLen * 3], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + return checkRect( + fromStartToARects[kLFLen * 2], + fromStartToARects[0], + `${description} rect for the linebreak in invisible text node should be same as first linebreak` + ); + })()) { + return; + } + + function test_query_text_rects_starting_from_invisible_text() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen, kLFLen * 2 + 1); + if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA); + if (!checkRect( + fromInvisibleToARects[kLFLen * 2], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + // For now the rect of characters in invisible text node should be caret rect + // before the following line break. This is inconsistent from the result of + // the query text rect array event starting from the previous visible line + // break or character, but users anyway cannot insert text into the invisible + // text node only with user's operation. Therefore, this won't be problem + // in usual web apps. + const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[kLFLen]; + return checkRect( + fromInvisibleToARects[0], + caretRectBeforeLineBreakBeforeA, + `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"` + ); + } + if (!test_query_text_rects_starting_from_invisible_text()) { + return; + } + + if (kLFLen > 1) { + function test_query_text_rects_starting_from_middle_of_invisible_linebreak() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_middle_of_invisible_linebreak: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen + 1, 1 + kLFLen + 1); + if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA); + if (!checkRect( + fromInvisibleToARects[1 + kLFLen], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + // For now the rect of characters in invisible text node should be caret rect + // before the following line break. This is inconsistent from the result of + // the query text rect array event starting from the previous visible line + // break or character, but users anyway cannot insert text into the invisible + // text node only with user's operation. Therefore, this won't be problem + // in usual web apps. + const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[1]; + return checkRect( + fromInvisibleToARects[0], + caretRectBeforeLineBreakBeforeA, + `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"` + ); + } + if (!test_query_text_rects_starting_from_middle_of_invisible_linebreak()) { + return; + } + } + + function test_query_text_rects_ending_with_invisible_text() { + contenteditable.innerHTML = "<div><div>abc</div>\n</div>"; + // \n 0 1 234 5 + // \r\n 01 23 456 78 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_ending_with_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "c" + const rectC = synthesizeQueryTextRect(kLFLen * 2 + 2, 1); + if (!checkQueryContentResult(rectC, `${description} rect of "c"`)) { + return false; + } + const rectArrayFromCToInvisible = synthesizeQueryTextRectArray(kLFLen * 2 + 2, 1 + kLFLen); + if (!checkQueryContentResult(rectArrayFromCToInvisible, `${description} rect array from "c" to invisible linebreak`)) { + return false; + } + const fromCToInvisibleRects = getRectArray(rectArrayFromCToInvisible); + if (!checkRect( + fromCToInvisibleRects[0], + rectC, + `${description} rect for "c" in array should be same as the result of query only it` + )) { + return false; + } + const caretRectAfterC = { + left: rectC.left + rectC.width, + top: rectC.top, + width: 1, + height: rectC.height, + }; + return checkRectFuzzy( + fromCToInvisibleRects[1], + caretRectAfterC, + { + left: 1, + top: 0, + width: 0, + height: 0, + }, + `${description} rect for the linebreak in invisible text node should be same as caret rect after "c"` + ); + } + if (!test_query_text_rects_ending_with_invisible_text()) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function runCharAtPointTest(aFocusedEditor, aTargetName) +{ + aFocusedEditor.value = "This is a test of the\nContent Events"; + // 012345678901234567890 12345678901234 + // 0 1 2 3 + + aFocusedEditor.focus(); + + const kNone = -1; + const kTestingOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen]; + const kLeftSideOffset = [ kNone, 9, 19, kNone, 33 + kLFLen]; + const kRightSideOffset = [ 1, 11, kNone, 22 + kLFLen, kNone]; + const kLeftTentativeCaretOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen]; + const kRightTentativeCaretOffset = [ 1, 11, 21, 22 + kLFLen, 35 + kLFLen]; + + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCharAtPointTest (" + aTargetName + "): editorRect")) { + return; + } + + for (let i = 0; i < kTestingOffset.length; i++) { + let textRect = synthesizeQueryTextRect(kTestingOffset[i], 1); + if (!checkQueryContentResult(textRect, + "runCharAtPointTest (" + aTargetName + "): textRect: i=" + i)) { + continue; + } + + checkRectContainsRect(textRect, editorRect, + "runCharAtPointTest (" + aTargetName + + "): the text rect isn't in the editor"); + + // Test #1, getting same character rect by the point near the top-left. + let charAtPt1 = synthesizeCharAtPoint(textRect.left + 1, + textRect.top + 1); + if (checkQueryContentResult(charAtPt1, + "runCharAtPointTest (" + aTargetName + "): charAtPt1: i=" + i)) { + ok(!charAtPt1.notFound, + "runCharAtPointTest (" + aTargetName + "): charAtPt1 isn't found: i=" + i); + if (!charAtPt1.notFound) { + is(charAtPt1.offset, kTestingOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt1 offset is wrong: i=" + i); + checkRect(charAtPt1, textRect, "runCharAtPointTest (" + aTargetName + + "): charAtPt1 left is wrong: i=" + i); + } + ok(!charAtPt1.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 isn't found: i=" + i); + if (!charAtPt1.tentativeCaretOffsetNotFound) { + is(charAtPt1.tentativeCaretOffset, kLeftTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 is wrong: i=" + i); + } + } + + // Test #2, getting same character rect by the point near the bottom-right. + let charAtPt2 = synthesizeCharAtPoint(textRect.left + textRect.width - 2, + textRect.top + textRect.height - 2); + if (checkQueryContentResult(charAtPt2, + "runCharAtPointTest (" + aTargetName + "): charAtPt2: i=" + i)) { + ok(!charAtPt2.notFound, + "runCharAtPointTest (" + aTargetName + "): charAtPt2 isn't found: i=" + i); + if (!charAtPt2.notFound) { + is(charAtPt2.offset, kTestingOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt2 offset is wrong: i=" + i); + checkRect(charAtPt2, textRect, "runCharAtPointTest (" + aTargetName + + "): charAtPt1 left is wrong: i=" + i); + } + ok(!charAtPt2.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 isn't found: i=" + i); + if (!charAtPt2.tentativeCaretOffsetNotFound) { + is(charAtPt2.tentativeCaretOffset, kRightTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 is wrong: i=" + i); + } + } + + // Test #3, getting left character offset. + let charAtPt3 = synthesizeCharAtPoint(textRect.left - 2, + textRect.top + 1); + if (checkQueryContentResult(charAtPt3, + "runCharAtPointTest (" + aTargetName + "): charAtPt3: i=" + i)) { + is(charAtPt3.notFound, kLeftSideOffset[i] == kNone, + kLeftSideOffset[i] == kNone ? + "runCharAtPointTest (" + aTargetName + "): charAtPt3 is found: i=" + i : + "runCharAtPointTest (" + aTargetName + "): charAtPt3 isn't found: i=" + i); + if (!charAtPt3.notFound) { + is(charAtPt3.offset, kLeftSideOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt3 offset is wrong: i=" + i); + } + if (kLeftSideOffset[i] == kNone) { + // There may be no enough padding-left (depends on platform) + todo(false, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't tested: i=" + i); + } else { + ok(!charAtPt3.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't found: i=" + i); + if (!charAtPt3.tentativeCaretOffsetNotFound) { + is(charAtPt3.tentativeCaretOffset, kLeftTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 is wrong: i=" + i); + } + } + } + + // Test #4, getting right character offset. + let charAtPt4 = synthesizeCharAtPoint(textRect.left + textRect.width + 1, + textRect.top + textRect.height - 2); + if (checkQueryContentResult(charAtPt4, + "runCharAtPointTest (" + aTargetName + "): charAtPt4: i=" + i)) { + is(charAtPt4.notFound, kRightSideOffset[i] == kNone, + kRightSideOffset[i] == kNone ? + "runCharAtPointTest (" + aTargetName + "): charAtPt4 is found: i=" + i : + "runCharAtPointTest (" + aTargetName + "): charAtPt4 isn't found: i=" + i); + if (!charAtPt4.notFound) { + is(charAtPt4.offset, kRightSideOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt4 offset is wrong: i=" + i); + } + ok(!charAtPt4.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 isn't found: i=" + i); + if (!charAtPt4.tentativeCaretOffsetNotFound) { + is(charAtPt4.tentativeCaretOffset, kRightTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 is wrong: i=" + i); + } + } + } +} + +function runCharAtPointAtOutsideTest() +{ + textarea.focus(); + textarea.value = "some text"; + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCharAtPointAtOutsideTest: editorRect")) { + return; + } + // Check on a text node which is at the outside of editor. + let charAtPt = synthesizeCharAtPoint(editorRect.left + 20, + editorRect.top - 10); + if (checkQueryContentResult(charAtPt, + "runCharAtPointAtOutsideTest: charAtPt")) { + ok(charAtPt.notFound, + "runCharAtPointAtOutsideTest: charAtPt is found on outside of editor"); + ok(charAtPt.tentativeCaretOffsetNotFound, + "runCharAtPointAtOutsideTest: tentative caret offset for charAtPt is found on outside of editor"); + } +} + +async function runSetSelectionEventTest() +{ + contenteditable.focus(); + + const selection = windowOfContenteditable.getSelection(); + + // #1 + contenteditable.innerHTML = "abc<br>def"; + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children"); + checkSelection(0, "abc" + kLF + "def", "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2, 2 + kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 2, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(2, "c" + kLF + "d", "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1, 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.wholeText.length, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node"); + checkSelection(1, "bc", "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, contenteditable.firstChild.wholeText.length, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 2, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the last text node"); + checkSelection(3, kLF, "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen, 0); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.anchorOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(100, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node of the editor"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children"); + checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\""); + + // #2 + contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>"; + + await synthesizeSelectionSet(kLFLen, 4+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, "abc" + kLF + "d", "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <b> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <b> node"); + checkSelection(kLFLen, "ab", "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node in the first <p> node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node in the first <p> node"); + checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 2+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <b> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <b> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the last <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(2+kLFLen, "c" + kLF + "d", "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the second <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen*2, "d", "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the <b> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <b> node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #3 + contenteditable.innerHTML = "<div>abc<p>def</p></div>"; + + await synthesizeSelectionSet(1+kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node"); + checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen, 3+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(1+kLFLen, "bc" + kLF + "d", "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node"); + checkSelection(3+kLFLen, "", "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 6+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 100); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first text node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1+kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node of the <div> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF + "a", "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, 2, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #4 + contenteditable.innerHTML = "<div><p>abc</p>def</div>"; + + await synthesizeSelectionSet(1+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node"); + checkSelection(1+kLFLen*2, "bc", "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen*2, 3); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(1+kLFLen*2, "bcd", "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node"); + checkSelection(3+kLFLen*2, "", "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 6+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 100); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF + kLF + "a", "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, kLF +"a", "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #5 + contenteditable.innerHTML = "<br>"; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF, "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\""); + + // #6 + contenteditable.innerHTML = "<p><br></p>"; + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #7 + contenteditable.innerHTML = "<br><br>"; + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF, "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen * 2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, kLFLen) selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen * 2, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen * 2, "", "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + // #8 + contenteditable.innerHTML = "<p><br><br></p>"; + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen * 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen, kLF + kLF, "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen) selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*2, kLF, "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*3, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*3, "", "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\""); + + // #9 (ContentEventHandler cannot distinguish if <p> can have children, so, the result is same as case #5, "<br>") + contenteditable.innerHTML = "<p></p>"; + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the <p> node + 1"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #10 + contenteditable.innerHTML = ""; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\""); + + // #11 + contenteditable.innerHTML = "<span></span><i><u></u></i>"; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, "", "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\""); + + // #12 + contenteditable.innerHTML = "<span>abc</span><i><u></u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #13 + contenteditable.innerHTML = "<span></span><i>abc<u></u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).firstChild, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #14 + contenteditable.innerHTML = "<span></span><i><u>abc</u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild.firstChild, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).firstChild.firstChild, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #15 + contenteditable.innerHTML = "<span></span><i><u></u>abc</i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).lastChild, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).lastChild, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #16 + contenteditable.innerHTML = "a<blink>b</blink>c"; + await synthesizeSelectionSet(0, 3); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, "abc", "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\""); + + // #17 (bug 1319660 - incorrect adjustment of content iterator last node) + contenteditable.innerHTML = "<div>a</div><div><br></div>"; + + await synthesizeSelectionSet(kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "a" + kLF, "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+2*kLFLen, 0); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <div> element"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(1+2*kLFLen, "", "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #18 (bug 1319660 - content iterator start node regression) + contenteditable.innerHTML = "<div><br></div><div><br></div>"; + + await synthesizeSelectionSet(2*kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2*kLFLen, kLF, "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); +} + +function runQueryTextContentEventTest() +{ + contenteditable.focus(); + + let result; + + // #1 + contenteditable.innerHTML = "abc<br>def"; + + result = synthesizeQueryTextContent(0, 6 + kLFLen); + is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 6+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2, 2 + kLFLen); + is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1, 2); + is(result.text, "bc", "runQueryTextContentEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6 + kLFLen, 1); + is(result.text, "", "runQueryTextContentEventTest #1 (6 + kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #2 + contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>"; + + result = synthesizeQueryTextContent(kLFLen, 4+kLFLen); + is(result.text, "abc" + kLF + "d", "runQueryTextContentEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 2); + is(result.text, "ab", "runQueryTextContentEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen, 2); + is(result.text, "bc", "runQueryTextContentEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 2+kLFLen); + is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen); + is(result.text, "c" + kLF, "runQueryTextContentEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen); + is(result.text, kLF + "d", "runQueryTextContentEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #3 + contenteditable.innerHTML = "<div>abc<p>def</p></div>"; + + result = synthesizeQueryTextContent(1+kLFLen, 2); + is(result.text, "bc", "runQueryTextContentEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen, 3+kLFLen); + is(result.text, "bc" + kLF + "d", "runQueryTextContentEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #3 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 6+kLFLen*2); + is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 2); + is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 100); + is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6+kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 1+kLFLen); + is(result.text, kLF + "a", "runQueryTextContentEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen); + is(result.text, "c" + kLF, "runQueryTextContentEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen); + is(result.text, kLF + "d", "runQueryTextContentEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #4 + contenteditable.innerHTML = "<div><p>abc</p>def</div>"; + + result = synthesizeQueryTextContent(1+kLFLen*2, 2); + is(result.text, "bc", "runQueryTextContentEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen*2, 3); + is(result.text, "bcd", "runQueryTextContentEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #4 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 6+kLFLen*2); + is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 2); + is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 100); + is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6+kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen*2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 1+kLFLen*2); + is(result.text, kLF + kLF + "a", "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 1+kLFLen); + is(result.text, kLF + "a", "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #5 + contenteditable.innerHTML = "<br>"; + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 1); + is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\""); + + // #6 + contenteditable.innerHTML = "<p><br></p>"; + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen*2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + // #7 + contenteditable.innerHTML = "<br><br>"; + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen * 2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen * 2, 1); + is(result.text, "", "runQueryTextContentEventTest #7 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + // #8 + contenteditable.innerHTML = "<p><br><br></p>"; + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen * 2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*2, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*3, 1); + is(result.text, "", "runQueryTextContentEventTest #8 (kLFLen*3, 1), \"" + contenteditable.innerHTML + "\""); + + // #16 + contenteditable.innerHTML = "a<blink>b</blink>c"; + + result = synthesizeQueryTextContent(0, 3); + is(result.text, "abc", "runQueryTextContentEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\""); +} + +function runQuerySelectionEventTest() +{ + contenteditable.focus(); + + let selection = windowOfContenteditable.getSelection(); + + // #1 + contenteditable.innerHTML = "<br/>a"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild, + 1 + ); + checkSelection( + 0, + `${kLF}a`, + `runQuerySelectionEventTest #1, "${contenteditable.innerHTML}"` + ); + + // #2 + contenteditable.innerHTML = "<p></p><p>abc</p>"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild.firstChild, + 1 + ); + checkSelection( + kLFLen, + `${kLF}a`, + `runQuerySelectionEventTest #2, "${contenteditable.innerHTML}"` + ); + + // #3 + contenteditable.innerHTML = "<p>abc</p><p>def</p>"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild.firstChild, + 1 + ); + checkSelection( + kLFLen, + `abc${kLF}d`, + `runQuerySelectionEventTest #3, "${contenteditable.innerHTML}"` + ); + + // #4 + contenteditable.innerHTML = "<p>abc</p>"; + selection.removeAllRanges(); + checkSelection( + null, + null, + `runQuerySelectionEventTest #4, "${contenteditable.innerHTML}"` + ); +} + +function runQueryIMESelectionTest() +{ + textarea.focus(); + textarea.value = "before after"; + let startoffset = textarea.selectionStart = textarea.selectionEnd = "before ".length; + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "abcdefgh", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "abcdefgh", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "ABCDEFGH", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("ConvertedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("SelectedClause", true, startoffset, "AB", "runQueryIMESelectionTest: starting to convert")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "ABCDEFGH", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("ConvertedClause", true, startoffset, "AB", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("SelectedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: changing selected clause")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition")) { + return; + } + + startoffset = textarea.selectionStart; + + synthesizeCompositionChange( + { "composition": + { "string": "abcdefgh", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("SelectedRawClause", true, startoffset + 1, "b", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("ConvertedClause", true, startoffset + 2, "c", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("SelectedClause", true, startoffset + 3, "d", "runQueryIMESelectionTest: unrealistic testcase")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); +} + +function runQueryPasswordTest() { + function checkRange(aOffset, aLength, aExpectedResult, aDescription) { + password.focus(); + let result = synthesizeQueryTextContent(aOffset, aLength); + is(result.text, aExpectedResult, + `${aDescription}: synthesizeQueryTextContent(${aOffset}, ${aLength})`); + password.setSelectionRange(aOffset, aOffset + aLength); + result = synthesizeQuerySelectedText(); + is(result.text, aExpectedResult, + `${aDescription}: synthesizeQuerySelectedText(${aOffset}, ${aLength})`); + } + + let editor = password.editor; + const kMask = editor.passwordMask; + password.value = "abcdef"; + + editor.mask(); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #1"); + checkRange(0, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #2"); + checkRange(3, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #3"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #4"); + + editor.unmask(0, 6); + checkRange(0, 6, "abcdef", + "runQueryPasswordTest: unmasked range 0-6 #1"); + checkRange(0, 3, "abc", + "runQueryPasswordTest: unmasked range 0-6 #2"); + checkRange(3, 3, "def", + "runQueryPasswordTest: unmasked range 0-6 #3"); + checkRange(2, 2, "cd", + "runQueryPasswordTest: unmasked range 0-6 #4"); + + editor.unmask(0, 3); + checkRange(0, 6, `abc${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #1"); + checkRange(0, 3, "abc", + "runQueryPasswordTest: unmasked range 0-3 #2"); + checkRange(3, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #3"); + checkRange(2, 2, `c${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #4"); + + editor.unmask(3, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}def`, + "runQueryPasswordTest: unmasked range 3-6 #1"); + checkRange(0, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 3-6 #2"); + checkRange(3, 3, `def`, + "runQueryPasswordTest: unmasked range 3-6 #3"); + checkRange(2, 2, `${kMask}d`, + "runQueryPasswordTest: unmasked range 3-6 #4"); + + editor.unmask(2, 4); + checkRange(0, 6, `${kMask}${kMask}cd${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #1"); + checkRange(1, 2, `${kMask}c`, + "runQueryPasswordTest: unmasked range 3-4 #2"); + checkRange(1, 3, `${kMask}cd`, + "runQueryPasswordTest: unmasked range 3-4 #3"); + checkRange(1, 4, `${kMask}cd${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #4"); + checkRange(2, 2, "cd", + "runQueryPasswordTest: unmasked range 3-4 #5"); + checkRange(2, 3, `cd${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #6"); + + + const kEmoji = String.fromCodePoint(0x1f914); + password.value = `${kEmoji}${kEmoji}${kEmoji}` + + editor.mask(); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range is not specified"); + + editor.unmask(0, 2); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #1"); + checkRange(0, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #2"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #3"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #4"); + + editor.unmask(2, 4); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #1"); + checkRange(0, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #2"); + checkRange(2, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #3"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #4"); + + editor.unmask(4, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #1"); + checkRange(0, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #2"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #3"); + checkRange(4, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #4"); + + editor.unmask(0, 1); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-1"); + + editor.unmask(1, 2); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-3"); + + editor.unmask(3, 4); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 3-4"); + + editor.unmask(4, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 5-6"); + + + const kEmojiSuperhero = String.fromCodePoint(0x1f9b8); + const kEmojiMediumSkinTone = String.fromCodePoint(0x1f3fd); + const kZeroWidthJoiner = "\u200d"; + const kFemaleSign = "\u2640"; + const kVariationSelector16 = "\ufe0f"; + const kComplicatedEmoji = `${kEmojiSuperhero}${kEmojiMediumSkinTone}${kZeroWidthJoiner}${kFemaleSign}${kVariationSelector16}`; + password.value = `${kComplicatedEmoji}${kComplicatedEmoji}${kComplicatedEmoji}` + editor.mask(); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range is not specified"); + + editor.unmask(0, 7); + checkRange(0, 21, `${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #1"); + checkRange(0, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #2"); + checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #3"); + checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #4"); + + editor.unmask(7, 14); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #1"); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #2"); + checkRange(7, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #3"); + checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #4"); + + editor.unmask(14, 21); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #1"); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #2"); + checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #3"); + checkRange(14, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #4"); + + password.value = `${kComplicatedEmoji}` + editor.unmask(0, 1); + checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 0-1"); + + editor.unmask(1, 2); + checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 2-3"); + + editor.unmask(3, 4); + checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 3-4"); + + editor.unmask(4, 5); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kZeroWidthJoiner}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kFemaleSign}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 5-6"); + + editor.unmask(6, 7); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kVariationSelector16}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 6-7"); + + + const kKanji = "\u8fba"; + const kIVS = String.fromCodePoint(0xe0101); + const kKanjiWithIVS = `${kKanji}${kIVS}`; + password.value = `${kKanjiWithIVS}${kKanjiWithIVS}${kKanjiWithIVS}` + + editor.mask(); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range is not specified"); + + editor.unmask(0, 3); + checkRange(0, 9, `${kKanjiWithIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1"); + checkRange(0, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2"); + checkRange(1, 3, `${kIVS}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3"); + checkRange(0, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4"); + checkRange(1, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #8"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #9"); + + editor.unmask(0, 1); + checkRange(0, 9, `${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #1"); + checkRange(0, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #7"); + + editor.unmask(1, 3); + checkRange(0, 9, `${kMask}${kIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2"); + checkRange(1, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7"); + + editor.unmask(3, 6); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanjiWithIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #1"); + checkRange(3, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #2"); + checkRange(4, 3, `${kIVS}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #3"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #4"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #5"); + checkRange(3, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #6"); + checkRange(4, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #7"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #8"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #9"); + + editor.unmask(3, 4); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #3"); + checkRange(3, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #7"); + + editor.unmask(4, 6); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #4"); + checkRange(4, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #7"); + + editor.unmask(6, 9); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #1"); + checkRange(6, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #2"); + checkRange(4, 3, `${kMask}${kMask}${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #3"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #4"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #5"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #6"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #7"); + checkRange(6, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #8"); + checkRange(7, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #9"); + + editor.unmask(6, 7); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #5"); + checkRange(6, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #7"); + + editor.unmask(7, 9); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #6"); + checkRange(7, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #7"); + + password.value = `${kKanjiWithIVS}${kKanjiWithIVS}`; + editor.unmask(0, 2); + checkRange(0, 6, `${kKanjiWithIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-2"); + + editor.unmask(1, 2); + checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 2-3"); + + editor.unmask(3, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-5"); + + editor.unmask(4, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 5-6"); + + editor.mask(); +} + +function runQueryContentEventRelativeToInsertionPoint() +{ + textarea.focus(); + textarea.value = "0123456789"; + + // "[]0123456789" + let startOffset = textarea.selectionStart = textarea.selectionEnd = 0; + if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#1") || + !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#2") || + !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#3") || + !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#5")) { + return; + } + + // "[01234]56789" + textarea.selectionEnd = 5; + if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#1") || + !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#2") || + !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#3") || + !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#5")) { + return; + } + + // "0123[]456789" + startOffset = textarea.selectionStart = textarea.selectionEnd = 4; + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "4", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "5", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "9", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#5")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + // "0123[a]456789" + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "4", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "89", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#4") || + !checkContentRelativeToSelection(11, 1, 11, "", "runQueryContentEventRelativeToInsertionPoint[composition at 4]")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); + + // Move start of composition at first compositionupdate event. + function onCompositionUpdate(aEvent) + { + startOffset = textarea.selectionStart = textarea.selectionEnd = textarea.selectionStart - 1; + textarea.removeEventListener("compositionupdate", onCompositionUpdate); + } + textarea.addEventListener("compositionupdate", onCompositionUpdate); + + synthesizeCompositionChange( + { "composition": + { "string": "b", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + // "0123[b]a456789" + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "b", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "789", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#4") || + !checkContentRelativeToSelection(12, 1, 12, "", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#5")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); +} + +function runBug1375825Test() +{ + contenteditable.focus(); + + // #1 + contenteditable.innerHTML = "abc<span contenteditable=\"false\">defgh</span>"; + + let ret = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "c", "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'"); + + ret = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "d", "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'"); + + ret = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "e", "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'"); + + ret = synthesizeQueryTextRect(5, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "f", "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'"); + + ret = synthesizeQueryTextRect(6, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "g", "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'"); + + ret = synthesizeQueryTextRect(7, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "h", "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'"); + + // #2 + contenteditable.innerHTML = "abc<span style=\"user-select: all;\">defgh</span>"; + + ret = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "c", "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'"); + + ret = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "d", "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'"); + + ret = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "e", "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'"); + + ret = synthesizeQueryTextRect(5, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "f", "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'"); + + ret = synthesizeQueryTextRect(6, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "g", "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'"); + + ret = synthesizeQueryTextRect(7, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "h", "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'"); +} + +function runBug1530649Test() +{ + // Vietnamese IME on macOS commits composition with typing space key. + // Then, typing new word shouldn't trim the trailing whitespace. + contenteditable.focus(); + contenteditable.innerHTML = ""; + synthesizeCompositionChange( + {composition: {string: "abc", clauses: [{length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 3, length: 0}}); + synthesizeComposition({type: "compositioncommit", data: "abc ", key: " "}); + + is(contenteditable.innerHTML, "abc <br>", + "runBug1530649Test: The trailing space shouldn't be removed"); + + synthesizeCompositionChange( + {composition: {string: "d", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 1, length: 0}}); + + is(contenteditable.innerHTML, "abc d<br>", + "runBug1530649Test: The new composition string shouldn't remove the last space"); + + synthesizeComposition({type: "compositioncommitasis", key: "KEY_Enter"}); + + is(contenteditable.innerHTML, "abc d<br>", + "runBug1530649Test: Committing the new composition string shouldn't remove the last space"); +} + +function runBug1571375Test() +{ + let selection = windowOfContenteditableBySpan.getSelection(); + let doc = document.getElementById("iframe7").contentDocument; + + contenteditableBySpan.focus(); + + contenteditableBySpan.innerHTML = "hello world"; + let range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 6); + range.setEnd(contenteditableBySpan.firstChild, 11); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: { start: 5, length: 0 }, + }); + synthesizeComposition({type: "compositioncommit", data: "world", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world", + "runBug1571375Test: space must not be removed by commit"); + + contenteditableBySpan.innerHTML = "hello world"; + range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 0); + range.setEnd(contenteditableBySpan.firstChild, 5); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "hello", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: { start: 5, length: 0 }, + }); + synthesizeComposition({type: "compositioncommit", data: "hello", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world", + "runBug1571375Test: space must not be removed by commit"); + + contenteditableBySpan.innerHTML = "hello world<div>.</div>"; + range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 6); + range.setEnd(contenteditableBySpan.firstChild, 11); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 0, length: 0}} + ); + synthesizeComposition({type: "compositioncommit", data: "world", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world<div>.</div>", + "runBug1571375Test: space must not be removed by commit"); +} + +async function runBug1584901Test() +{ + contenteditableBySpan.focus(); + contenteditableBySpan.innerHTML = ""; + + // XXX synthesizeCompositionChange won't work without wait. + await waitForTick(); + + synthesizeCompositionChange({ + composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommitasis", key: " "}); + + is(contenteditableBySpan.innerHTML, "a ", + "runBug1584901Test: space must not be removed by composition change"); + + synthesizeCompositionChange({ + composition: {string: "b ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommitasis", key: " "}); + + is(contenteditableBySpan.innerHTML, "a b ", + "runBug1584901Test: space must not be removed by composition change"); +} + +function runBug1675313Test() +{ + input.value = ""; + input.focus(); + let count = 0; + + function handler() { + input.focus(); + count++; + } + + input.addEventListener("keydown", handler); + input.addEventListener("keyup", handler); + + synthesizeCompositionChange({ + composition: { + string: "a", + clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + key: { key: "a", type: "keyup" }, + }, + }); + synthesizeCompositionChange({ + composition: { + string: "b", + clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + key: { key: "b", type: "keyup" }, + }, + }); + synthesizeComposition({type: "compositioncommitasis"}); + + is(count, 6, "runBug1675313Test: keydown event and keyup event are fired correctly"); + is(input.value, "b", + "runBug1675313Test: re-focus element doesn't commit composition if re-focus isn't click by user"); + + input.removeEventListener("keyup", handler); +} + +function runCommitCompositionWithSpaceKey() +{ + contenteditable.focus(); + contenteditable.innerHTML = ""; + + // Last white space might be if last child is no <br> + // Actually, our implementation will insert <br> element at last child, so + // white space will be ASCII space. + + synthesizeCompositionChange({ + composition: {string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "a"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a <br>", + "runCommitCompositionWithSpaceKey: last single space should be kept"); + + synthesizeCompositionChange({ + composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "b"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a b <br>", + "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space."); + + synthesizeCompositionChange({ + composition: {string: "c", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "c"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a b c <br>", + "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space."); + + contenteditable.innerHTML = "a"; + windowOfContenteditable.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length); + is(contenteditable.innerHTML, "a", + "runCommitCompositionWithSpaceKey: contenteditable should be initialized with text ending with a space and without following <br> element"); + + synthesizeCompositionChange({ + composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "b ", key: { key: " ", code: "Space" }}); + + is(contenteditable.innerHTML, "ab <br>", + "runCommitCompositionWithSpaceKey: contenteditable should end with a padding <br> element after inserting commit string ending with a space"); +} + +function runCSSTransformTest() +{ + textarea.focus(); + textarea.value = "some text"; + textarea.selectionStart = textarea.selectionEnd = textarea.value.length; + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCSSTransformTest: editorRect")) { + return; + } + let firstCharRect = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRect, + "runCSSTransformTest: firstCharRect")) { + return; + } + let lastCharRect = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRect, + "runCSSTransformTest: lastCharRect")) { + return; + } + let caretRect = synthesizeQueryCaretRect(textarea.selectionStart); + if (!checkQueryContentResult(caretRect, + "runCSSTransformTest: caretRect")) { + return; + } + let caretRectBeforeFirstChar = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRectBeforeFirstChar, + "runCSSTransformTest: caretRectBeforeFirstChar")) { + return; + } + + try { + textarea.style.transform = "translate(10px, 15px)"; + function movedRect(aRect, aCSS_CX, aCSS_CY) + { + return { + left: aRect.left + Math.round(aCSS_CX * window.devicePixelRatio), + top: aRect.top + Math.round(aCSS_CY * window.devicePixelRatio), + width: aRect.width, + height: aRect.height + }; + } + + let editorRectTranslated = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRectTranslated, + "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(editorRectTranslated, movedRect(editorRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform)) { + return; + } + let firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(firstCharRectTranslated, movedRect(firstCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + let lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(lastCharRectTranslated, movedRect(lastCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + let caretRectTranslated = synthesizeQueryCaretRect(textarea.selectionStart); + if (!checkQueryContentResult(caretRectTranslated, + "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(caretRectTranslated, movedRect(caretRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform)) { + return; + } + let caretRectBeforeFirstCharTranslated = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRectBeforeFirstCharTranslated, + "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform) || + !checkRectFuzzy(caretRectBeforeFirstCharTranslated, movedRect(caretRectBeforeFirstChar, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform)) { + return; + } + let firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + let lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + + // XXX It's too difficult to check the result with scale and rotate... + // For now, let's check if query text rect and query text rect array returns same rect. + textarea.style.transform = "scale(1.5)"; + firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + + textarea.style.transform = "rotate(30deg)"; + firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + } finally { + textarea.style.transform = ""; + } +} + +function runBug722639Test() +{ + textarea.focus(); + textarea.value = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; + textarea.value += textarea.value; + textarea.value += textarea.value; // 80 characters + + let firstLine = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstLine, + "runBug722639Test: firstLine")) { + return; + } + ok(true, "runBug722639Test: 1st line, top=" + firstLine.top + ", left=" + firstLine.left); + let firstLineAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstLineAsArray, "runBug722639Test: 1st line as array") || + !checkRectArray(firstLineAsArray, [firstLine], "runBug722639Test: 1st line as array should match with text rect result")) { + return; + } + if (kLFLen > 1) { + let firstLineLF = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(firstLineLF, + "runBug722639Test: firstLineLF")) { + return; + } + is(firstLineLF.top, firstLine.top, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + is(firstLineLF.left, firstLine.left, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + isfuzzy(firstLineLF.height, firstLine.height, 1, + "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + is(firstLineLF.width, firstLine.width, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + let firstLineLFAsArray = synthesizeQueryTextRectArray(1, 1); + if (!checkQueryContentResult(firstLineLFAsArray, "runBug722639Test: 1st line's \\n rect as array") || + !checkRectArray(firstLineLFAsArray, [firstLineLF], "runBug722639Test: 1st line's rect as array should match with text rect result")) { + return; + } + } + let secondLine = synthesizeQueryTextRect(kLFLen, 1); + if (!checkQueryContentResult(secondLine, + "runBug722639Test: secondLine")) { + return; + } + ok(true, "runBug722639Test: 2nd line, top=" + secondLine.top + ", left=" + secondLine.left); + let secondLineAsArray = synthesizeQueryTextRectArray(kLFLen, 1); + if (!checkQueryContentResult(secondLineAsArray, "runBug722639Test: 2nd line as array") || + !checkRectArray(secondLineAsArray, [secondLine], "runBug722639Test: 2nd line as array should match with text rect result")) { + return; + } + if (kLFLen > 1) { + let secondLineLF = synthesizeQueryTextRect(kLFLen + 1, 1); + if (!checkQueryContentResult(secondLineLF, + "runBug722639Test: secondLineLF")) { + return; + } + is(secondLineLF.top, secondLine.top, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + is(secondLineLF.left, secondLine.left, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + isfuzzy(secondLineLF.height, secondLine.height, 1, + "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + is(secondLineLF.width, secondLine.width, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + let secondLineLFAsArray = synthesizeQueryTextRectArray(kLFLen + 1, 1); + if (!checkQueryContentResult(secondLineLFAsArray, "runBug722639Test: 2nd line's \\n rect as array") || + !checkRectArray(secondLineLFAsArray, [secondLineLF], "runBug722639Test: 2nd line's rect as array should match with text rect result")) { + return; + } + } + let lineHeight = secondLine.top - firstLine.top; + ok(lineHeight > 0, + "runBug722639Test: lineHeight must be positive"); + is(secondLine.left, firstLine.left, + "runBug722639Test: the left value must be always same value"); + isfuzzy(secondLine.height, firstLine.height, 1, + "runBug722639Test: the height must be always same value"); + let previousTop = secondLine.top; + for (let i = 3; i <= textarea.value.length + 1; i++) { + let currentLine = synthesizeQueryTextRect(kLFLen * (i - 1), 1); + if (!checkQueryContentResult(currentLine, + "runBug722639Test: " + i + "th currentLine")) { + return; + } + ok(true, "runBug722639Test: " + i + "th line, top=" + currentLine.top + ", left=" + currentLine.left); + let currentLineAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1), 1); + if (!checkQueryContentResult(currentLineAsArray, "runBug722639Test: " + i + "th line as array") || + !checkRectArray(currentLineAsArray, [currentLine], "runBug722639Test: " + i + "th line as array should match with text rect result")) { + return; + } + // NOTE: the top position may be 1px larger or smaller than other lines + // due to sub pixel positioning. + if (Math.abs(currentLine.top - (previousTop + lineHeight)) <= 1) { + ok(true, "runBug722639Test: " + i + "th line's top is expected"); + } else { + is(currentLine.top, previousTop + lineHeight, + "runBug722639Test: " + i + "th line's top is unexpected"); + } + is(currentLine.left, firstLine.left, + "runBug722639Test: " + i + "th line's left is unexpected"); + isfuzzy(currentLine.height, firstLine.height, 1, + `runBug722639Test: ${i}th line's height is unexpected`); + if (kLFLen > 1) { + let currentLineLF = synthesizeQueryTextRect(kLFLen * (i - 1) + 1, 1); + if (!checkQueryContentResult(currentLineLF, + "runBug722639Test: " + i + "th currentLineLF")) { + return; + } + is(currentLineLF.top, currentLine.top, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + is(currentLineLF.left, currentLine.left, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + isfuzzy(currentLineLF.height, currentLine.height, 1, + `runBug722639Test: ${i}th line's \\n rect should be same as same line's \\r rect`); + is(currentLineLF.width, currentLine.width, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + let currentLineLFAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1) + 1, 1); + if (!checkQueryContentResult(currentLineLFAsArray, "runBug722639Test: " + i + "th line's \\n rect as array") || + !checkRectArray(currentLineLFAsArray, [currentLineLF], "runBug722639Test: " + i + "th line's rect as array should match with text rect result")) { + return; + } + } + previousTop = currentLine.top; + } +} + +function runCompositionWithSelectionChange() { + function doTest(aEditor, aDescription) { + aEditor.focus(); + const isHTMLEditor = + aEditor.nodeName.toLowerCase() != "input" && aEditor.nodeName.toLowerCase() != "textarea"; + const win = isHTMLEditor ? windowOfContenteditable : window; + function getValue() { + return isHTMLEditor ? aEditor.innerHTML : aEditor.value; + } + function setSelection(aStart, aLength) { + if (isHTMLEditor) { + win.getSelection().setBaseAndExtent(aEditor.firstChild, aStart, aEditor.firstChild, aStart + aLength); + } else { + aEditor.setSelectionRange(aStart, aStart + aLength); + } + } + + if (isHTMLEditor) { + aEditor.innerHTML = "abcxyz"; + } else { + aEditor.value = "abcxyz"; + } + setSelection("abc".length, 0); + + synthesizeCompositionChange({ + composition: { + string: "1", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 1, length: 0 }, + } + }); + + is(getValue(), "abc1xyz", + `${aDescription}: First composing character should be inserted middle of the text`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("abc".length, "1".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "12", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 2, length: 0 }, + } + }); + + is(getValue(), "abc12xyz", + `${aDescription}: Only composition string should be updated even if selection range is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("abc1".length, "2d".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "123", + clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 3, length: 0 }, + } + }); + + is(getValue(), "abc123xyz", + `${aDescription}: Only composition string should be updated even if selection range wider than composition string is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("ab".length, "c123d".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "456", + clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 3, length: 0 }, + } + }); + + is(getValue(), "abc456xyz", + `${aDescription}: Only composition string should be updated even if selection range which covers all over the composition string is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("beforeinput", () => { + setSelection("abc456d".length, 0); + }, {once: true}); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(getValue(), "abc456xyz", + `${aDescription}: Only composition string should be updated when committing composition but selection is updated by "beforeinput" event listener`); + if (isHTMLEditor) { + is(win.getSelection().focusNode, aEditor.firstChild, + `${aDescription}: The focus node after composition should be the text node`); + is(win.getSelection().focusOffset, "abc456".length, + `${aDescription}: The focus offset after composition should be end of the composition string`); + is(win.getSelection().anchorNode, aEditor.firstChild, + `${aDescription}: The anchor node after composition should be the text node`); + is(win.getSelection().anchorOffset, "abc456".length, + `${aDescription}: The anchor offset after composition should be end of the composition string`); + } else { + is(aEditor.selectionStart, "abc456".length, + `${aDescription}: The selectionStart after composition should be end of the composition string`); + is(aEditor.selectionEnd, "abc456".length, + `${aDescription}: The selectionEnd after composition should be end of the composition string`); + } + } + doTest(textarea, "runCompositionWithSelectionChange(textarea)"); + doTest(input, "runCompositionWithSelectionChange(input)"); + doTest(contenteditable, "runCompositionWithSelectionChange(contenteditable)"); +} + +function runForceCommitTest() +{ + let events; + function eventHandler(aEvent) + { + events.push(aEvent); + } + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + // Make the composition in textarea commit by click in the textarea + textarea.focus(); + textarea.value = ""; + + events = []; + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + is(events.length, 5, + "runForceCommitTest: wrong event count #1"); + is(events[0].type, "compositionstart", + "runForceCommitTest: the 1st event must be compositionstart #1"); + is(events[1].type, "compositionupdate", + "runForceCommitTest: the 2nd event must be compositionupdate #1"); + is(events[2].type, "text", + "runForceCommitTest: the 3rd event must be text #1"); + is(events[3].type, "beforeinput", + "runForceCommitTest: the 4th event must be beforeinput #1"); + checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #1"); + is(events[4].type, "input", + "runForceCommitTest: the 5th event must be input #1"); + checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #1"); + + events = []; + synthesizeMouseAtCenter(textarea, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #2"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #2"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #2"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #2"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #2"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #2"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #2"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #2"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #2"); + + // Make the composition in textarea commit by click in another editor (input) + textarea.focus(); + textarea.value = ""; + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(input, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #3"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #3"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #3`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #3"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #3`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #3"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #3"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #3`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #3"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #3"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #3`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #3"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #3"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input has composition #3"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #3"); + is(input.value, "", + "runForceCommitTest: the input has the committed text? #3"); + + // Make the composition in textarea commit by blur() + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.blur(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #4"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #4"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #4`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #4"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #4`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #4"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #4"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #4`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #4"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #4"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #4`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #4"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #4"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #4"); + + // Make the composition in textarea commit by input.focus() + textarea.focus(); + textarea.value = ""; + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.focus(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #5"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #5"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #5`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #5"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #5`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #5"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #5"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #5`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #5"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #5"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #5`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #5"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #5"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input has composition #5"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #5"); + is(input.value, "", + "runForceCommitTest: the input has the committed text? #5"); + + // Make the composition in textarea commit by click in another document's editor + textarea.focus(); + textarea.value = ""; + textareaInFrame.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(textareaInFrame, {}, iframe.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #6"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #6"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #6`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #6"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #6`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #6"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #6"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #6`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #6"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #6"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #6`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #6"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #6"); + ok(!getEditor(textareaInFrame).isComposing, + "runForceCommitTest: the textarea in frame has composition #6"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #6"); + is(textareaInFrame.value, "", + "runForceCommitTest: the textarea in frame has the committed text? #6"); + + // Make the composition in textarea commit by another document's editor's focus() + textarea.focus(); + textarea.value = ""; + textareaInFrame.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textareaInFrame.focus(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #7"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #7"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #7`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #7"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #7`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #7"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #7"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #7`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #7"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #7"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #7`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #7"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #7"); + ok(!getEditor(textareaInFrame).isComposing, + "runForceCommitTest: the textarea in frame has composition #7"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #7"); + is(textareaInFrame.value, "", + "runForceCommitTest: the textarea in frame has the committed text? #7"); + + // Make the composition in a textarea commit by click in another editable document + textarea.focus(); + textarea.value = ""; + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + let iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #8"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #8"); + is(events[0].target, textarea, + `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #8`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #8"); + is(events[1].target, textarea, + `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #8`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #8"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #8"); + is(events[2].target, textarea, + `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #8`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #8"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #8"); + is(events[3].target, textarea, + `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #8`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #8"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #8"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document has composition #8"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #8"); + is(iframe2.contentDocument.body.innerHTML, iframe2BodyInnerHTML, + "runForceCommitTest: the editable document has the committed text? #8"); + + // Make the composition in an editable document commit by click in it + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #9"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #9"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #9`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #9"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #9`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #9"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #9"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #9`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #9"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #9"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #9`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #9"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #9"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #9"); + + // Make the composition in an editable document commit by click in another document's editor + textarea.value = ""; + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(textarea, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #10"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #10"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #10`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #10"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #10`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #10"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #10"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #10`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #10"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #10"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #10`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #10"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #10"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea has composition #10"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #10"); + is(textarea.value, "", + "runForceCommitTest: the textarea has the committed text? #10"); + + // Make the composition in an editable document commit by click in the another editable document + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + iframe3.contentDocument.body.innerHTML = "Text in the Body"; + let iframe3BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(iframe3.contentDocument.body, {}, iframe3.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #11"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #11"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #11`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #11"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #11`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #11"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #11"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #11`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #11"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #11"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #11`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #11"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #11"); + ok(!getHTMLEditorIMESupport(iframe3.contentWindow).isComposing, + "runForceCommitTest: the other editable document has composition #11"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #11"); + is(iframe3.contentDocument.body.innerHTML, iframe3BodyInnerHTML, + "runForceCommitTest: the other editable document has the committed text? #11"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value = "set value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #12"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #12"); + is(events[0].target, input, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #12`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #12"); + is(events[1].target, input, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #12`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #12"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #12"); + is(events[2].target, input, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #12`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #12"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #12"); + is(events[3].target, input, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #12`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #12"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input still has composition #12"); + is(input.value, "set value", + "runForceCommitTest: the input doesn't have the set text #12"); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.value = "set value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #13"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #13"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #13`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #13"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #13`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #13"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #13"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #13`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #13"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #13"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #13`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #13"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #13"); + is(textarea.value, "set value", + "runForceCommitTest: the textarea doesn't have the set text #13"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value += " appended value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #14"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #14"); + is(events[0].target, input, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #14`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #14"); + is(events[1].target, input, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #14`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #14"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #14"); + is(events[2].target, input, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #14`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #14"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #14"); + is(events[3].target, input, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #14`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #14"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input still has composition #14"); + is(input.value, "\u306E appended value", + "runForceCommitTest: the input should have both composed text and appended text #14"); + + input.focus(); + input.value = "abcd"; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value = "abcd\u306E"; + + is(events.length, 0, + "runForceCommitTest: setting same value to input with composition shouldn't cause any events #15"); + is(input.value, "abcd\u306E", + "runForceCommitTest: the input has unexpected value #15"); + + input.blur(); // commit composition + + textarea.focus(); + textarea.value = "abcd"; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.value = "abcd\u306E"; + + is(events.length, 0, + "runForceCommitTest: setting same value to textarea with composition shouldn't cause any events #16"); + is(textarea.value, "abcd\u306E", + "runForceCommitTest: the input has unexpected value #16"); + + textarea.blur(); // commit composition + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); +} + +function runNestedSettingValue() +{ + let isTesting = false; + let events = []; + function eventHandler(aEvent) + { + events.push(aEvent); + if (isTesting) { + aEvent.target.value += aEvent.type + ", "; + } + } + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + textarea.value = "first setting value, "; + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #1"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #1"); + is(events[0].target, textarea, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #1`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #1"); + is(events[1].target, textarea, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #1`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #1"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #1"); + is(events[2].target, textarea, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #1`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #1"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #1"); + is(events[3].target, textarea, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #1`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #1"); + ok(!getEditor(textarea).isComposing, + "runNestedSettingValue: the textarea still has composition #1"); + is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the textarea should have all string set to value attribute"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + input.value = "first setting value, "; + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #2"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #2"); + is(events[0].target, input, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #2"); + is(events[1].target, input, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #2"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #2"); + is(events[2].target, input, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #2"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #2"); + is(events[3].target, input, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #2"); + ok(!getEditor(input).isComposing, + "runNestedSettingValue: the input still has composition #2"); + is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the input should have all string set to value attribute #2"); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + textarea.setRangeText("first setting value, "); + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #3"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #3"); + is(events[0].target, textarea, + `runNestedSettingValue: The ${events[0].type} event was fired on wrong event target #3`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #3"); + is(events[1].target, textarea, + `runNestedSettingValue: The ${events[1].type} event was fired on wrong event target #3`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #3"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #3"); + is(events[2].target, textarea, + `runNestedSettingValue: The ${events[2].type} event was fired on wrong event target #3`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #3"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #3"); + is(events[3].target, textarea, + `runNestedSettingValue: The ${events[3].type} event was fired on wrong event target #3`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #3"); + ok(!getEditor(textarea).isComposing, + "runNestedSettingValue: the textarea still has composition #3"); + is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the textarea should have appended by setRangeText() and all string set to value attribute #3"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + input.setRangeText("first setting value, "); + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #4"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #4"); + is(events[0].target, input, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #4`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #4"); + is(events[1].target, input, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #4`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #4"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #4"); + is(events[2].target, input, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #4`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #4"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #4"); + is(events[3].target, input, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #4`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #4"); + ok(!getEditor(input).isComposing, + "runNestedSettingValue: the input still has composition #4"); + is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the input should have all string appended by setRangeText() and set to value attribute #4"); + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); + +} + +async function runAsyncForceCommitTest() +{ + let events; + function eventHandler(aEvent) + { + events.push(aEvent); + }; + + // If IME commits composition for a request, TextComposition commits + // composition automatically because most web apps must expect that active + // composition should be committed synchronously. Therefore, in this case, + // a click during composition should cause committing composition + // synchronously and delayed commit shouldn't cause composition events. + let commitRequested = false; + let onFinishTest = null; + function callback(aTIP, aNotification) + { + ok(true, aNotification.type); + if (aNotification.type != "request-to-commit") { + return true; + } + commitRequested = true; + if (onFinishTest) { + let resolve = onFinishTest; + onFinishTest = null; + + SimpleTest.executeSoon(() => { + events = []; + aTIP.commitComposition(); + + is(events.length, 0, + "runAsyncForceCommitTest: composition events shouldn't been fired by asynchronous call of nsITextInputProcessor.commitComposition()"); + + SimpleTest.executeSoon(resolve); + }); + } + return true; + }; + + function promiseCleanUp() { + return new Promise(resolve => { onFinishTest = resolve; }); + } + + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + // Make the composition in textarea commit by click in the textarea + textarea.focus(); + textarea.value = ""; + + events = []; + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, window, callback); + + is(events.length, 5, + "runAsyncForceCommitTest: wrong event count #1"); + is(events[0].type, "compositionstart", + "runAsyncForceCommitTest: the 1st event must be compositionstart #1"); + is(events[1].type, "compositionupdate", + "runAsyncForceCommitTest: the 2nd event must be compositionupdate #1"); + is(events[2].type, "text", + "runAsyncForceCommitTest: the 3rd event must be text #1"); + is(events[3].type, "beforeinput", + "runAsyncForceCommitTest: the 4th event must be beforeinput #1"); + checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #1"); + is(events[4].type, "input", + "runAsyncForceCommitTest: the 5th event must be input #1"); + checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #1"); + + events = []; + let waitCleanState = promiseCleanUp(); + + synthesizeMouseAtCenter(textarea, {}); + + ok(commitRequested, + "runAsyncForceCommitTest: \"request-to-commit\" should've been notified"); + is(events.length, 4, + "runAsyncForceCommitTest: wrong event count #2"); + is(events[0].type, "text", + "runAsyncForceCommitTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runAsyncForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runAsyncForceCommitTest: the 2nd event must be beforeinput #2"); + is(events[1].target, textarea, + `runAsyncForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #2"); + is(events[2].type, "compositionend", + "runAsyncForceCommitTest: the 3rd event must be compositionend #2"); + is(events[2].target, textarea, + `runAsyncForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runAsyncForceCommitTest: compositionend has wrong data #2"); + is(events[3].type, "input", + "runAsyncForceCommitTest: the 4th event must be input #2"); + is(events[3].target, textarea, + `runAsyncForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #2"); + ok(!getEditor(textarea).isComposing, + "runAsyncForceCommitTest: the textarea still has composition #2"); + is(textarea.value, "\u306E", + "runAsyncForceCommitTest: the textarea doesn't have the committed text #2"); + + await waitCleanState; + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); +} + +function runBug811755Test() +{ + iframe2.contentDocument.body.innerHTML = "<div>content<br/></div>"; + iframe2.contentWindow.focus(); + // Query everything + let textContent = synthesizeQueryTextContent(0, 10); + if (!checkQueryContentResult(textContent, "runBug811755Test: synthesizeQueryTextContent #1")) { + return; + } + // Query everything but specify exact end offset, which should be immediately after the <br> node + // If PreContentIterator is used, the next node after <br> is the node after </div>. + // If ContentIterator is used, the next node is the <div> node itself. In this case, the end + // node ends up being before the start node, and an empty string is returned. + let queryContent = synthesizeQueryTextContent(0, textContent.text.length); + if (!checkQueryContentResult(queryContent, "runBug811755Test: synthesizeQueryTextContent #2")) { + return; + } + is(queryContent.text, textContent.text, "runBug811755Test: two queried texts don't match"); +} + +function runIsComposingTest() +{ + let expectedIsComposing = false; + let description = ""; + + function eventHandler(aEvent) + { + if (aEvent.type == "keydown" || aEvent.type == "keyup") { + is(aEvent.isComposing, expectedIsComposing, + "runIsComposingTest: " + description + " (type=" + aEvent.type + ", key=" + aEvent.key + ")"); + } else if (aEvent.type == "keypress") { + // keypress event shouldn't be fired during composition so that isComposing should be always false. + is(aEvent.isComposing, false, + "runIsComposingTest: " + description + " (type=" + aEvent.type + ")"); + } else { + checkInputEvent(aEvent, expectedIsComposing, "insertCompositionText", "\u3042", [], + `runIsComposingTest: ${description}`); + } + } + + function onComposition(aEvent) + { + if (aEvent.type == "compositionstart") { + expectedIsComposing = true; + } else if (aEvent.type == "compositionend") { + expectedIsComposing = false; + } + } + + textarea.addEventListener("keydown", eventHandler, true); + textarea.addEventListener("keypress", eventHandler, true); + textarea.addEventListener("keyup", eventHandler, true); + textarea.addEventListener("beforeinput", eventHandler, true); + textarea.addEventListener("input", eventHandler, true); + textarea.addEventListener("compositionstart", onComposition, true); + textarea.addEventListener("compositionend", onComposition, true); + + textarea.focus(); + textarea.value = ""; + + // XXX These cases shouldn't occur in actual native key events because we + // don't dispatch key events while composition (bug 354358). + description = "events before dispatching compositionstart"; + synthesizeKey("KEY_ArrowLeft"); + + description = "events after dispatching compositionchange"; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }, + }); + + // Although, firing keypress event during composition is a bug. + synthesizeKey("KEY_Insert"); + + description = "events for committing composition string"; + + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter", code: "Enter", type: "keydown" } }); + + // input event will be fired by synthesizing compositionend event. + // Then, its isComposing should be false. + description = "events after dispatching compositioncommitasis"; + synthesizeKey("KEY_Enter", {type: "keyup"}); + + textarea.removeEventListener("keydown", eventHandler, true); + textarea.removeEventListener("keypress", eventHandler, true); + textarea.removeEventListener("keyup", eventHandler, true); + textarea.removeEventListener("beforeinput", eventHandler, true); + textarea.removeEventListener("input", eventHandler, true); + textarea.removeEventListener("compositionstart", onComposition, true); + textarea.removeEventListener("compositionend", onComposition, true); + + textarea.value = ""; +} + +function runRedundantChangeTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + textarea.value = ""; + + // synthesize change event + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #1"); + is(result[0].type, "compositionupdate", + "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1"); + is(result[1].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1"); + is(result[2].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #1"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [], + "runRedundantChangeTest: after synthesizing composition change #1"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired after synthesizing composition change #1"); + checkInputEvent(result[3], true, "insertCompositionText", "\u3042", [], + "runRedundantChangeTest: after synthesizing composition change #1"); + is(textarea.value, "\u3042", "runRedundantChangeTest: textarea has uncommitted string #1"); + + // synthesize another change event + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042\u3044", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #2"); + is(result[0].type, "compositionupdate", + "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2"); + is(result[1].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2"); + is(result[2].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #2"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: after synthesizing composition change #2"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired after synthesizing composition change #2"); + checkInputEvent(result[3], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: after synthesizing composition change #2"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #2"); + + // synthesize same change event again + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042\u3044", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + is(result.length, 0, "runRedundantChangeTest: no events should be fired after synthesizing composition change again"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #3"); + + // synthesize commit-as-is + clearResult(); + synthesizeComposition({ type: "compositioncommitasis" }); + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition commit-as-is"); + is(result[0].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition commit-as-is for removing the ranges"); + is(result[1].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition commit-as-is for removing the ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: at synthesizing commit-as-is"); + is(result[2].type, "compositionend", + "runRedundantChangeTest: compositionend should be fired after synthesizing composition commit-as-is"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired before compositionend at synthesizing commit-as-is"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: at synthesizing commit-as-is"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has the commit string"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runNotRedundantChangeTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + textarea.value = "abcde"; + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #1"); + + // synthesize change event with null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + }); + is(result.length, 3, + "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges"); + is(result[0].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges"); + is(result[1].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges"); + is(result[2].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with null ranges after non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #2"); + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 3, + "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges"); + is(result[0].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges"); + is(result[1].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges"); + is(result[2].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #3"); + + // synthesize change event with empty data and null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + }); + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data and null ranges after non-null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #1"); + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after empty data and null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #4"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 5, + "runNotRedundantChangeTest: 5 events should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data after non-empty data"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition commit with empty data after non-empty data"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data"); + is(result[3].type, "compositionend", + "runNotRedundantChangeTest: compositionend should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[4].type, "input", + "runNotRedundantChangeTest: input should be fired after compositionend after synthesizing composition change with empty data after non-empty data"); + checkInputEvent(result[4], false, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data"); + is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #2"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runNativeLineBreakerTest() +{ + textarea.focus(); + + let result = {}; + function clearResult() + { + result = { compositionupdate: null, compositionend: null }; + } + + function handler(aEvent) + { + result[aEvent.type] = aEvent.data; + } + + Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", false); + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + // '\n' in composition string shouldn't be changed. + clearResult(); + textarea.value = ""; + let clauses = [ "abc\n", "def\n\ng", "hi\n", "\njkl" ]; + let caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#1"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\n/g, kLF).length, clauses[1].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\n/g, kLF).length, clauses[2].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\n/g, kLF).length, clauses[3].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + is(result.compositionupdate, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionupdate.data shouldn't be removed nor replaced with other characters #1"); + is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #1"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#2"); + is(result.compositionend, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionend.data shouldn't be removed nor replaced with other characters #2"); + is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #2"); + + // \r\n in composition string should be replaced with \n. + clearResult(); + textarea.value = ""; + clauses = [ "abc\r\n", "def\r\n\r\ng", "hi\r\n", "\r\njkl" ]; + caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\r\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#3"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r\n/g, kLF).length, clauses[1].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r\n/g, kLF).length, clauses[2].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r\n/g, kLF).length, clauses[3].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + is(result.compositionupdate, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionudpate.data should be replaced with \\n #3"); + is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #3"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\r\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#4"); + is(result.compositionend, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionend.data should be replaced with \\n #4"); + is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #4"); + + // \r (not followed by \n) in composition string should be replaced with \n. + clearResult(); + textarea.value = ""; + clauses = [ "abc\r", "def\r\rg", "hi\r", "\rjkl" ]; + caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\r/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#5"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r/g, kLF).length, clauses[1].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r/g, kLF).length, clauses[2].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r/g, kLF).length, clauses[3].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + is(result.compositionupdate, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionupdate.data should be replaced with \\n #5"); + is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #5"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\r/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#6"); + is(result.compositionend, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionend.data should be replaced with \\n #6"); + is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #6"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + + Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters"); +} + +function runControlCharTest() +{ + textarea.focus(); + + let result = {}; + function clearResult() + { + result = { compositionupdate: null, compositionend: null }; + } + + function handler(aEvent) + { + result[aEvent.type] = aEvent.data; + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + textarea.value = ""; + + let controlChars = String.fromCharCode.apply(null, Object.keys(Array.from({length:0x20}))) + "\x7F"; + let allowedChars = "\t\n\n"; + + let data = "AB" + controlChars + "CD" + controlChars + "EF"; + let removedData = "AB" + allowedChars + "CD" + allowedChars + "EF"; + + let DIndex = data.indexOf("D"); + let removedDIndex = removedData.indexOf("D"); + + // input string contains control characters + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": data, + "clauses": + [ + { "length": DIndex, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": data.length - DIndex, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": DIndex, "length": 0 } + }); + + checkSelection(removedDIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#1") + + is(result.compositionupdate, removedData, "runControlCharTest: control characters in event.data should be removed in compositionupdate event #1"); + is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #1"); + + synthesizeComposition({ type: "compositioncommit", data }); + + is(result.compositionend, removedData, "runControlCharTest: control characters in event.data should be removed in compositionend event #2"); + is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #2"); + + textarea.value = ""; + + clearResult(); + + Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", true); + + // input string contains control characters, allowing control characters + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": data, + "clauses": + [ + { "length": DIndex, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": data.length - DIndex, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": DIndex, "length": 0 } + }); + + checkSelection(DIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#3") + + is(result.compositionupdate, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionupdate event #3"); + is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #3"); + + synthesizeComposition({ type: "compositioncommit", data }); + + is(result.compositionend, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionend event #4"); + is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #4"); + + Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); +} + +async function runRemoveContentTest() +{ + let events = []; + function eventHandler(aEvent) + { + events.push(aEvent); + } + textarea.addEventListener("compositionstart", eventHandler, true); + textarea.addEventListener("compositionupdate", eventHandler, true); + textarea.addEventListener("compositionend", eventHandler, true); + textarea.addEventListener("beforeinput", eventHandler, true); + textarea.addEventListener("input", eventHandler, true); + textarea.addEventListener("text", eventHandler, true); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + let nextSibling = textarea.nextSibling; + let parent = textarea.parentElement; + + events = []; + parent.removeChild(textarea); + + await waitForEventLoops(50); + + // XXX Currently, "input" event and "beforeinput" event are not fired on removed content. + is(events.length, 3, + "runRemoveContentTest: wrong event count #1"); + is(events[0].type, "compositionupdate", + "runRemoveContentTest: the 1st event must be compositionupdate #1"); + is(events[0].target, textarea, + `runRemoveContentTest: The "${events[0].type}" event was fired on wrong event target #1`); + is(events[0].data, "", + "runRemoveContentTest: compositionupdate has wrong data #1"); + is(events[1].type, "text", + "runRemoveContentTest: the 2nd event must be text #1"); + is(events[1].target, textarea, + `runRemoveContentTest: The "${events[1].type}" event was fired on wrong event target #1`); + todo_is(events[2].type, "beforeinput", + "runRemoveContentTest: the 3rd event must be beforeinput #1"); + // is(events[2].target, textarea, + // `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`); + // checkInputEvent(events[2], true, "insertCompositionText", "", [], + // "runRemoveContentTest: #1"); + is(events[2].type, "compositionend", + "runRemoveContentTest: the 4th event must be compositionend #1"); + is(events[2].target, textarea, + `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`); + is(events[2].data, "", + "runRemoveContentTest: compositionend has wrong data #1"); + ok(!getEditor(textarea).isComposing, + "runRemoveContentTest: the textarea still has composition #1"); + todo_is(textarea.value, "", + "runRemoveContentTest: the textarea has the committed text? #1"); + + parent.insertBefore(textarea, nextSibling); + + textarea.focus(); + textarea.value = ""; + + synthesizeComposition({ type: "compositionstart" }); + + events = []; + parent.removeChild(textarea); + + await waitForEventLoops(50); + + // XXX Currently, "input" event and "beforeinput" event are not fired on removed content. + is(events.length, 2, + "runRemoveContentTest: wrong event count #2"); + is(events[0].type, "text", + "runRemoveContentTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runRemoveContentTest: The ${events[0].type} event was fired on wrong event target #2`); + todo_is(events[1].type, "beforeinput", + "runRemoveContentTest: the 2nd event must be beforeinput #2"); + // is(events[1].target, textarea, + // `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`); + // checkInputEvent(events[1], true, "insertCompositionText", "", [], + // "runRemoveContentTest: #2"); + is(events[1].type, "compositionend", + "runRemoveContentTest: the 3rd event must be compositionend #2"); + is(events[1].target, textarea, + `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`); + is(events[1].data, "", + "runRemoveContentTest: compositionupdate has wrong data #2"); + ok(!getEditor(textarea).isComposing, + "runRemoveContentTest: the textarea still has composition #2"); + is(textarea.value, "", + "runRemoveContentTest: the textarea has the committed text? #2"); + + parent.insertBefore(textarea, nextSibling); + + textarea.removeEventListener("compositionstart", eventHandler, true); + textarea.removeEventListener("compositionupdate", eventHandler, true); + textarea.removeEventListener("compositionend", eventHandler, true); + textarea.removeEventListener("beforeinput", eventHandler, true); + textarea.removeEventListener("input", eventHandler, true); + textarea.removeEventListener("text", eventHandler, true); + + await waitForTick(); +} + +function runTestOnAnotherContext(aPanelOrFrame, aFocusedEditor, aTestName) +{ + aFocusedEditor.value = ""; + + // The frames and panel are cross-origin, and we no longer + // propagate flushes to parent cross-origin iframes explicitly, + // so flush our own layout here so the positions are correct. + document.documentElement.getBoundingClientRect(); + + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, aTestName + ": editorRect")) { + return; + } + + let r = aPanelOrFrame.getBoundingClientRect(); + let parentRect = { + left: r.left * window.devicePixelRatio, + top: r.top * window.devicePixelRatio, + width: (r.right - r.left) * window.devicePixelRatio, + height: (r.bottom - r.top) * window.devicePixelRatio, + }; + checkRectContainsRect(editorRect, parentRect, aTestName + + ": the editor rect coordinates are wrong"); + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3078\u3093\u3057\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3078\u3093\u3057\u3093", aTestName, "#1-1") || + !checkSelection(4, "", aTestName, "#1-1")) { + return; + } + + // convert them #1 + synthesizeCompositionChange( + { "composition": + { "string": "\u8FD4\u4FE1", + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u8FD4\u4FE1", aTestName, "#1-2") || + !checkSelection(2, "", aTestName, "#1-2")) { + return; + } + + // convert them #2 + synthesizeCompositionChange( + { "composition": + { "string": "\u5909\u8EAB", + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u5909\u8EAB", aTestName, "#1-3") || + !checkSelection(2, "", aTestName, "#1-3")) { + return; + } + + // commit them + synthesizeComposition({ type: "compositioncommitasis" }); + if (!checkContent("\u5909\u8EAB", aTestName, "#1-4") || + !checkSelection(2, "", aTestName, "#1-4")) { + return; + } + + is(aFocusedEditor.value, "\u5909\u8EAB", + aTestName + ": composition isn't in the focused editor"); + if (aFocusedEditor.value != "\u5909\u8EAB") { + return; + } + + let textRect = synthesizeQueryTextRect(0, 1); + let caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(textRect, + aTestName + ": synthesizeQueryTextRect") || + !checkQueryContentResult(caretRect, + aTestName + ": synthesizeQueryCaretRect")) { + return; + } + checkRectContainsRect(textRect, editorRect, aTestName + ":testRect"); + checkRectContainsRect(caretRect, editorRect, aTestName + ":caretRect"); +} + +function runFrameTest() +{ + textareaInFrame.focus(); + runTestOnAnotherContext(iframe, textareaInFrame, "runFrameTest"); + runCharAtPointTest(textareaInFrame, "textarea in the iframe"); +} + +async function runPanelTest() +{ + panel.hidden = false; + let waitOpenPopup = new Promise(resolve => { + panel.addEventListener("popupshown", resolve, {once: true}); + }); + let waitFocusTextBox = new Promise(resolve => { + textbox.addEventListener("focus", resolve, {once: true}); + }); + panel.openPopupAtScreen(window.screenX + window.outerWidth, 0, false); + await waitOpenPopup; + textbox.focus(); + await waitFocusTextBox; + is(panel.state, "open", "The panel should be open (after textbox.focus())"); + await waitForTick(); + is(panel.state, "open", "The panel should be open (after waitForTick())"); + runTestOnAnotherContext(panel, textbox, "runPanelTest"); + is(panel.state, "open", "The panel should be open (after runTestOnAnotherContext())"); + runCharAtPointTest(textbox, "textbox in the panel"); + is(panel.state, "open", "The panel should be open (after runCharAtPointTest())"); + let waitClosePopup = new Promise(resolve => { + panel.addEventListener("popuphidden", resolve, {once: true}); + }); + panel.hidePopup(); + await waitClosePopup; + await waitForTick(); +} + +// eslint-disable-next-line complexity +function runMaxLengthTest() +{ + input.maxLength = 1; + input.value = ""; + input.focus(); + + let kDesc ="runMaxLengthTest"; + + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u3089", kDesc, "#1-1") || + !checkSelection(1, "", kDesc, "#1-1")) { + return; + } + + // input second character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC", kDesc, "#1-2") || + !checkSelection(2, "", kDesc, "#1-2")) { + return; + } + + // input third character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-3") || + !checkSelection(3, "", kDesc, "#1-3")) { + return; + } + + // input fourth character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-4") || + !checkSelection(4, "", kDesc, "#1-4")) { + return; + } + + + // backspace + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-5") || + !checkSelection(3, "", kDesc, "#1-5")) { + return; + } + + // re-input + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-6") || + !checkSelection(4, "", kDesc, "#1-6")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", kDesc, "#1-7") || + !checkSelection(5, "", kDesc, "#1-7")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044", + "clauses": + [ + { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", kDesc, "#1-8") || + !checkSelection(6, "", kDesc, "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + "clauses": + [ + { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 7, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + kDesc, "#1-8") || + !checkSelection(7, "", kDesc, "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + kDesc, "#1-9") || + !checkSelection(8, "", kDesc, "#1-9")) { + return; + } + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", kDesc, "#1-10") || + !checkSelection(4, "", kDesc, "#1-10")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + if (!checkContent("\u30E9", kDesc, "#1-11") || + !checkSelection(1, "", kDesc, "#1-11")) { + return; + } + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3057", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u30E9\u3057", kDesc, "#2-1") || + !checkSelection(1 + 1, "", kDesc, "#2-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommit", data: "\u3058" }); + if (!checkContent("\u30E9", kDesc, "#2-2") || + !checkSelection(1 + 0, "", kDesc, "#2-2")) { + return; + } + + // Undo + synthesizeKey("Z", {accelKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9", kDesc, "#3-1") || + !checkSelection(1 + 0, "", kDesc, "#3-1")) { + return; + } + + // Undo + synthesizeKey("Z", {accelKey: true}); + if (!checkContent("", kDesc, "#3-2") || + !checkSelection(0, "", kDesc, "#3-2")) { + return; + } + + // Redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + if (!checkContent("\u30E9", kDesc, "#3-3") || + !checkSelection(1, "", kDesc, "#3-3")) { + return; + } + + // Redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + if (!checkContent("\u30E9", kDesc, "#3-4") || + !checkSelection(1 + 0, "", kDesc, "#3-4")) { + return; + } + + // The input element whose content length is already maxlength and + // the carest is at start of the content. + input.value = "X"; + input.selectionStart = input.selectionEnd = 0; + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u9B54", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u9B54X", kDesc, "#4-1") || + !checkSelection(1, "", kDesc, "#4-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + // The input text must be discarded. Then, the caret position shouldn't be + // updated from its position at compositionstart. + if (!checkContent("X", kDesc, "#4-2") || + !checkSelection(0, "", kDesc, "#4-2")) { + return; + } + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u9B54\u6CD5", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u9B54\u6CD5X", kDesc, "#5-1") || + !checkSelection(2, "", kDesc, "#5-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + if (checkContent("X", kDesc, "#5-2")) { + checkSelection(0, "", kDesc, "#5-2"); + } +} + +async function runEditorReframeTests() +{ + async function runEditorReframeTest(aEditor, aWindow, aEventType) + { + function getValue() + { + return aEditor == contenteditable ? + aEditor.innerHTML.replace("<br>", "") : aEditor.value; + } + + let description = "runEditorReframeTest(" + aEditor.id + ", \"" + aEventType + "\"): "; + + let tests = [ + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "a", description + "Typing 'a'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ab", description + "Typing 'b' next to 'a'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "abc", description + "Typing 'c' next to 'ab'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "abc", description + "Starting to convert 'ab][c'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ABc", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABc", description + "Starting to convert 'AB][c'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ABC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABC", description + "Starting to convert 'AB][C'"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABC", description + "Committed as 'ABC'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "d", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCd", description + "Typing 'd' next to ABC"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "de", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCde", description + "Typing 'e' next to ABCd"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "def", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCdef", description + "Typing 'f' next to ABCde"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABCdef", description + "Commit 'def' without convert"); + }, + }, + { test () { + // Select "Cd" + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Shift", {type: "keydown", shiftKey: true}); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_Shift", {type: "keyup"}); + }, + check () { + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "g", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABgef", description + "Typing 'g' next to AB"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "gh", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABghef", description + "Typing 'h' next to ABg"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ghi", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABghief", description + "Typing 'i' next to ABgh"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "GHI", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABGHIef", description + "Convert 'ghi' to 'GHI'"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABGHIef", description + "Commit 'GHI'"); + }, + }, + ]; + + function doReframe(aEvent) + { + aEvent.target.style.overflow = + aEvent.target.style.overflow != "hidden" ? "hidden" : "auto"; + } + aEditor.focus(); + aEditor.addEventListener(aEventType, doReframe); + + for (const currentTest of tests) { + currentTest.test(); + await waitForEventLoops(20); + currentTest.check(); + await waitForTick(); + } + + await new Promise(resolve => { + aEditor.style.overflow = "auto"; + aEditor.removeEventListener(aEventType, doReframe); + requestAnimationFrame(() => { SimpleTest.executeSoon(resolve); }); + }); + } + + // TODO: Add "beforeinput" case. + input.value = ""; + await runEditorReframeTest(input, window, "input"); + input.value = ""; + await runEditorReframeTest(input, window, "compositionupdate"); + textarea.value = ""; + await runEditorReframeTest(textarea, window, "input"); + textarea.value = ""; + await runEditorReframeTest(textarea, window, "compositionupdate"); + contenteditable.innerHTML = ""; + await runEditorReframeTest(contenteditable, windowOfContenteditable, "input"); + contenteditable.innerHTML = ""; + await runEditorReframeTest(contenteditable, windowOfContenteditable, "compositionupdate"); +} + +async function runIMEContentObserverTest() +{ + let notifications = []; + let onReceiveNotifications = null; + function callback(aTIP, aNotification) + { + if (aNotification.type != "notify-end-input-transaction") { + notifications.push(aNotification); + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + } + if (onReceiveNotifications) { + let resolve = onReceiveNotifications; + onReceiveNotifications = null; + SimpleTest.executeSoon(() => { + resolve(); + }); + } + return true; + } + + function dumpUnexpectedNotifications(aDescription, aExpectedCount) + { + if (notifications.length <= aExpectedCount) { + return; + } + for (let i = aExpectedCount; i < notifications.length; i++) { + ok(false, + aDescription + " caused unexpected notification: " + notifications[i].type); + } + } + + function promiseReceiveNotifications() + { + notifications = []; + return new Promise(resolve => { + onReceiveNotifications = resolve; + }); + } + + function flushNotifications() + { + return new Promise(resolve => { + // FYI: Dispatching non-op keyboard events causes forcibly flushing pending + // notifications. + synthesizeKey("KEY_Unidentified", { code: "" }); + SimpleTest.executeSoon(()=>{ + notifications = []; + resolve(); + }); + }); + } + + function ensureToRemovePrecedingPositionChangeNotification(aDescription) + { + if (!notifications.length) { + return; + } + if (notifications[0].type != "notify-position-change") { + return; + } + // Sometimes, notify-position-change is notified first separately if + // the operation causes scroll or something. Tests can ignore this. + ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it"); + notifications.shift(); + } + + // Bug 1374057 - On ubuntu 16.04 there are notify-position-change events that are + // recorded after all the other events so we remove them through this function. + function ensureToRemovePostPositionChangeNotification(aDescription, expectedCount) + { + if (!notifications.length) { + return; + } + if (notifications.length <= expectedCount) { + return; + } + if (notifications[notifications.length-1].type != "notify-position-change") { + return; + } + ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it"); + notifications.pop(); + } + + function getNativeText(aXPText) + { + if (kLF == "\n") { + return aXPText; + } + return aXPText.replace(/\n/g, kLF); + } + + function checkPositionChangeNotification(aNotification, aDescription) + { + is(!aNotification || aNotification.type, "notify-position-change", + aDescription + " should cause position change notification"); + } + + function checkSelectionChangeNotification(aNotification, aDescription, aExpected) + { + is(!aNotification || aNotification.type, "notify-selection-change", + aDescription + " should cause selection change notification"); + if (!aNotification || (aNotification.type != "notify-selection-change")) { + return; + } + is(aNotification.offset, aExpected.offset, + aDescription + " should cause selection change notification whose offset is " + aExpected.offset); + is(aNotification.text, aExpected.text, + aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'"); + is(aNotification.collapsed, !aExpected.text.length, + aDescription + " should cause selection change notification whose collapsed is " + (!aExpected.text.length)); + is(aNotification.length, aExpected.text.length, + aDescription + " should cause selection change notification whose length is " + aExpected.text.length); + is(aNotification.reversed, aExpected.reversed || false, + aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false)); + is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb", + aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb")); + } + + function checkTextChangeNotification(aNotification, aDescription, aExpected) + { + is(!aNotification || aNotification.type, "notify-text-change", + aDescription + " should cause text change notification"); + if (!aNotification || aNotification.type != "notify-text-change") { + return; + } + is(aNotification.offset, aExpected.offset, + aDescription + " should cause text change notification whose offset is " + aExpected.offset); + is(aNotification.removedLength, aExpected.removedLength, + aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength); + is(aNotification.addedLength, aExpected.addedLength, + aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength); + } + + async function testWithPlaintextEditor(aDescription, aElement, aTestLineBreaker) + { + aElement.value = ""; + aElement.blur(); + let doc = aElement.ownerDocument; + let win = doc.defaultView; + aElement.focus(); + await flushNotifications(); + + // "a[]" + let description = aDescription + "typing 'a'"; + let waitNotifications = promiseReceiveNotifications(); + synthesizeKey("a", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "ab[]" + description = aDescription + "typing 'b'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("b", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "abc[]" + description = aDescription + "typing 'c'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("c", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 3, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "ab[c]" + description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 2, text: "c", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "a[bc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: "bc", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[abc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "abc", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[]abc" + description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[a]bc" + description = aDescription + "selecting 'a' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "a" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[ab]c" + description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "ab" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[]c" + description = aDescription + "deleting 'ab' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "[]" + description = aDescription + "deleting following 'c' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 1, addedLength: 0 }); + checkPositionChangeNotification(notifications[1], description); + ensureToRemovePostPositionChangeNotification(description, 2); + dumpUnexpectedNotifications(description, 2); + + // "abc[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + await flushNotifications(); + + // "ab[]" + description = aDescription + "deleting 'c' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 1, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "[ab]" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "[]" + description = aDescription + "deleting 'ab' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "abcd[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("d", {}, win, callback); + await flushNotifications(); + + // "a[bc]d" + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a[]d" + description = aDescription + "deleting 'bc' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[bc]d" + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "aB[]d" + description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("B", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + if (!aTestLineBreaker) { + return; + } + + // "aB\n[]d" + description = aDescription + "inserting a line break after 'B' with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: kLFLen }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "aB[]d" + description = aDescription + "removing a line break after 'B' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: kLFLen, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[B]d" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a\n[]d" + description = aDescription + "replacing 'B' with a line break with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 1, addedLength: kLFLen }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[\n]d" + description = aDescription + "selecting '\n' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: kLF, reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "a[]d" + description = aDescription + "removing selected '\n' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: kLFLen, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // ab\ncd\nef\ngh\n[] + description = aDescription + "setting the value property to 'ab\ncd\nef\ngh\n'"; + waitNotifications = promiseReceiveNotifications(); + aElement.value = "ab\ncd\nef\ngh\n"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: getNativeText("ab\ncd\nef\ngh\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("ab\ncd\nef\ngh\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // [] + description = aDescription + "setting the value property to ''"; + waitNotifications = promiseReceiveNotifications(); + aElement.value = ""; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: getNativeText("ab\ncd\nef\ngh\n").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + } + + async function testWithHTMLEditor(aDescription, aElement, aDefaultParagraphSeparator) + { + const isTraditionalSplitDirection =!SpecialPowers.getBoolPref( + "editor.join_split_direction.compatible_with_the_other_browsers" + ); + + let doc = aElement.ownerDocument; + let win = doc.defaultView; + let sel = doc.getSelection(); + let inDesignMode = doc.designMode == "on"; + let offsetAtStart = 0; + let offsetAtContainer = 0; + let isDefaultParagraphSeparatorBlock = aDefaultParagraphSeparator != "br"; + doc.execCommand("defaultParagraphSeparator", false, aDefaultParagraphSeparator); + + // "[]", "<p>[]</p>" or "<div>[]</div>" + switch (aDefaultParagraphSeparator) { + case "br": + aElement.innerHTML = ""; + break; + case "p": + case "div": + // eslint-disable-next-line no-unsanitized/property + aElement.innerHTML = "<" + aDefaultParagraphSeparator + "></" + aDefaultParagraphSeparator + ">"; + sel.collapse(aElement.firstChild, 0); + offsetAtContainer = offsetAtStart + kLFLen; + break; + default: + ok(false, aDescription + "aDefaultParagraphSeparator is illegal value"); + await flushNotifications(); + return; + } + + if (inDesignMode) { + win.focus(); + } else { + aElement.focus(); + } + await flushNotifications(); + + // "a[]" + let description = aDescription + "typing 'a'"; + let waitNotifications = promiseReceiveNotifications(); + synthesizeKey("a", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "ab[]" + description = aDescription + "typing 'b'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("b", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "abc[]" + description = aDescription + "typing 'c'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("c", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 3 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "ab[c]" + description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, text: "c", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "a[bc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: "bc", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "[abc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "abc", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "[]abc" + description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "" }); + dumpUnexpectedNotifications(description, 1); + + // "[a]bc" + description = aDescription + "selecting 'a' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "a" }); + dumpUnexpectedNotifications(description, 1); + + // "[ab]c" + description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "ab" }); + dumpUnexpectedNotifications(description, 1); + + // "[]c" + description = aDescription + "deleting 'ab' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "[]" + description = aDescription + "deleting following 'c' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 1, addedLength: kLFLen }); + checkPositionChangeNotification(notifications[1], description); + dumpUnexpectedNotifications(description, 2); + + // "abc[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + await flushNotifications(); + + // "ab[]" + description = aDescription + "deleting 'c' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 1, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "[ab]" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "[]" + description = aDescription + "deleting 'ab' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "abcd[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("d", {}, win, callback); + await flushNotifications(); + + // "a[bc]d" + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a[]d" + description = aDescription + "deleting 'bc' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "a[bc]d" + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "aB[]d" + description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("B", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "aB<br>[]d" or "<block>aB</block><block>[]d</block>" + description = aDescription + "inserting a line break after 'B' with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + if (isTraditionalSplitDirection) { + // Splitting current block causes removing "<block>aB" and inserting "<block>aB</block><block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer - kLFLen, + removedLength: getNativeText("\naB").length, + addedLength: getNativeText("\naB\n").length, + }); + } else { + // Splitting current block causes removing "d</block>" and inserting "</block><block>d</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: getNativeText("d\n").length, + addedLength: getNativeText("\nd\n").length, + }); + } + } else { + // eslint-disable-next-line no-lonely-if + if (isTraditionalSplitDirection) { + // Oddly, inserting <br> causes removing "aB" and inserting "ab<br>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer, + removedLength: 2, + addedLength: getNativeText("aB\n").length, + }); + } else { + // Inserting <br> causes removing "d" and inserting "<br>d" + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: "d".length, + addedLength: getNativeText("\nd").length, + }); + } + } + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "aB[]d" + description = aDescription + "removing a line break after 'B' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + if (isTraditionalSplitDirection) { + // Joining two blocks causes removing both block elements and inserting new block element. + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer - kLFLen, + removedLength: getNativeText("\naB\nd").length, + addedLength: getNativeText("\naBd").length, + }); + } else { + // Joining two blocks causes removing "aB</block><block>d</block>" and inserting "aBd</block>" + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer, + removedLength: getNativeText("aB\nd\n").length, + addedLength: getNativeText("aBd\n").length, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: kLFLen, + addedLength: 0, + }); + is(notifications.length, 3, description + " should cause 3 notifications"); + is(notifications[1] && notifications[1].type, "notify-selection-change", description + " should cause selection change notification"); + is(notifications[2] && notifications[2].type, "notify-position-change", description + " should cause position change notification"); + } + + // "a[B]d" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a<br>[]d" or "<block>a</block><block>[]d</block>" + description = aDescription + "replacing 'B' with a line break with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + if (isTraditionalSplitDirection) { + // Splitting current block causes removing "<block>aB" and inserting "<block>aB</block><block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer - kLFLen, + removedLength: getNativeText("\naB").length, + addedLength: getNativeText("\na\n").length, + }); + } else { + // Splitting current block causes removing "Bd</block>" and inserting "</block><block>d</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: getNativeText("Bd\n").length, + addedLength: getNativeText("\nd\n").length, + }); + } + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: "B".length, + addedLength: kLFLen, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "a[<br>]d" or "<block>a[</block><block>]d</block>" + description = aDescription + "selecting '\\n' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: kLF, reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "a[]d" + description = aDescription + "removing selected '\\n' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + if (isTraditionalSplitDirection) { + // Joining the blocks causes removing "<block>a</block><block>d</block>" and inserting "<block>ad</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer - kLFLen, + removedLength: getNativeText("\na\nd").length, + addedLength: getNativeText("\nad").length, + }); + } else { + // Joining the blocks causes removing "a</block><block>d</block>" and inserting "<block>ad</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer, + removedLength: getNativeText("a\nd\n").length, + addedLength: getNativeText("ad\n").length, + }); + } + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: kLFLen, + addedLength: 0, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>" + description = aDescription + "inserting HTML which has nested block elements"; + waitNotifications = promiseReceiveNotifications(); + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + // There is <br> after the end of the line. Therefore, removed length includes a line breaker length. + if (isDefaultParagraphSeparatorBlock) { + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\nad\n").length, addedLength: getNativeText("\n1\n2\n345").length }); + } else { + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2 + kLFLen, addedLength: getNativeText("\n1\n2\n345").length }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>3</div>4</div>]5</div>" and removing selection + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>15</div>", description + " should remove '<div>2<div>3</div>4</div>'"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>3</div>]4</div>5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #1) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>145</div>", description + " should remove '<div>2<div>3</div></div>'"); + // It causes removing '<div>2<div>3</div>4</div>' and inserting '4'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>]3</div>4</div>5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #2) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>13<div>4</div>5</div>", description + " should remove '<div>2</div>'"); + // It causes removing '1<div>2<div>3</div></div>' and inserting '13<div>'. + checkTextChangeNotification(notifications[0], description, { offset: kLFLen + offsetAtStart, removedLength: getNativeText("1\n2\n3").length, addedLength: getNativeText("13\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>]5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #3) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>35</div></div></div>", description + " should remove '4'"); + // It causes removing '45' and inserting '5'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>" + description = aDescription + "inserting HTML which has a pair of nested block elements"; + waitNotifications = promiseReceiveNotifications(); + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtStart, removedLength: getNativeText("\n1\n2\n35").length, addedLength: getNativeText("\n1\n2\n345\n6\n789").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between same level descendants) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>37</div></div><div>8</div>9</div>", description + " should remove '456<div>7'"); + // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>37</div><div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n7").length, addedLength: getNativeText("\n37\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #1) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>27</div><div>8</div>9</div>", description + " should remove '<div>2<div>3</div>4</div>5<div>6<div>7</div>'"); + // It causes removing '<div>2<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>27</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n345\n6\n7").length, addedLength: getNativeText("\n27\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #2) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>29</div></div>", description + " should remove '<div>3</div>4</div>5<div>6<div>7</div>8</div>'"); + // It causes removing '<div>3</div>4</div>5</div>6<div>7</div>8</div>9' and inserting '9</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n789").length, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>]6<div>7</div>8</div>9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #3) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>36<div>7</div>8</div></div>9</div>", description + " should remove '<div>36<div>7</div>8</div>'"); + // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n78").length, addedLength: getNativeText("\n36\n78").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #4) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>39</div></div></div>", description + " should remove '</div>4</div>5<div>6<div>7</div>8</div>'"); + // It causes removing '</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: getNativeText("45\n6\n789").length, addedLength: getNativeText("9").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<p>abc</p><p><br></p><p>{<br>}</p>" and removing second paragraph with DOM API + aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>"; + sel.collapse(aElement.firstChild.nextSibling.nextSibling, 0); + await flushNotifications(); + description = aDescription + "deleting previous paragraph with DOM API"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications + aElement.firstChild.nextSibling.remove(); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the second paragraph should've been removed"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\nabc\n").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<p>abc</p><p>{<br>}</p><p><br></p>" and removing last paragraph with DOM API + aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>"; + sel.collapse(aElement.firstChild.nextSibling, 0); + await flushNotifications(); + description = aDescription + "deleting next paragraph with DOM API"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications + aElement.firstChild.nextSibling.nextSibling.remove(); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the last paragraph should've been removed"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc\n\n").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 }); + checkPositionChangeNotification(notifications[1], description); + dumpUnexpectedNotifications(description, 2); + } + + await testWithPlaintextEditor("runIMEContentObserverTest with input element: ", input, false); + await testWithPlaintextEditor("runIMEContentObserverTest with textarea element: ", textarea, true); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is br): ", contenteditable, "br"); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is p): ", contenteditable, "p"); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is div): ", contenteditable, "div"); + // XXX Due to the difference of HTML editor behavior between designMode and contenteditable, + // testWithHTMLEditor() gets some unexpected behavior. However, IMEContentObserver is + // not depend on editor's detail. So, we should investigate this issue later. It's not + // so important for now. + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is br): ", iframe2.contentDocument.body, "br"); + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is p): ", iframe2.contentDocument.body, "p"); + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is div): ", iframe2.contentDocument.body, "div"); +} + +async function runPasswordMaskDelayTest() { + await SpecialPowers.pushPrefEnv({ + set: [["editor.password.mask_delay", 600], + ["editor.password.testing.mask_delay", true], + ], + }); + + let iframe5 = document.getElementById("iframe5"); + let iframe6 = document.getElementById("iframe6"); + let inputWindow = iframe5.contentWindow; + let passwordWindow = iframe6.contentWindow; + + let inputElement = iframe5.contentDocument.getElementById("input"); + let passwordElement = iframe6.contentDocument.getElementById("password"); + + const kMask = passwordElement.editor.passwordMask; + + function promiseAllPasswordMasked() { + return new Promise(resolve => { + passwordElement.addEventListener("MozLastInputMasked", resolve, {once: true}); + }); + } + + function checkSnapshots(aResult, aReference, aMatch, aDescription) { + let [correct, data1, data2] = compareSnapshots(aResult, aReference, true); + is(correct, aMatch, `${aDescription}\nREFTEST IMAGE 1 (TEST): ${data1}\nREFTEST IMAGE 2 (REFERENCE): ${data2}`); + } + + // First character input + passwordElement.value = ""; + passwordElement.focus(); + let waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("a"); + let unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + let maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = "a"; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + let unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = kMask; + inputElement.setSelectionRange(1, 1); + let maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): first inputted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): first inputted character should be masked after a while"); + + // Second character input + passwordElement.value = "a"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("b"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}b`; + inputElement.focus(); + inputElement.setSelectionRange(2, 2); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}`; + inputElement.setSelectionRange(2, 2); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): second inputted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): second inputted character should be masked after a while"); + + // Typing new character should mask the previous unmasked characters + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(2, 2); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("c"); + synthesizeKey("d"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}${kMask}${kMask}d`; + inputElement.focus(); + inputElement.setSelectionRange(4, 4); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(4, 4); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): forth character input should mask the third character"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): forth inputted character should be masked after a while"); + + // Typing middle of password should unmask the last input character + passwordElement.value = "abcd"; + passwordElement.focus(); + passwordElement.setSelectionRange(2, 2); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("e"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}${kMask}e${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(3, 3); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(3, 3); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): inserted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): inserted character should be masked after a while"); + + // Composition string should be unmasked for a while, and shouldn't be committed at masking + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #1"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: kMask, + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): composing character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): composing character should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Updating composition string should unmask the composition string for a while + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "d", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #2"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "d", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: kMask, + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): updated composing character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): updated composing character should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Composing multi-characters should be unmasked for a while. + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #3"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: `${kMask}${kMask}`, + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): all of composing string should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): all of composing string should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Committing composition should make the commit string unmasked. + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}cd${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(3, 3); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(3, 3); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): committed string should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): committed string should be masked after a while"); +} + +async function runInputModeTest() +{ + await SpecialPowers.setBoolPref("dom.forms.inputmode", true); + + let result = []; + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.inputMode = "text"; + textarea.value = ""; + textarea.focus(); + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + synthesizeCompositionChange({ + composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + + is(result[0].type, "compositionupdate", "Set initial composition for inputmode test"); + result = []; + + textarea.inputMode = "tel"; + is(result.length, 0, "No compositonend event even if inputmode is updated"); + + // Clean up + synthesizeComposition({ type: "compositioncommitasis" }); + textarea.inputMode = ""; + textarea.value = ""; + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + + await SpecialPowers.clearUserPref("dom.forms.inputmode"); +} + + +async function runTest() +{ + window.addEventListener("unload", window.arguments[0].SimpleTest.finish, {once: true, capture: true}); + + contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable"); + windowOfContenteditable = document.getElementById("iframe4").contentWindow; + textareaInFrame = iframe.contentDocument.getElementById("textarea"); + + contenteditableBySpan = document.getElementById("iframe7").contentDocument.getElementById("contenteditable"); + windowOfContenteditableBySpan = document.getElementById("iframe7").contentWindow; + + await runIMEContentObserverTest(); + await runEditorReframeTests(); + await runAsyncForceCommitTest(); + await runRemoveContentTest(); + await runPanelTest(); + await runPasswordMaskDelayTest(); + await runBug1584901Test(); + await runInputModeTest(); + await runCompositionTest(); + await runCompositionCommitTest(); + await runSetSelectionEventTest(); + + runUndoRedoTest(); + runCompositionCommitAsIsTest(); + runCompositionEventTest(); + runCompositionTestWhoseTextNodeModified(); + runQueryTextRectInContentEditableTest(); + runCharAtPointTest(textarea, "textarea in the document"); + runCharAtPointAtOutsideTest(); + runQueryTextContentEventTest(); + runQuerySelectionEventTest(); + runQueryIMESelectionTest(); + runQueryContentEventRelativeToInsertionPoint(); + runQueryPasswordTest(); + runCSSTransformTest(); + runBug722639Test(); + runBug1375825Test(); + runBug1530649Test(); + runBug1571375Test(); + runBug1675313Test(); + runCommitCompositionWithSpaceKey(); + runCompositionWithSelectionChange(); + runForceCommitTest(); + runNestedSettingValue(); + runBug811755Test(); + runIsComposingTest(); + runRedundantChangeTest(); + runNotRedundantChangeTest(); + runNativeLineBreakerTest(); + runControlCharTest(); + runFrameTest(); + runMaxLengthTest(); + + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); + +]]> +</script> + +</window> |