summaryrefslogtreecommitdiffstats
path: root/widget/tests/window_composition_text_querycontent.xhtml
diff options
context:
space:
mode:
Diffstat (limited to 'widget/tests/window_composition_text_querycontent.xhtml')
-rw-r--r--widget/tests/window_composition_text_querycontent.xhtml10977
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,&lt;textarea id='textarea' cols='20' rows='4'&gt;&lt;/textarea&gt;"></iframe><br/>
+<iframe id="iframe2" width="300" height="150"
+ src="data:text/html,&lt;body onload='document.designMode=%22on%22'&gt;body content&lt;/body&gt;"></iframe><br/>
+<iframe id="iframe3" width="300" height="150"
+ src="data:text/html,&lt;body onload='document.designMode=%22on%22'&gt;body content&lt;/body&gt;"></iframe><br/>
+<iframe id="iframe4" width="300" height="150"
+ src="data:text/html,&lt;div contenteditable id='contenteditable'&gt;&lt;/div&gt;"></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,&lt;input id='input'&gt;"></iframe>
+<iframe id="iframe6" style="width:10ch" height="50" src="data:text/html,&lt;input id='password' type='password'&gt;"></iframe><br/>
+<iframe id="iframe7" width="300" height="150"
+ src="data:text/html,&lt;span contenteditable id='contenteditable'&gt;&lt;/span&gt;"></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&nbsp;",
+ "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&nbsp;",
+ "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 &nbsp; 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>