--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -54,16 +54,18 @@ function finish()
window.close();
}
function onunload()
{
SimpleTest.finish();
}
+const kIsMac = (navigator.platform.indexOf("Mac") == 0);
+
var iframe = document.getElementById("iframe");
var childWindow = iframe.contentWindow;
var textareaInFrame;
var input = document.getElementById("input");
var otherWindow = window.opener;
var otherDocument = otherWindow.document;
var inputInChildWindow = otherDocument.getElementById("input");
@@ -3781,17 +3783,17 @@ function runCommitCompositionTests()
TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
TIP.setCaretInPendingComposition(3);
TIP.flushPendingComposition();
doCommitWithNullCheck(undefined);
is(input.value, "",
description + "doCommitWithNullCheck(undefined) should commit the composition with empty string");
}
-function runUnloadTests1(aNextTest)
+function runUnloadTests1()
{
var description = "runUnloadTests1(): ";
var TIP1 = createTIP();
ok(TIP1.beginInputTransactionForTests(childWindow),
description + "TIP1.beginInputTransactionForTests() should succeed");
var oldSrc = iframe.src;
@@ -3803,17 +3805,17 @@ function runUnloadTests1(aNextTest)
childWindow = iframe.contentWindow;
textareaInFrame = null;
iframe.addEventListener("load", function () {
ok(true, description + "old iframe is restored");
// And also restore the iframe information with restored contents.
iframe.removeEventListener("load", arguments.callee, true);
childWindow = iframe.contentWindow;
textareaInFrame = iframe.contentDocument.getElementById("textarea");
- setTimeout(aNextTest, 0);
+ SimpleTest.executeSoon(continueTest);
}, true);
// The composition should be committed internally. So, another TIP should
// be able to steal the rights to using TextEventDispatcher.
var TIP2 = createTIP();
ok(TIP2.beginInputTransactionForTests(parentWindow),
description + "TIP2.beginInputTransactionForTests() should succeed");
@@ -3843,17 +3845,17 @@ function runUnloadTests1(aNextTest)
TIP1.flushPendingComposition();
is(textareaInFrame.value, "foo",
description + "the textarea in the iframe should have composition string");
// Load different web page on the frame.
iframe.src = "data:text/html,<body>dummy page</body>";
}
-function runUnloadTests2(aNextTest)
+function runUnloadTests2()
{
var description = "runUnloadTests2(): ";
var TIP = createTIP();
ok(TIP.beginInputTransactionForTests(childWindow),
description + "TIP.beginInputTransactionForTests() should succeed");
var oldSrc = iframe.src;
@@ -3865,17 +3867,17 @@ function runUnloadTests2(aNextTest)
childWindow = iframe.contentWindow;
textareaInFrame = null;
iframe.addEventListener("load", function () {
ok(true, description + "old iframe is restored");
// And also restore the iframe information with restored contents.
iframe.removeEventListener("load", arguments.callee, true);
childWindow = iframe.contentWindow;
textareaInFrame = iframe.contentDocument.getElementById("textarea");
- setTimeout(aNextTest, 0);
+ SimpleTest.executeSoon(continueTest);
}, true);
input.focus();
input.value = "";
// TIP should be still available in the same top level widget.
TIP.setPendingCompositionString("bar");
TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
@@ -3906,53 +3908,121 @@ function runUnloadTests2(aNextTest)
TIP.flushPendingComposition();
is(textareaInFrame.value, "foo",
description + "the textarea in the iframe should have composition string");
// Load different web page on the frame.
iframe.src = "data:text/html,<body>dummy page</body>";
}
-function runCallbackTests(aForTests)
+function* runCallbackTests(aForTests)
{
var description = "runCallbackTests(aForTests=" + aForTests + "): ";
input.value = "";
input.focus();
input.blur();
var TIP = createTIP();
var notifications = [];
+ var callContinueTest = false;
function callback(aTIP, aNotification)
{
+ if (aTIP == TIP) {
+ notifications.push(aNotification);
+ }
switch (aNotification.type) {
case "request-to-commit":
aTIP.commitComposition();
break;
case "request-to-cancel":
aTIP.cancelComposition();
break;
}
- if (aTIP == TIP) {
- notifications.push(aNotification);
+ if (callContinueTest) {
+ callContinueTest = false;
+ SimpleTest.executeSoon(continueTest);
}
return true;
}
function dumpUnexpectedNotifications(aExpectedCount)
{
if (notifications.length <= aExpectedCount) {
return;
}
for (var i = aExpectedCount; i < notifications.length; i++) {
ok(false,
description + "Unexpected notification: " + notifications[i].type);
}
}
+ function waitUntilNotificationsReceived()
+ {
+ if (notifications.length > 0) {
+ SimpleTest.executeSoon(continueTest);
+ } else {
+ callContinueTest = true;
+ }
+ }
+
+ function checkPositionChangeNotification(aNotification, aDescription)
+ {
+ is(!aNotification || aNotification.type, "notify-position-change",
+ aDescription + " should cause position change notification");
+ }
+
+ function checkSelectionChangeNotification(aNotification, aDescription, aExpected)
+ {
+ is(aNotification.type, "notify-selection-change",
+ aDescription + " should cause selection change notification");
+ if (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 == 0,
+ aDescription + " should cause selection change notification whose collapsed is " + (aExpected.text.length == 0));
+ 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"));
+ is(aNotification.causedByComposition, aExpected.causedByComposition || false,
+ aDescription + " should cause selection change notification whose causedByComposition is " + (aExpected.causedByComposition || false));
+ is(aNotification.causedBySelectionEvent, aExpected.causedBySelectionEvent || false,
+ aDescription + " should cause selection change notification whose causedBySelectionEvent is " + (aExpected.causedBySelectionEvent || false));
+ is(aNotification.occurredDuringComposition, aExpected.occurredDuringComposition || false,
+ aDescription + " should cause cause selection change notification whose occurredDuringComposition is " + (aExpected.occurredDuringComposition || false));
+ }
+
+ function checkTextChangeNotification(aNotification, aDescription, aExpected)
+ {
+ is(aNotification.type, "notify-text-change",
+ aDescription + " should cause text change notification");
+ if (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);
+ is(aNotification.causedOnlyByComposition, aExpected.causedOnlyByComposition || false,
+ aDescription + " should cause text change notification whose causedOnlyByComposition is " + (aExpected.causedOnlyByComposition || false));
+ is(aNotification.includingChangesDuringComposition, aExpected.includingChangesDuringComposition || false,
+ aDescription + " should cause text change notification whose includingChangesDuringComposition is " + (aExpected.includingChangesDuringComposition || false));
+ is(aNotification.includingChangesWithoutComposition, typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true,
+ aDescription + " should cause text change notification whose includingChangesWithoutComposition is " + (typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true));
+ }
+
if (aForTests) {
TIP.beginInputTransactionForTests(window, callback);
} else {
TIP.beginInputTransaction(window, callback);
}
notifications = [];
input.focus();
@@ -3966,58 +4036,151 @@ function runCallbackTests(aForTests)
input.blur();
is(notifications.length, 1,
description + "input.blur() should cause a notification");
is(notifications[0].type, "notify-blur",
description + "input.blur() should cause \"notify-focus\"");
dumpUnexpectedNotifications(1);
input.focus();
+ notifications = [];
TIP.setPendingCompositionString("foo");
TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
TIP.flushPendingComposition();
+ is(notifications.length, 3,
+ description + "creating composition string 'foo' should cause 3 notifications");
+ checkTextChangeNotification(notifications[0], description + "creating composition string 'foo'",
+ { offset: 0, removedLength: 0, addedLength: 3,
+ causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
+ checkSelectionChangeNotification(notifications[1], description + "creating composition string 'foo'",
+ { offset: 3, text: "", causedByComposition: true, occurredDuringComposition: true });
+ checkPositionChangeNotification(notifications[2], description + "creating composition string 'foo'");
+ dumpUnexpectedNotifications(3);
+
notifications = [];
synthesizeMouseAtCenter(input, {});
- is(notifications.length, 1,
- description + "synthesizeMouseAtCenter(input, {}) during composition should cause a notification");
+ is(notifications.length, 3,
+ description + "synthesizeMouseAtCenter(input, {}) during composition should cause 3 notifications");
is(notifications[0].type, "request-to-commit",
description + "synthesizeMouseAtCenter(input, {}) during composition should cause \"request-to-commit\"");
+ checkTextChangeNotification(notifications[1], description + "synthesizeMouseAtCenter(input, {}) during composition",
+ { offset: 0, removedLength: 3, addedLength: 3,
+ causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
+ checkPositionChangeNotification(notifications[2], description + "synthesizeMouseAtCenter(input, {}) during composition");
+ dumpUnexpectedNotifications(3);
+
+ // XXX On macOS, window.moveBy() doesn't cause notify-position-change.
+ // Investigate this later (although, we cannot notify position change to
+ // native IME on macOS).
+ if (!kIsMac) {
+ input.focus();
+ notifications = [];
+ window.moveBy(0, 10);
+ yield waitUntilNotificationsReceived();
+ is(notifications.length, 1,
+ description + "window.moveBy(0, 10) should cause a notification");
+ checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)");
+ dumpUnexpectedNotifications(1);
+
+ input.focus();
+ notifications = [];
+ window.moveBy(10, 0);
+ yield waitUntilNotificationsReceived();
+ is(notifications.length, 1,
+ description + "window.moveBy(10, 0) should cause a notification");
+ checkPositionChangeNotification(notifications[0], description + "window.moveBy(10, 0)");
+ dumpUnexpectedNotifications(1);
+ }
+
+ input.focus();
+ input.value = "abc"
+ notifications = [];
+ input.selectionStart = input.selectionEnd = 0;
+ yield waitUntilNotificationsReceived();
+ notifications = [];
+ var rightArrowKeyEvent =
+ new KeyboardEvent("", { key: "ArrowRight", code: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT });
+ TIP.keydown(rightArrowKeyEvent);
+ TIP.keyup(rightArrowKeyEvent);
+ is(notifications.length, 1,
+ description + "ArrowRight key press should cause a notification");
+ checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press", { offset: 1, text: "" });
+ dumpUnexpectedNotifications(1);
+
+ notifications = [];
+ var shiftKeyEvent =
+ new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT });
+ var leftArrowKeyEvent =
+ new KeyboardEvent("", { key: "ArrowLeft", code: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT });
+ TIP.keydown(shiftKeyEvent);
+ TIP.keydown(leftArrowKeyEvent);
+ TIP.keyup(leftArrowKeyEvent);
+ TIP.keyup(shiftKeyEvent);
+ is(notifications.length, 1,
+ description + "ArrowLeft key press with Shift should cause a notification");
+ checkSelectionChangeNotification(notifications[0], description + "ArrowLeft key press with Shift", { offset: 0, text: "a", reversed: true });
+ dumpUnexpectedNotifications(1);
+
+ TIP.keydown(rightArrowKeyEvent);
+ TIP.keyup(rightArrowKeyEvent);
+ notifications = [];
+ TIP.keydown(shiftKeyEvent);
+ TIP.keydown(rightArrowKeyEvent);
+ TIP.keyup(rightArrowKeyEvent);
+ TIP.keyup(shiftKeyEvent);
+ is(notifications.length, 1,
+ description + "ArrowRight key press with Shift should cause a notification");
+ checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press with Shift", { offset: 1, text: "b" });
dumpUnexpectedNotifications(1);
notifications = [];
var TIP2 = createTIP();
if (aForTests) {
TIP2.beginInputTransactionForTests(window, callback);
} else {
TIP2.beginInputTransaction(window, callback);
}
is(notifications.length, 1,
description + "Initializing another TIP should cause a notification");
is(notifications[0].type, "notify-end-input-transaction",
description + "Initializing another TIP should cause \"notify-detached\"");
dumpUnexpectedNotifications(1);
}
-function runTests()
+var gTestContinuation = null;
+
+function continueTest()
{
- textareaInFrame = iframe.contentDocument.getElementById("textarea");
-
+ if (!gTestContinuation) {
+ gTestContinuation = testBody();
+ }
+ var ret = gTestContinuation.next();
+ if (ret.done) {
+ finish();
+ }
+}
+
+function* testBody()
+{
runBeginInputTransactionMethodTests();
runReleaseTests();
runCompositionTests();
runCompositionWithKeyEventTests();
runConsumingKeydownBeforeCompositionTests();
runKeyTests();
runErrorTests();
runCommitCompositionTests();
- runCallbackTests(false);
- runCallbackTests(true);
- runUnloadTests1(function () {
- runUnloadTests2(function () {
- finish();
- });
- });
+ yield* runCallbackTests(false);
+ yield* runCallbackTests(true);
+ yield runUnloadTests1();
+ yield runUnloadTests2();
+}
+
+function runTests()
+{
+ textareaInFrame = iframe.contentDocument.getElementById("textarea");
+ continueTest();
}
]]>
</script>
</window>
--- a/dom/interfaces/base/nsITextInputProcessorCallback.idl
+++ b/dom/interfaces/base/nsITextInputProcessorCallback.idl
@@ -39,18 +39,153 @@ interface nsITextInputProcessorNotificat
* "notify-focus" (optional)
* This is notified when an editable editor gets focus and Gecko starts
* to observe changes in the content. E.g., selection changes.
* IME shouldn't change DOM tree, focus nor something when this is notified.
*
* "notify-blur" (optional)
* This is notified when an editable editor loses focus and Gecko stops
* observing the changes in the content.
+ *
+ * "notify-text-change" (optional)
+ * This is notified when text in the focused editor is modified.
+ * Some attributes below are available to retrieve the detail.
+ * IME shouldn't change DOM tree, focus nor something when this is notified.
+ * Note that when there is no chance to notify you of some text changes
+ * safely, this represents all changes as a change.
+ *
+ * "notify-selection-change" (optional)
+ * This is notified when selection in the focused editor is changed.
+ * Some attributes below are available to retrieve the detail.
+ * IME shouldn't change DOM tree, focus nor something when this is notified.
+ * Note that when there was no chance to notify you of this safely, this
+ * represents the latest selection change.
+ *
+ * "notify-position-change" (optional)
+ * This is notified when layout is changed in the editor or the window
+ * is moved.
+ * IME shouldn't change DOM tree, focus nor something when this is notified.
+ * Note that when there was no chance to notify you of this safely, this
+ * represents the latest layout change.
*/
readonly attribute ACString type;
+
+ /**
+ * Be careful, line breakers in the editor are treated as native line
+ * breakers. I.e., on Windows, a line breaker is "\r\n" (CRLF). On the
+ * others, it is "\n" (LF). If you want TextInputProcessor to treat line
+ * breakers on Windows as XP line breakers (LF), please file a bug with
+ * the reason why you need the behavior.
+ */
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change" or
+ * "notify-selection-change".
+ * This is offset of the start of modified text range if type is
+ * "notify-text-change". Or offset of start of selection if type is
+ * "notify-selection-change".
+ */
+ readonly attribute unsigned long offset;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * This is selected text. I.e., the length is selected length and
+ * it's empty if the selection is collapsed.
+ */
+ readonly attribute AString text;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * This is set to true when the selection is collapsed. Otherwise, false.
+ */
+ readonly attribute boolean collapsed;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * This is selected length. I.e., if this is 0, collapsed is set to true.
+ */
+ readonly attribute uint32_t length;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * When selection is created from latter point to former point, this is
+ * set to true. Otherwise, false.
+ * I.e., if this is true, offset + length is the anchor of selection.
+ */
+ readonly attribute boolean reversed;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * This indicates the start of the selection's writing mode.
+ * The value can be "horizontal-tb", "vertical-rl" or "vertical-lr".
+ */
+ readonly attribute ACString writingMode;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * If the selection change was caused by composition, this is set to true.
+ * Otherwise, false.
+ */
+ readonly attribute boolean causedByComposition;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * If the selection change was caused by selection event, this is set to true.
+ * Otherwise, false.
+ */
+ readonly attribute boolean causedBySelectionEvent;
+
+ /**
+ * This attribute has a valid value when type is "notify-selection-change".
+ * If the selection change occurred during composition, this is set to true.
+ * Otherwise, false.
+ */
+ readonly attribute boolean occurredDuringComposition;
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change".
+ * This is removed text length by the change(s). If this is empty, new text
+ * was just inserted. Otherwise, the text is replaced with new text.
+ */
+ readonly attribute unsigned long removedLength;
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change".
+ * This is added text length by the change(s). If this is empty, old text
+ * was just deleted. Otherwise, the text replaces the old text.
+ */
+ readonly attribute unsigned long addedLength;
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change".
+ * If the text change(s) was caused only by composition, this is set to true.
+ * Otherwise, false. I.e., if one of text changes are caused by JS or
+ * modifying without composition, this is set to false.
+ */
+ readonly attribute boolean causedOnlyByComposition;
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change".
+ * If at least one text change not caused by composition occurred during
+ * composition, this is set to true. Otherwise, false.
+ * Note that this is set to false when new change is caused by neither
+ * composition nor occurred during composition because it's outdated for
+ * new composition.
+ * In other words, when text changes not caused by composition occurred
+ * during composition and it may cause committing composition, this is
+ * set to true.
+ */
+ readonly attribute boolean includingChangesDuringComposition;
+
+ /**
+ * This attribute has a valid value when type is "notify-text-change".
+ * If at least one text change occurred when there was no composition, this
+ * is set to true. Otherwise, false.
+ */
+ readonly attribute boolean includingChangesWithoutComposition;
};
/**
* nsITextInputProcessorCallback is a callback interface for JS to implement
* IME. IME implemented by JS can implement onNotify() function and must send
* it to nsITextInputProcessor at initializing. Then, onNotify() will be
* called with nsITextInputProcessorNotification instance.
* The reason why onNotify() uses string simply is that if we will support