diff options
Diffstat (limited to 'dom/base/test/chrome/window_nsITextInputProcessor.xhtml')
-rw-r--r-- | dom/base/test/chrome/window_nsITextInputProcessor.xhtml | 4750 |
1 files changed, 4750 insertions, 0 deletions
diff --git a/dom/base/test/chrome/window_nsITextInputProcessor.xhtml b/dom/base/test/chrome/window_nsITextInputProcessor.xhtml new file mode 100644 index 0000000000..291a9e4324 --- /dev/null +++ b/dom/base/test/chrome/window_nsITextInputProcessor.xhtml @@ -0,0 +1,4750 @@ +<?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 nsITextInputProcessor behavior" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onunload="onunload();"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="display"> +<input id="input" type="text"/><input id="anotherInput" type="text"/><br/> +<textarea></textarea> +<iframe id="iframe" width="300" height="150" + src="data:text/html,<textarea id='textarea' cols='20' rows='4'></textarea>"></iframe><br/> +<div contenteditable=""><br/></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var SimpleTest = window.arguments[0].SimpleTest; + +SimpleTest.waitForFocus(runTests, window); + +function getHTMLEditor(aWindow) { + return SpecialPowers.wrap(aWindow).docShell.editingSession?.getEditorForWindow(aWindow); +} + +function ok(aCondition, aMessage) +{ + SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function todo_is(aLeft, aRight, aMessage) +{ + SimpleTest.todo_is(aLeft, aRight, aMessage); +} + +function info(aMessage) { + SimpleTest.info(aMessage); +} + +function finish() +{ + window.close(); +} + +function onunload() +{ + SimpleTest.finish(); +} + +function checkInputEvent(aEvent, aCancelable, aIsComposing, aInputType, aData, aDescription) { + if (aEvent.type !== "input" && aEvent.type !== "beforeinput") { + throw new Error(`${aDescription}"${aEvent.type}" is not InputEvent`); + } + ok(InputEvent.isInstance(aEvent), `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface`); + is(aEvent.cancelable, aCancelable, `${aDescription}"${aEvent.type}" event should ${aCancelable ? "be" : "not be"} cancelable`); + is(aEvent.bubbles, true, `${aDescription}"${aEvent.type}" event should always bubble`); + is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing of "${aEvent.type}" event should be ${aIsComposing}`); + is(aEvent.inputType, aInputType, `${aDescription}inputType of "${aEvent.type}" event should be "${aInputType}"`); + is(aEvent.data, aData, `${aDescription}data of "${aEvent.type}" event should be "${aData}"`); + is(aEvent.dataTransfer, null, `${aDescription}dataTransfer of "${aEvent.type}" event should be null`); + is(aEvent.getTargetRanges().length, 0, `${aDescription}getTargetRanges() of "${aEvent.type}" event should return empty array`); +} + +const kIsMac = (navigator.platform.indexOf("Mac") == 0); + +const iframe = document.getElementById("iframe"); +let childWindow = iframe.contentWindow; +let textareaInFrame; +const input = document.getElementById("input"); +const textarea = document.querySelector("textarea"); +const otherWindow = window.arguments[0]; +const otherDocument = otherWindow.document; +const inputInChildWindow = otherDocument.getElementById("input"); +const contenteditable = document.querySelector("div[contenteditable]"); + +const kLF = navigator.platform.startsWith("Win") ? "\r\n" : "\n"; +function getNativeText(aXPText) +{ + if (kLF == "\n") { + return aXPText; + } + return aXPText.replace(/\n/g, kLF); +} + +function createTIP() +{ + return Cc["@mozilla.org/text-input-processor;1"]. + createInstance(Ci.nsITextInputProcessor); +} + +function runBeginInputTransactionMethodTests() +{ + var description = "runBeginInputTransactionMethodTests: "; + input.value = ""; + input.focus(); + + var simpleCallback = function (aTIP, aNotification) + { + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + } + return true; + }; + + var TIP1 = createTIP(); + var TIP2 = createTIP(); + isnot(TIP1, TIP2, + description + "TIP instances should be different"); + + // beginInputTransaction() and beginInputTransactionForTests() can take ownership if there is no composition. + ok(TIP1.beginInputTransaction(window, simpleCallback), + description + "TIP1.beginInputTransaction(window) should succeed because there is no composition"); + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests(window) should succeed because there is no composition"); + ok(TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2.beginInputTransaction(window) should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests(window) should succeed because there is no composition"); + + // Start composition with TIP1, then, other TIPs cannot take ownership during a composition. + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + var composingStr = "foo"; + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + ok(TIP1.flushPendingComposition(), + description + "TIP1.flushPendingComposition() should return true becuase it should be valid composition"); + is(input.value, composingStr, + description + "The input element should have composing string"); + + // Composing nsITextInputProcessor instance shouldn't allow initialize it again. + try { + TIP1.beginInputTransaction(window, simpleCallback); + ok(false, + "TIP1.beginInputTransaction(window) should cause throwing an exception because it's composing with different purpose"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(window) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing for tests"); + } + try { + TIP1.beginInputTransactionForTests(otherWindow); + ok(false, + "TIP1.beginInputTransactionForTests(otherWindow) should cause throwing an exception because it's composing on different window"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing on this window"); + } + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests(window) should succeed because TextEventDispatcher was initialized with same purpose"); + ok(TIP1.beginInputTransactionForTests(childWindow), + description + "TIP1.beginInputTransactionForTests(childWindow) should succeed because TextEventDispatcher was initialized with same purpose and is shared by window and childWindow"); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2.beginInputTransaction(window) should not succeed because there is composition synthesized by TIP1"); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests(window) should not succeed because there is composition synthesized by TIP1"); + ok(!TIP2.beginInputTransaction(childWindow, simpleCallback), + description + "TIP2.beginInputTransaction(childWindow) should not succeed because there is composition synthesized by TIP1"); + ok(!TIP2.beginInputTransactionForTests(childWindow), + description + "TIP2.beginInputTransactionForTests(childWindow) should not succeed because there is composition synthesized by TIP1"); + ok(TIP2.beginInputTransaction(otherWindow, simpleCallback), + description + "TIP2.beginInputTransaction(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window"); + ok(TIP2.beginInputTransactionForTests(otherWindow), + description + "TIP2.beginInputTransactionForTests(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window"); + + // Let's confirm that the composing string is NOT committed by above tests. + TIP1.commitComposition(); + is(input.value, composingStr, + description + "TIP1.commitString() without specifying commit string should commit current composition with the last composing string"); + ok(!TIP1.hasComposition, + description + "TIP1.commitString() without specifying commit string should've end composition"); + + ok(TIP1.beginInputTransaction(window, simpleCallback), + description + "TIP1.beginInputTransaction() should succeed because there is no composition #2"); + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition #2"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because the composition was already committed #2"); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during startComposition(). + var events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + input.removeEventListener(aEvent.type, arguments.callee, false); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.startComposition();"); + }, false); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.startComposition(); + is(events.length, 1, + description + "compositionstart event should be fired by TIP1.startComposition()"); + TIP1.cancelComposition(); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during flushPendingComposition(). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + is(events.length, 5, + description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description); + TIP1.cancelComposition(); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition(). + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + TIP1.commitComposition(); + is(events.length, 4, + description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()"); + is(events[0].type, "text", + description + "events[0] should be text"); + is(events[1].type, "beforeinput", + description + "events[1] should be beforeinput"); + checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description); + is(events[2].type, "compositionend", + description + "events[2] should be compositionend"); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertCompositionText", composingStr, description); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar"). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction during text event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction during compositionend event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction during beforeinput event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction during input event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.commitCompositionWith("bar"); + is(events.length, 6, + description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description); + is(events[4].type, "compositionend", + description + "events[4] should be compositionend"); + is(events[5].type, "input", + description + "events[5] should be input"); + checkInputEvent(events[5], false, false, "insertCompositionText", "bar", description); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition(). + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + TIP1.cancelComposition(); + is(events.length, 5, + description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()"); + is(events[0].type, "compositionupdate", + description + "events[0] should be compositionupdate"); + is(events[1].type, "text", + description + "events[1] should be text"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], false, true, "insertCompositionText", "", description); + is(events[3].type, "compositionend", + description + "events[3] should be compositionend"); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, false, "insertCompositionText", "", description); + + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup(). + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + input.addEventListener("keydown", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from keydown event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("keypress", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from keypress event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("keyup", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransaction(window, simpleCallback), + description + "TIP2 shouldn't be able to begin input transaction from keyup event handler during a call of TIP1.keyup();"); + }, {once: true}); + var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + TIP1.keydown(keyA); + TIP1.keyup(keyA); + is(events.length, 5, + description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()"); + is(events[0].type, "keydown", + description + "events[0] should be keydown"); + is(events[1].type, "keypress", + description + "events[1] should be keypress"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], true, false, "insertText", "a", description); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertText", "a", description); + is(events[4].type, "keyup", + description + "events[4] should be keyup"); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during startComposition(). + var events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + input.removeEventListener(aEvent.type, arguments.callee, false); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.startComposition();"); + }, false); + TIP1.beginInputTransactionForTests(window); + TIP1.startComposition(); + is(events.length, 1, + description + "compositionstart event should be fired by TIP1.startComposition()"); + TIP1.cancelComposition(); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during flushPendingComposition(). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.flushPendingComposition();"); + }, {once: true}); + TIP1.beginInputTransactionForTests(window); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + is(events.length, 5, + description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description); + TIP1.cancelComposition(); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition(). + events = []; + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.commitComposition();"); + }, {once: true}); + TIP1.commitComposition(); + is(events.length, 4, + description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()"); + is(events[0].type, "text", + description + "events[0] should be text"); + is(events[1].type, "beforeinput", + description + "events[1] should be beforeinput"); + checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description); + is(events[2].type, "compositionend", + description + "events[2] should be compositionend"); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertCompositionText", composingStr, description); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar"). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests during text event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests during compositionend event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests during beforeinput event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests during input event handler TIP1.commitCompositionWith(\"bar\");"); + }, {once: true}); + TIP1.beginInputTransactionForTests(window); + TIP1.commitCompositionWith("bar"); + is(events.length, 6, + description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description); + is(events[4].type, "compositionend", + description + "events[4] should be compositionend"); + is(events[5].type, "input", + description + "events[5] should be input"); + checkInputEvent(events[5], false, false, "insertCompositionText", "bar", description); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition(). + events = []; + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.cancelComposition();"); + }, {once: true}); + TIP1.cancelComposition(); + is(events.length, 5, + description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()"); + is(events[0].type, "compositionupdate", + description + "events[0] should be compositionupdate"); + is(events[1].type, "text", + description + "events[1] should be text"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], false, true, "insertCompositionText", "", description); + is(events[3].type, "compositionend", + description + "events[3] should be compositionend"); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, false, "insertCompositionText", "", description); + + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup(). + events = []; + TIP1.beginInputTransactionForTests(window); + input.addEventListener("keydown", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests for tests from keydown event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("keypress", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from keypress event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.keydown();"); + }, {once: true}); + input.addEventListener("keyup", function (aEvent) { + events.push(aEvent); + ok(!TIP2.beginInputTransactionForTests(window), + description + "TIP2 shouldn't be able to begin input transaction for tests from keyup event handler during a call of TIP1.keyup();"); + }, {once: true}); + var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + TIP1.keydown(keyA); + TIP1.keyup(keyA); + is(events.length, 5, + description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()"); + is(events[0].type, "keydown", + description + "events[0] should be keydown"); + is(events[1].type, "keypress", + description + "events[1] should be keypress"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], true, false, "insertText", "a", description); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertText", "a", description); + is(events[4].type, "keyup", + description + "events[4] should be keyup"); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition(). + var events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()"); + } + }, {once: true}); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.startComposition(); + is(events.length, 1, + description + "compositionstart event should be fired by TIP1.startComposition()"); + TIP1.cancelComposition(); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition(). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + is(events.length, 5, + description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description); + TIP1.cancelComposition(); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition(). + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + TIP1.commitComposition(); + is(events.length, 4, + description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()"); + is(events[0].type, "text", + description + "events[0] should be text"); + is(events[1].type, "beforeinput", + description + "events[1] should be beforeinput"); + checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description); + is(events[2].type, "compositionend", + description + "events[2] should be compositionend"); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertCompositionText", composingStr, description); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");. + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.commitCompositionWith("bar"); + is(events.length, 6, + description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description); + is(events[4].type, "compositionend", + description + "events[4] should be compositionend"); + is(events[5].type, "input", + description + "events[5] should be input"); + checkInputEvent(events[5], false, false, "insertCompositionText", "bar", description); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();. + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + TIP1.cancelComposition(); + is(events.length, 5, + description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()"); + is(events[0].type, "compositionupdate", + description + "events[0] should be compositionupdate"); + is(events[1].type, "text", + description + "events[1] should be text"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], false, true, "insertCompositionText", "", description); + is(events[3].type, "compositionend", + description + "events[3] should be compositionend"); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, false, "insertCompositionText", "", description); + + // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();. + events = []; + TIP1.beginInputTransaction(window, simpleCallback); + input.addEventListener("keydown", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("keypress", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("keyup", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransaction(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()"); + } + }, {once: true}); + var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + TIP1.keydown(keyA); + TIP1.keyup(keyA); + is(events.length, 5, + description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()"); + is(events[0].type, "keydown", + description + "events[0] should be keydown"); + is(events[1].type, "keypress", + description + "events[1] should be keypress"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], true, false, "insertText", "a", description); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertText", "a", description); + is(events[4].type, "keyup", + description + "events[4] should be keyup"); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition(). + var events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()"); + } + }, {once: true}); + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.startComposition(); + is(events.length, 1, + description + "compositionstart event should be fired by TIP1.startComposition()"); + TIP1.cancelComposition(); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition(). + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()"); + } + }, {once: true}); + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + is(events.length, 5, + description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description); + TIP1.cancelComposition(); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition(). + events = []; + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()"); + } + }, {once: true}); + TIP1.commitComposition(); + is(events.length, 4, + description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()"); + is(events[0].type, "text", + description + "events[0] should be text"); + is(events[1].type, "beforeinput", + description + "events[1] should be beforeinput"); + checkInputEvent(events[1], false, true, "insertCompositionText", composingStr, description); + is(events[2].type, "compositionend", + description + "events[2] should be compositionend"); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertCompositionText", composingStr, description); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");. + events = []; + input.addEventListener("compositionstart", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")"); + } + }, {once: true}); + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.commitCompositionWith("bar"); + is(events.length, 6, + description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")"); + is(events[0].type, "compositionstart", + description + "events[0] should be compositionstart"); + is(events[1].type, "compositionupdate", + description + "events[1] should be compositionupdate"); + is(events[2].type, "text", + description + "events[2] should be text"); + is(events[3].type, "beforeinput", + description + "events[3] should be beforeinput"); + checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description); + is(events[4].type, "compositionend", + description + "events[4] should be compositionend"); + is(events[5].type, "input", + description + "events[5] should be input"); + checkInputEvent(events[5], false, false, "insertCompositionText", "bar", description); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();. + events = []; + TIP1.beginInputTransactionForTests(window, simpleCallback); + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition(); + input.addEventListener("compositionupdate", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("text", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("compositionend", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()"); + } + }, {once: true}); + TIP1.cancelComposition(); + is(events.length, 5, + description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()"); + is(events[0].type, "compositionupdate", + description + "events[0] should be compositionupdate"); + is(events[1].type, "text", + description + "events[1] should be text"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], false, true, "insertCompositionText", "", description); + is(events[3].type, "compositionend", + description + "events[3] should be compositionend"); + is(events[4].type, "input", + description + "events[4] should be input"); + checkInputEvent(events[4], false, false, "insertCompositionText", "", description); + + // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();. + events = []; + TIP1.beginInputTransactionForTests(window, simpleCallback); + input.addEventListener("keydown", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("keypress", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("beforeinput", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("input", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()"); + } + }, {once: true}); + input.addEventListener("keyup", function (aEvent) { + events.push(aEvent); + try { + TIP1.beginInputTransactionForTests(otherWindow, simpleCallback); + ok(false, + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"), + description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()"); + } + }, {once: true}); + var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + TIP1.keydown(keyA); + TIP1.keyup(keyA); + is(events.length, 5, + description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()"); + is(events[0].type, "keydown", + description + "events[0] should be keydown"); + is(events[1].type, "keypress", + description + "events[1] should be keypress"); + is(events[2].type, "beforeinput", + description + "events[2] should be beforeinput"); + checkInputEvent(events[2], true, false, "insertText", "a", description); + is(events[3].type, "input", + description + "events[3] should be input"); + checkInputEvent(events[3], false, false, "insertText", "a", description); + is(events[4].type, "keyup", + description + "events[4] should be keyup"); + + // Let's check if startComposition() throws an exception after ownership is stolen. + input.value = ""; + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); + try { + TIP1.startComposition(); + ok(false, + description + "TIP1.startComposition() should cause throwing an exception because TIP2 took the ownership"); + TIP1.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"), + description + "TIP1.startComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); + } finally { + is(input.value, "", + description + "The input element should not have commit string"); + } + + // Let's check if flushPendingComposition() throws an exception after ownership is stolen. + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); + input.value = ""; + try { + TIP1.setPendingCompositionString(composingStr); + TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE); + TIP1.flushPendingComposition() + ok(false, + description + "TIP1.flushPendingComposition() should cause throwing an exception because TIP2 took the ownership"); + TIP1.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"), + description + "TIP1.flushPendingComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); + } finally { + is(input.value, "", + description + "The input element should not have commit string"); + } + + // Let's check if commitCompositionWith("bar") throws an exception after ownership is stolen. + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); + input.value = ""; + try { + TIP1.commitCompositionWith("bar"); + ok(false, + description + "TIP1.commitCompositionWith(\"bar\") should cause throwing an exception because TIP2 took the ownership"); + } catch (e) { + ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"), + description + "TIP1.commitCompositionWith(\"bar\") should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); + } finally { + is(input.value, "", + description + "The input element should not have commit string"); + } + + // Let's check if keydown() throws an exception after ownership is stolen. + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); + input.value = ""; + try { + var keyF = new KeyboardEvent("", { key: "f", code: "KeyF", keyCode: KeyboardEvent.DOM_VK_F }); + TIP1.keydown(keyF); + ok(false, + description + "TIP1.keydown(keyF) should cause throwing an exception because TIP2 took the ownership"); + } catch (e) { + ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"), + description + "TIP1.keydown(keyF) should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // Let's check if keyup() throws an exception after ownership is stolen. + ok(TIP1.beginInputTransactionForTests(window), + description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); + ok(TIP2.beginInputTransactionForTests(window), + description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); + input.value = ""; + try { + var keyF = new KeyboardEvent("", { key: "f", code: "KeyF", keyCode: KeyboardEvent.DOM_VK_F }); + TIP1.keyup(keyF); + ok(false, + description + "TIP1.keyup(keyF) should cause throwing an exception because TIP2 took the ownership"); + } catch (e) { + ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"), + description + "TIP1.keyup(keyF) should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // aCallback of nsITextInputProcessor.beginInputTransaction() must not be omitted. + try { + TIP1.beginInputTransaction(window); + ok(false, + description + "TIP1.beginInputTransaction(window) should be failed since aCallback is omitted"); + } catch (e) { + ok(e.message.includes("Not enough arguments"), + description + "TIP1.beginInputTransaction(window) should cause throwing an exception including \"Not enough arguments\" since aCallback is omitted"); + } + + // aCallback of nsITextInputProcessor.beginInputTransaction() must not be undefined. + try { + TIP1.beginInputTransaction(window, undefined); + ok(false, + description + "TIP1.beginInputTransaction(window, undefined) should be failed since aCallback is undefined"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "TIP1.beginInputTransaction(window, undefined) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is undefined"); + } + + // aCallback of nsITextInputProcessor.beginInputTransaction() must not be null. + try { + TIP1.beginInputTransaction(window, null); + ok(false, + description + "TIP1.beginInputTransaction(window, null) should be failed since aCallback is null"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "TIP1.beginInputTransaction(window, null) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is null"); + } +} + +function runReleaseTests() +{ + var description = "runReleaseTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + input.value = ""; + input.focus(); + + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + is(input.value, "foo", + description + "the input should have composition string"); + + // Release the TIP + TIP = null; + // Needs to run GC forcibly for testing this. + Cu.forceGC(); + + is(input.value, "", + description + "the input should be empty because the composition should be canceled"); + + TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed #2"); +} + +function runCompositionTests() +{ + var description = "runCompositionTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + + function reset() + { + events = []; + } + + function handler(aEvent) + { + events.push({ "type": aEvent.type, "data": aEvent.data }); + } + + window.addEventListener("compositionstart", handler, false); + window.addEventListener("compositionupdate", handler, false); + window.addEventListener("compositionend", handler, false); + + input.value = ""; + input.focus(); + + // nsITextInputProcessor.startComposition() + reset(); + TIP.startComposition(); + is(events.length, 1, + description + "startComposition() should cause only compositionstart"); + is(events[0].type, "compositionstart", + description + "startComposition() should cause only compositionstart"); + is(input.value, "", + description + "startComposition() shouldn't modify the focused editor"); + + // Setting composition string "foo" as a raw clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 1, + description + "flushPendingComposition() after startComposition() should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition() after startComposition() should cause compositionupdate"); + is(events[0].data, "foo", + description + "compositionupdate caused by flushPendingComposition() should have new composition string in its data"); + is(input.value, "foo", + description + "modifying composition string should cause modifying the focused editor"); + + // Changing the raw clause to a selected clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 0, + description + "flushPendingComposition() changing only clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Separating the selected clause to two clauses + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 0, + description + "flushPendingComposition() separating a clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "separating composition clause shouldn't cause modifying the focused editor"); + + // Modifying the composition string + TIP.setPendingCompositionString("FOo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 1, + description + "flushPendingComposition() causing modifying composition string should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition() causing modifying composition string should cause compositionupdate"); + is(events[0].data, "FOo", + description + "compositionupdate caused by flushPendingComposition() should have new composition string in its data"); + is(input.value, "FOo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Committing the composition string + reset(); + TIP.commitComposition(); + is(events.length, 1, + description + "commitComposition() should cause compositionend but shouldn't cause compositionupdate"); + is(events[0].type, "compositionend", + description + "commitComposition() should cause compositionend"); + is(events[0].data, "FOo", + description + "compositionend caused by commitComposition() should have the committed string in its data"); + is(input.value, "FOo", + description + "commitComposition() shouldn't cause modifying the focused editor"); + + // Starting new composition without a call of startComposition() + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 2, + description + "flushPendingComposition() without a call of startComposition() should cause both compositionstart and compositionupdate"); + is(events[0].type, "compositionstart", + description + "flushPendingComposition() without a call of startComposition() should cause compositionstart"); + is(events[1].type, "compositionupdate", + description + "flushPendingComposition() without a call of startComposition() should cause compositionupdate after compositionstart"); + is(events[1].data, "bar", + description + "compositionupdate caused by flushPendingComposition() without a call of startComposition() should have the composition string in its data"); + is(input.value, "FOobar", + description + "new composition string should cause appending composition string to the focused editor"); + + // Canceling the composition + reset(); + TIP.cancelComposition(); + is(events.length, 2, + description + "cancelComposition() should cause both compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "cancelComposition() should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by cancelComposition() should have empty string in its data"); + is(events[1].type, "compositionend", + description + "cancelComposition() should cause compositionend after compositionupdate"); + is(events[1].data, "", + description + "compositionend caused by cancelComposition() should have empty string in its data"); + is(input.value, "FOo", + description + "canceled composition string should be removed from the focused editor"); + + // Starting composition explicitly and canceling it + reset(); + TIP.startComposition(); + TIP.cancelComposition(); + is(events.length, 2, + description + "canceling composition immediately after startComposition() should cause compositionstart and compositionend"); + is(events[0].type, "compositionstart", + description + "canceling composition immediately after startComposition() should cause compositionstart first"); + is(events[1].type, "compositionend", + description + "canceling composition immediately after startComposition() should cause compositionend after compositionstart"); + is(events[1].data, "", + description + "compositionend caused by canceling composition should have empty string in its data"); + is(input.value, "FOo", + description + "canceling composition shouldn't modify the focused editor"); + + // Create composition for next test. + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobar", + description + "The focused editor should have new composition string \"bar\""); + + // Allow to set empty composition string + reset(); + TIP.flushPendingComposition(); + is(events.length, 1, + description + "making composition string empty should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "making composition string empty should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by making composition string empty should have empty string in its data"); + + // Allow to insert new composition string without compositionend/compositionstart + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(); + is(events.length, 1, + description + "modifying composition string from empty string should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "modifying composition string from empty string should cause compositionupdate"); + is(events[0].data, "buzz", + description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data"); + is(input.value, "FOobuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with different string + reset(); + TIP.commitCompositionWith("bar"); + is(events.length, 2, + description + "committing with different string should cause compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "committing with different string should cause compositionupdate first"); + is(events[0].data, "bar", + description + "compositionupdate caused by committing with different string should have the committing string in its data"); + is(events[1].type, "compositionend", + description + "committing with different string should cause compositionend after compositionupdate"); + is(events[1].data, "bar", + description + "compositionend caused by committing with different string should have the committing string in its data"); + is(input.value, "FOobar", + description + "new committed string should be appended to the focused editor"); + + // Appending new composition string + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobarbuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with same string + reset(); + TIP.commitCompositionWith("buzz"); + is(events.length, 1, + description + "committing with same string should cause only compositionend"); + is(events[0].type, "compositionend", + description + "committing with same string should cause compositionend"); + is(events[0].data, "buzz", + description + "compositionend caused by committing with same string should have the committing string in its data"); + is(input.value, "FOobarbuzz", + description + "new committed string should be appended to the focused editor"); + + // Inserting commit string directly + reset(); + TIP.commitCompositionWith("boo!"); + is(events.length, 3, + description + "committing text directly should cause compositionstart, compositionupdate and compositionend"); + is(events[0].type, "compositionstart", + description + "committing text directly should cause compositionstart first"); + is(events[1].type, "compositionupdate", + description + "committing text directly should cause compositionupdate after compositionstart"); + is(events[1].data, "boo!", + description + "compositionupdate caused by committing text directly should have the committing text in its data"); + is(events[2].type, "compositionend", + description + "committing text directly should cause compositionend after compositionupdate"); + is(events[2].data, "boo!", + description + "compositionend caused by committing text directly should have the committing text in its data"); + is(input.value, "FOobarbuzzboo!", + description + "committing text directly should append the committing text to the focused editor"); + + window.removeEventListener("compositionstart", handler, false); + window.removeEventListener("compositionupdate", handler, false); + window.removeEventListener("compositionend", handler, false); +} + +function runCompositionWithKeyEventTests() +{ + var description = "runCompositionWithKeyEventTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + + function reset() + { + events = []; + } + + function handler(aEvent) + { + events.push(aEvent); + } + + window.addEventListener("compositionstart", handler, false); + window.addEventListener("compositionupdate", handler, false); + window.addEventListener("compositionend", handler, false); + window.addEventListener("keydown", handler, false); + window.addEventListener("keypress", handler, false); + window.addEventListener("keyup", handler, false); + + input.value = ""; + input.focus(); + + var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + var convertKeyEvent = new KeyboardEvent("", { key: "Convert", code: "Convert", keyCode: KeyboardEvent.DOM_VK_CONVERT }); + var backspaceKeyEvent = new KeyboardEvent("", { key: "Backspace", code: "Backspace", keyCode: KeyboardEvent.DOM_VK_BACK_SPACE }); + + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false); + + // nsITextInputProcessor.startComposition() + reset(); + TIP.startComposition(printableKeyEvent); + is(events.length, 2, + description + "startComposition(printableKeyEvent) should cause keydown and compositionstart"); + is(events[0].type, "keydown", + description + "startComposition(printableKeyEvent) should cause keydown"); + is(events[1].type, "compositionstart", + description + "startComposition(printableKeyEvent) should cause compositionstart"); + is(input.value, "", + description + "startComposition(printableKeyEvent) shouldn't modify the focused editor"); + + // Setting composition string "foo" as a raw clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 1, + description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate"); + is(events[0].data, "foo", + description + "compositionupdate caused by flushPendingComposition(KeyupInternal) should have new composition string in its data"); + is(input.value, "foo", + description + "modifying composition string should cause modifying the focused editor"); + + // Changing the raw clause to a selected clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 0, + description + "flushPendingComposition(convertKeyEvent) changing only clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Separating the selected clause to two clauses + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 0, + description + "flushPendingComposition(convertKeyEvent) separating a clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "separating composition clause shouldn't cause modifying the focused editor"); + + // Modifying the composition string + TIP.setPendingCompositionString("FOo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 1, + description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate"); + is(events[0].data, "FOo", + description + "compositionupdate caused by flushPendingComposition(convertKeyEvent) should have new composition string in its data"); + is(input.value, "FOo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Committing the composition string + reset(); + TIP.commitComposition(enterKeyEvent); + is(events.length, 2, + description + "commitComposition(enterKeyEvent) should cause compositionend and keyup but shoudn't cause compositionupdate"); + is(events[0].type, "compositionend", + description + "commitComposition(enterKeyEvent) should cause compositionend"); + is(events[0].data, "FOo", + description + "compositionend caused by commitComposition(enterKeyEvent) should have the committed string in its data"); + is(events[1].type, "keyup", + description + "commitComposition(enterKeyEvent) should cause keyup"); + is(input.value, "FOo", + description + "commitComposition(enterKeyEvent) shouldn't cause modifying the focused editor"); + + // Starting new composition without a call of startComposition() + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 3, + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause both compositionstart and compositionupdate"); + is(events[0].type, "keydown", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause keydown"); + is(events[1].type, "compositionstart", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionstart"); + is(events[2].type, "compositionupdate", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionupdate after compositionstart"); + is(events[2].data, "bar", + description + "compositionupdate caused by flushPendingComposition(printableKeyEvent) without a call of startComposition() should have the composition string in its data"); + is(input.value, "FOobar", + description + "new composition string should cause appending composition string to the focused editor"); + + // Canceling the composition + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 3, + description + "cancelComposition(escKeyEvent) should cause both compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "cancelComposition(escKeyEvent) should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by cancelComposition(escKeyEvent) should have empty string in its data"); + is(events[1].type, "compositionend", + description + "cancelComposition(escKeyEvent) should cause compositionend after compositionupdate"); + is(events[1].data, "", + description + "compositionend caused by cancelComposition(escKeyEvent) should have empty string in its data"); + is(events[2].type, "keyup", + description + "cancelComposition(escKeyEvent) should cause keyup after compositionend"); + is(input.value, "FOo", + description + "canceled composition string should be removed from the focused editor"); + + // Starting composition explicitly and canceling it + reset(); + TIP.startComposition(printableKeyEvent); + TIP.cancelComposition(escKeyEvent); + is(events.length, 4, + description + "canceling composition immediately after startComposition() should cause keydown, compositionstart, compositionend and keyup"); + is(events[0].type, "keydown", + description + "canceling composition immediately after startComposition() should cause keydown first"); + is(events[1].type, "compositionstart", + description + "canceling composition immediately after startComposition() should cause compositionstart after keydown"); + is(events[2].type, "compositionend", + description + "canceling composition immediately after startComposition() should cause compositionend after compositionstart"); + is(events[2].data, "", + description + "compositionend caused by canceling composition should have empty string in its data"); + is(events[3].type, "keyup", + description + "canceling composition immediately after startComposition() should cause keyup after compositionend"); + is(input.value, "FOo", + description + "canceling composition shouldn't modify the focused editor"); + + // Create composition for next test. + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobar", + description + "The focused editor should have new composition string \"bar\""); + + // Allow to set empty composition string + reset(); + TIP.flushPendingComposition(backspaceKeyEvent); + is(events.length, 1, + description + "making composition string empty should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "making composition string empty should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by making composition string empty should have empty string in its data"); + + // Allow to insert new composition string without compositionend/compositionstart + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 1, + description + "modifying composition string from empty string should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "modifying composition string from empty string should cause compositionupdate"); + is(events[0].data, "buzz", + description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data"); + is(input.value, "FOobuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with different string + reset(); + TIP.commitCompositionWith("bar", printableKeyEvent); + is(events.length, 3, + description + "committing with different string should cause compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "committing with different string should cause compositionupdate first"); + is(events[0].data, "bar", + description + "compositionupdate caused by committing with different string should have the committing string in its data"); + is(events[1].type, "compositionend", + description + "committing with different string should cause compositionend after compositionupdate"); + is(events[1].data, "bar", + description + "compositionend caused by committing with different string should have the committing string in its data"); + is(events[2].type, "keyup", + description + "committing with different string should cause keyup after compositionend"); + is(input.value, "FOobar", + description + "new committed string should be appended to the focused editor"); + + // Appending new composition string + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobarbuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with same string + reset(); + TIP.commitCompositionWith("buzz", enterKeyEvent); + is(events.length, 2, + description + "committing with same string should cause only compositionend"); + is(events[0].type, "compositionend", + description + "committing with same string should cause compositionend"); + is(events[0].data, "buzz", + description + "compositionend caused by committing with same string should have the committing string in its data"); + is(events[1].type, "keyup", + description + "committing with same string should cause keyup after compositionend"); + is(input.value, "FOobarbuzz", + description + "new committed string should be appended to the focused editor"); + + // Inserting commit string directly + reset(); + TIP.commitCompositionWith("boo!", printableKeyEvent); + is(events.length, 5, + description + "committing text directly should cause compositionstart, compositionupdate and compositionend"); + is(events[0].type, "keydown", + description + "committing text directly should cause keydown first"); + is(events[1].type, "compositionstart", + description + "committing text directly should cause compositionstart after keydown"); + is(events[2].type, "compositionupdate", + description + "committing text directly should cause compositionupdate after compositionstart"); + is(events[2].data, "boo!", + description + "compositionupdate caused by committing text directly should have the committing text in its data"); + is(events[3].type, "compositionend", + description + "committing text directly should cause compositionend after compositionupdate"); + is(events[3].data, "boo!", + description + "compositionend caused by committing text directly should have the committing text in its data"); + is(events[4].type, "keyup", + description + "committing text directly should cause keyup after compositionend"); + is(input.value, "FOobarbuzzboo!", + description + "committing text directly should append the committing text to the focused editor"); + + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true); + + // Even if "dom.keyboardevent.dispatch_during_composition" is true, keypress event shouldn't be fired during composition + reset(); + TIP.startComposition(printableKeyEvent); + is(events.length, 3, + description + "TIP.startComposition(printableKeyEvent) should cause keydown, compositionstart and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionstart", + description + "TIP.startComposition(printableKeyEvent) should cause compositionstart (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.startComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.flushPendingComposition(printableKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 3, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown, compositionupdate and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.commitComposition(enterKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.commitComposition(enterKeyEvent); + is(events.length, 3, + description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.commitComposition(enterKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.cancelComposition(escKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + TIP.startComposition(); + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 3, + description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionend", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.cancelComposition(escKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + var printableKeydownEvent = new KeyboardEvent("keydown", { key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B }); + var enterKeydownEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeydownEvent = new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + + // TIP.startComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.startComposition(printableKeydownEvent); + is(events.length, 2, + description + "TIP.startComposition(printableKeydownEvent) should cause keydown and compositionstart (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionstart", + description + "TIP.startComposition(printableKeydownEvent) should cause compositionstart (keyup event shouldn't be fired)"); + + // TIP.flushPendingComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeydownEvent); + is(events.length, 2, + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown and compositionupdate (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause compositionupdate (keyup event shouldn't be fired)"); + + // TIP.commitComposition(enterKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.commitComposition(enterKeydownEvent); + is(events.length, 2, + description + "TIP.commitComposition(enterKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeydownEvent) should cause compositionend (keyup event shouldn't be fired)"); + + // TIP.cancelComposition(escKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + TIP.startComposition(); + reset(); + TIP.cancelComposition(escKeydownEvent); + is(events.length, 2, + description + "TIP.cancelComposition(escKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionend", + description + "TIP.cancelComposition(escKeydownEvent) should cause compositionend (keyup event shouldn't be fired)"); + + Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition"); + + window.removeEventListener("compositionstart", handler, false); + window.removeEventListener("compositionupdate", handler, false); + window.removeEventListener("compositionend", handler, false); + window.removeEventListener("keydown", handler, false); + window.removeEventListener("keypress", handler, false); + window.removeEventListener("keyup", handler, false); +} + +function runConsumingKeydownBeforeCompositionTests() +{ + var description = "runConsumingKeydownBeforeCompositionTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + + function reset() + { + events = []; + } + + function handler(aEvent) + { + events.push(aEvent); + if (aEvent.type == "keydown") { + aEvent.preventDefault(); + } + } + + window.addEventListener("compositionstart", handler, false); + window.addEventListener("compositionupdate", handler, false); + window.addEventListener("compositionend", handler, false); + window.addEventListener("keydown", handler, false); + window.addEventListener("keypress", handler, false); + window.addEventListener("keyup", handler, false); + + input.value = ""; + input.focus(); + + var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false); + + // If keydown before compositionstart is consumed, composition shouldn't be started. + reset(); + ok(!TIP.startComposition(printableKeyEvent), + description + "TIP.startComposition(printableKeyEvent) should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.startComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.startComposition(printableKeyEvent) should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.startComposition(printableKeyEvent) shouldn't cause composition"); + is(input.value, "", + description + "TIP.startComposition(printableKeyEvent) shouldn't cause inserting text"); + + // If keydown before compositionstart caused by flushPendingComposition(printableKeyEvent) is consumed, composition shouldn't be started. + reset(); + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + ok(!TIP.flushPendingComposition(printableKeyEvent), + description + "TIP.flushPendingComposition(printableKeyEvent) should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause composition"); + is(input.value, "", + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause inserting text"); + + // If keydown before compositionstart is consumed, composition shouldn't be started. + reset(); + ok(!TIP.commitCompositionWith("foo", printableKeyEvent), + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) shouldn't cause composition"); + is(input.value, "", + description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) shouldn't cause inserting text"); + + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true); + + // If composition is already started, TIP.flushPendingComposition(printableKeyEvent) shouldn't be canceled. + TIP.startComposition(); + ok(TIP.hasComposition, + description + "Before TIP.flushPendingComposition(printableKeyEvent), composition should've been created"); + reset(); + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + ok(TIP.flushPendingComposition(printableKeyEvent), + description + "TIP.flushPendingComposition(printableKeyEvent) should return true even if preceding keydown is consumed because there was a composition already"); + is(events.length, 3, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate event after keydown"); + is(events[2].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after compositionupdate"); + ok(TIP.hasComposition, + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause canceling composition"); + is(input.value, "foo", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause inserting text even if preceding keydown is consumed because there was a composition already"); + + // If composition is already started, TIP.commitComposition(enterKeyEvent) shouldn't be canceled. + reset(); + TIP.commitComposition(enterKeyEvent); + is(events.length, 3, + description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup events"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeyEvent) should cause compositionend event after keydown"); + is(events[2].type, "keyup", + description + "TIP.commitComposition(enterKeyEvent) should cause keyup event after compositionend"); + ok(!TIP.hasComposition, + description + "TIP.commitComposition(enterKeyEvent) should cause committing composition even if preceding keydown is consumed because there was a composition already"); + is(input.value, "foo", + description + "TIP.commitComposition(enterKeyEvent) should commit composition even if preceding keydown is consumed because there was a composition already"); + + // cancelComposition() should work even if preceding keydown event is consumed. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + ok(TIP.hasComposition, + description + "Before TIP.cancelComposition(escKeyEvent), composition should've been created"); + is(input.value, "foo", + description + "Before TIP.cancelComposition(escKeyEvent) should have composition string"); + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 4, + description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionupdate, compositionend and keyup events even if preceding keydown is consumed because there was a composition already"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionupdate", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionupdate event after keydown"); + is(events[2].type, "compositionend", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionend event after compositionupdate"); + is(events[3].type, "keyup", + description + "TIP.cancelComposition(escKeyEvent) should cause keyup event after compositionend"); + ok(!TIP.hasComposition, + description + "TIP.cancelComposition(escKeyEvent) should cause canceling composition even if preceding keydown is consumed because there was a composition already"); + is(input.value, "", + description + "TIP.cancelComposition(escKeyEvent) should cancel composition even if preceding keydown is consumed because there was a composition already"); + + Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition"); + + window.removeEventListener("compositionstart", handler, false); + window.removeEventListener("compositionupdate", handler, false); + window.removeEventListener("compositionend", handler, false); + window.removeEventListener("keydown", handler, false); + window.removeEventListener("keypress", handler, false); + window.removeEventListener("keyup", handler, false); +} + +function runKeyTests() +{ + var description = "runKeyTests(): "; + const kModifiers = + [ "Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock", "Meta", "NumLock", + "ScrollLock", "Shift", "Symbol", "SymbolLock", "OS" ]; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + var doPreventDefaults; + + function reset() + { + events = []; + doPreventDefaults = []; + } + + function handler(aEvent) + { + events.push(aEvent); + if (doPreventDefaults.includes(aEvent.type)) { + aEvent.preventDefault(); + } + } + + function checkKeyAttrs(aMethodDescription, aEvent, aExpectedData) + { + var desc = description + aMethodDescription + ", type=\"" + aEvent.type + "\", key=\"" + aEvent.key + "\", code=\"" + aEvent.code + "\": "; + var defaultValues = { + key: "Unidentified", code: "", keyCode: 0, charCode: 0, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, repeat: false, isComposing: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, + defaultPrevented: false + }; + function expectedValue(aAttr) + { + return aExpectedData[aAttr] !== undefined ? aExpectedData[aAttr] : defaultValues[aAttr]; + } + is(aEvent.type, aExpectedData.type, + desc + " should cause keydown event"); + if (aEvent.type != aExpectedData.type) { + return; + } + is(aEvent.defaultPrevented, expectedValue("defaultPrevented"), + desc + ".defaultPrevented is wrong"); + is(aEvent.key, expectedValue("key"), + desc + ".key is wrong"); + is(aEvent.code, expectedValue("code"), + desc + ".code is wrong"); + is(aEvent.location, expectedValue("location"), + desc + ".location is wrong"); + is(aEvent.repeat, expectedValue("repeat"), + desc + ".repeat is wrong"); + is(aEvent.isComposing, expectedValue("isComposing"), + desc + ".isComposing is wrong"); + is(aEvent.keyCode, expectedValue("keyCode"), + desc + ".keyCode is wrong"); + is(aEvent.charCode, expectedValue("charCode"), + desc + ".charCode is wrong"); + is(aEvent.shiftKey, expectedValue("shiftKey"), + desc + ".shiftKey is wrong"); + is(aEvent.ctrlKey, expectedValue("ctrlKey"), + desc + ".ctrlKey is wrong"); + is(aEvent.altKey, expectedValue("altKey"), + desc + ".altKey is wrong"); + is(aEvent.metaKey, expectedValue("metaKey"), + desc + ".metaKey is wrong"); + for (var i = 0; i < kModifiers.length; i++) { + is(aEvent.getModifierState(kModifiers[i]), aExpectedData[kModifiers[i]] !== undefined ? aExpectedData[kModifiers[i]] : false, + desc + ".getModifierState(\"" + kModifiers[i] + "\") is wrong"); + } + } + + window.addEventListener("keydown", handler, false); + window.addEventListener("keypress", handler, false); + window.addEventListener("keyup", handler, false); + + input.value = ""; + input.focus(); + + + // Printable key test: + // Emulates pressing 'a' key. + var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + + reset(); + var doDefaultKeydown = TIP.keydown(keyA); + + is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED, + description + "TIP.keydown(keyA) should return 0x02 because the keypress event should be consumed by the input element"); + is(events.length, 2, + description + "TIP.keydown(keyA) should cause keydown and keypress event"); + checkKeyAttrs("TIP.keydown(keyA)", events[0], + { type: "keydown", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0 }); + checkKeyAttrs("TIP.keydown(keyA)", events[1], + { type: "keypress", key: "a", code: "KeyA", keyCode: 0, charCode: "a".charCodeAt(0), defaultPrevented: true }); + is(input.value, "a", + description + "input.value should be \"a\" which is inputted by TIP.keydown(keyA)"); + + // Emulates releasing 'a' key. + reset(); + var doDefaultKeyup = TIP.keyup(keyA); + ok(doDefaultKeyup, + description + "TIP.keyup(keyA) should return true"); + is(events.length, 1, + description + "TIP.keyup(keyA) should cause keyup event"); + checkKeyAttrs("TIP.keyup(keyA)", events[0], + { type: "keyup", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0 }); + is(input.value, "a", + description + "input.value should stay \"a\" which was inputted by TIP.keydown(keyA)"); + + + // Non-printable key test: + // Emulates pressing Enter key. + var keyEnter = new KeyboardEvent("", { key: "Enter", code: "Enter" }); + + reset(); + doDefaultKeydown = TIP.keydown(keyEnter); + + is(doDefaultKeydown, 0, + description + "TIP.keydown(keyEnter) should return 0"); + is(events.length, 2, + description + "TIP.keydown(keyEnter) should cause keydown and keypress event"); + checkKeyAttrs("TIP.keydown(keyEnter)", events[0], + { type: "keydown", key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + checkKeyAttrs("TIP.keydown(keyEnter)", events[1], + { type: "keypress", key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + is(input.value, "a", + description + "input.value should stay \"a\" which was inputted by TIP.keydown(keyA)"); + + // Emulates releasing Enter key. + reset(); + doDefaultKeyup = TIP.keyup(keyEnter); + ok(doDefaultKeyup, + description + "TIP.keyup(keyEnter) should return true"); + is(events.length, 1, + description + "TIP.keyup(keyEnter) should cause keyup event"); + checkKeyAttrs("TIP.keyup(keyEnter)", events[0], + { type: "keyup", key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + is(input.value, "a", + description + "input.value should stay \"a\" which was inputted by TIP.keydown(keyA)"); + + + // KEY_DEFAULT_PREVENTED should cause defaultPrevented = true and not cause keypress event + var keyB = new KeyboardEvent("", { key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B }); + + reset(); + doDefaultKeydown = TIP.keydown(keyB, TIP.KEY_DEFAULT_PREVENTED); + doDefaultKeyup = TIP.keyup(keyB, TIP.KEY_DEFAULT_PREVENTED); + + is(doDefaultKeydown, TIP.KEYDOWN_IS_CONSUMED, + description + "TIP.keydown(keyB, TIP.KEY_DEFAULT_PREVENTED) should return 0x01 because it's marked as consumed at dispatching the event"); + ok(!doDefaultKeyup, + description + "TIP.keyup(keyB, TIP.KEY_DEFAULT_PREVENTED) should return false because it's marked as consumed at dispatching the event"); + is(events.length, 2, + description + "TIP.keydown(keyB, TIP.KEY_DEFAULT_PREVENTED) and TIP.keyup(keyB, TIP.KEY_DEFAULT_PREVENTED) should cause keydown and keyup event"); + checkKeyAttrs("TIP.keydown(keyB, TIP.KEY_DEFAULT_PREVENTED) and TIP.keyup(keyB, TIP.KEY_DEFAULT_PREVENTED)", events[0], + { type: "keydown", key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B, defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyB, TIP.KEY_DEFAULT_PREVENTED) and TIP.keyup(keyB, TIP.KEY_DEFAULT_PREVENTED)", events[1], + { type: "keyup", key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B, defaultPrevented: true }); + is(input.value, "a", + description + "input.value shouldn't be modified by default prevented key events"); + + // Assume that KeyX causes inputting text "abc" + input.value = ""; + var keyABC = new KeyboardEvent("", { key: "abc", code: "KeyX", keyCode: KeyboardEvent.DOM_VK_A }); + + reset(); + doDefaultKeydown = TIP.keydown(keyABC); + doDefaultKeyup = TIP.keyup(keyABC); + + is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED, + description + "TIP.keydown(keyABC) should return false because the keypress events should be consumed by the input element"); + ok(doDefaultKeyup, + description + "TIP.keyup(keyABC) should return true"); + is(events.length, 5, + description + "TIP.keydown(keyABC) and TIP.keyup(keyABC) should cause keydown, keypress, keypress, keypress and keyup event"); + checkKeyAttrs("TIP.keydown(keyABC) and TIP.keyup(keyABC)", events[0], + { type: "keydown", key: "abc", code: "KeyX", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: false }); + checkKeyAttrs("TIP.keydown(keyABC) and TIP.keyup(keyABC)", events[1], + { type: "keypress", key: "abc".charAt(0), code: "KeyX", keyCode: 0, charCode: "abc".charCodeAt(0), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyABC) and TIP.keyup(keyABC)", events[2], + { type: "keypress", key: "abc".charAt(1), code: "KeyX", keyCode: 0, charCode: "abc".charCodeAt(1), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyABC) and TIP.keyup(keyABC)", events[3], + { type: "keypress", key: "abc".charAt(2), code: "KeyX", keyCode: 0, charCode: "abc".charCodeAt(2), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyABC) and TIP.keyup(keyABC)", events[4], + { type: "keyup", key: "abc", code: "KeyX", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: false }); + is(input.value, "abc", + description + "input.value should be \"abc\""); + + // If KEY_FORCE_PRINTABLE_KEY is specified, registered key names can be a printable key which inputs the specified value. + input.value = ""; + var keyEnterPrintable = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + + reset(); + doDefaultKeydown = TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY); + doDefaultKeyup = TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY); + + is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED, + description + "TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should return 0x02 because the keypress events should be consumed by the input element"); + ok(doDefaultKeyup, + description + "TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should return true"); + is(events.length, 7, + description + "TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should cause keydown, keypress, keypress, keypress, keypress, keypress and keyup event"); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[0], + { type: "keydown", key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0, defaultPrevented: false }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[1], + { type: "keypress", key: "Enter".charAt(0), code: "Enter", keyCode: 0, charCode: "Enter".charCodeAt(0), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[2], + { type: "keypress", key: "Enter".charAt(1), code: "Enter", keyCode: 0, charCode: "Enter".charCodeAt(1), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[3], + { type: "keypress", key: "Enter".charAt(2), code: "Enter", keyCode: 0, charCode: "Enter".charCodeAt(2), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[4], + { type: "keypress", key: "Enter".charAt(3), code: "Enter", keyCode: 0, charCode: "Enter".charCodeAt(3), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[5], + { type: "keypress", key: "Enter".charAt(4), code: "Enter", keyCode: 0, charCode: "Enter".charCodeAt(4), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[6], + { type: "keyup", key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0, defaultPrevented: false }); + is(input.value, "Enter", + description + "input.value should be \"Enter\""); + + // modifiers should be ignored. + var keyWithModifiers = new KeyboardEvent("", { key: "Escape", code: "Escape", shiftKey: true, ctrlKey: true, altKey: true, metaKey: true }); + + reset(); + doDefaultKeydown = TIP.keydown(keyWithModifiers); + doDefaultKeyup = TIP.keyup(keyWithModifiers); + + is(doDefaultKeydown, 0, + description + "TIP.keydown(keyWithModifiers) should return 0"); + ok(doDefaultKeyup, + description + "TIP.keyup(keyWithModifiers) should return true"); + is(events.length, 3, + description + "TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers) should cause keydown, keypress and keyup event"); + checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[0], + { type: "keydown", key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[1], + { type: "keypress", key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[2], + { type: "keyup", key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + is(input.value, "Enter", + description + "input.value should stay \"Enter\" which was inputted by TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)"); + + // Call preventDefault() at keydown + input.value = ""; + reset(); + doPreventDefaults = [ "keydown" ]; + doDefaultKeydown = TIP.keydown(keyA); + doDefaultKeyup = TIP.keyup(keyA); + + is(doDefaultKeydown, TIP.KEYDOWN_IS_CONSUMED, + description + "TIP.keydown(keyA) should return 0x01 because keydown event's preventDefault should be called"); + ok(doDefaultKeyup, + description + "TIP.keyup(keyA) should return true"); + is(events.length, 2, + description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown should cause keydown and keyup event"); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown", events[0], + { type: "keydown", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown", events[1], + { type: "keyup", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, defaultPrevented: false }); + is(input.value, "", + description + "input.value shouldn't be modified by TIP.keyup(keyA) if the keydown event is consumed"); + + // Call preventDefault() at keypress + reset(); + doPreventDefaults = [ "keypress" ]; + doDefaultKeydown = TIP.keydown(keyA); + doDefaultKeyup = TIP.keyup(keyA); + + is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED, + description + "TIP.keydown(keyA) should return 0x02 because keypress event's preventDefault should be called"); + ok(doDefaultKeyup, + description + "TIP.keyup(keyA) should return true"); + is(events.length, 3, + description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress should cause keydown, keypress and keyup event"); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[0], + { type: "keydown", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: false }); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[1], + { type: "keypress", key: "a", code: "KeyA", keyCode: 0, charCode: "a".charCodeAt(0), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[2], + { type: "keyup", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: false }); + is(input.value, "", + description + "input.value shouldn't be modified by TIP.keyup(keyA) if the keypress event is consumed"); + + // Call preventDefault() at keyup + input.value = ""; + reset(); + doPreventDefaults = [ "keyup" ]; + doDefaultKeydown = TIP.keydown(keyA); + doDefaultKeyup = TIP.keyup(keyA); + + is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED, + description + "TIP.keydown(keyA) should return 0x02 because the key event should be consumed by the input element"); + ok(!doDefaultKeyup, + description + "TIP.keyup(keyA) should return false because keyup event's preventDefault should be called"); + is(events.length, 3, + description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup should cause keydown, keypress and keyup event"); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[0], + { type: "keydown", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: false }); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[1], + { type: "keypress", key: "a", code: "KeyA", keyCode: 0, charCode: "a".charCodeAt(0), defaultPrevented: true }); + checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[2], + { type: "keyup", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, defaultPrevented: true }); + is(input.value, "a", + description + "input.value should be \"a\" by TIP.keyup(keyA) even if the keyup event is consumed"); + + // key events during composition + try { + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false); + + ok(TIP.startComposition(), "TIP.startComposition() should start composition"); + + input.value = ""; + reset(); + TIP.keydown(keyA); + is(events.length, 0, + description + "TIP.keydown(keyA) shouldn't cause key events during composition if it's disabled by the pref"); + reset(); + TIP.keyup(keyA); + is(events.length, 0, + description + "TIP.keyup(keyA) shouldn't cause key events during composition if it's disabled by the pref"); + + Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true); + reset(); + TIP.keydown(keyA); + is(events.length, 1, + description + "TIP.keydown(keyA) should cause keydown event even composition if it's enabled by the pref"); + checkKeyAttrs("TIP.keydown(keyA) during composition", events[0], + { type: "keydown", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, isComposing: true }); + reset(); + TIP.keyup(keyA); + is(events.length, 1, + description + "TIP.keyup(keyA) should cause keyup event even composition if it's enabled by the pref"); + checkKeyAttrs("TIP.keyup(keyA) during composition", events[0], + { type: "keyup", key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, isComposing: true }); + + } finally { + TIP.cancelComposition(); + Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition"); + } + + // Test .location computation + const kCodeToLocation = [ + { code: "BracketLeft", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "BracketRight", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Comma", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit0", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit1", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit2", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit3", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit4", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit5", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit6", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit7", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit8", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Digit9", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Equal", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Minus", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Period", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Slash", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "AltLeft", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + { code: "AltRight", location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + { code: "CapsLock", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ContextMenu", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ControlLeft", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + { code: "ControlRight", location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + { code: "Enter", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "OSLeft", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + { code: "OSRight", location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + { code: "ShiftLeft", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + { code: "ShiftRight", location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + { code: "Space", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Tab", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ArrowDown", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ArrowLeft", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ArrowRight", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "ArrowUp", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "NumLock", location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + { code: "Numpad0", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad1", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad2", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad3", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad4", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad5", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad6", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad7", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad8", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "Numpad9", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadAdd", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadBackspace", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadClear", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadClearEntry", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadComma", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadDecimal", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadDivide", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadEnter", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadEqual", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMemoryAdd", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMemoryClear", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMemoryRecall", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMemoryStore", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMemorySubtract", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadMultiply", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadParenLeft", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadParenRight", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + { code: "NumpadSubtract", location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + ]; + for (var i = 0; i < kCodeToLocation.length; i++) { + var keyEvent = new KeyboardEvent("", { code: kCodeToLocation[i].code }); + reset(); + doPreventDefaults = [ "keypress" ]; + // If the location isn't initialized or initialized with 0, it should be computed from the code value. + TIP.keydown(keyEvent); + TIP.keyup(keyEvent); + var longDesc = description + "testing computation of .location of \"" + kCodeToLocation[i].code + "\", "; + is(events.length, 3, + longDesc + "keydown, keypress and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].location, kCodeToLocation[i].location, + longDesc + " type=\"" + events[j].type + "\", location value is wrong"); + } + // However, if KEY_KEEP_KEY_LOCATION_STANDARD is specified, .location value should be kept as DOM_KEY_LOCATION_STANDARD (0). + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(keyEvent, TIP.KEY_KEEP_KEY_LOCATION_STANDARD); + TIP.keyup(keyEvent, TIP.KEY_KEEP_KEY_LOCATION_STANDARD); + var longDesc = description + "testing if .location is forcibly set to DOM_KEY_LOCATION_STANDARD, "; + is(events.length, 3, + longDesc + "keydown, keypress and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].location, KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + longDesc + " type=\"" + events[j].type + "\", location value is not 0"); + } + // If .location is initialized with non-zero value, the value shouldn't be computed again. + var keyEventWithLocation = new KeyboardEvent("", { code: kCodeToLocation[i].code, location: 0xFF }); + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(keyEventWithLocation); + TIP.keyup(keyEventWithLocation); + longDesc = description + "testing if .location is not computed for \"" + kCodeToLocation[i].location + "\", "; + is(events.length, 3, + longDesc + "keydown, keypress and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].location, 0xFF, + longDesc + " type=\"" + events[j].type + "\", location shouldn't be computed if it's initialized with non-zero value"); + } + } + + // Test .keyCode value computation + const kKeyToKeyCode = [ + { key: "Cancel", keyCode: KeyboardEvent.DOM_VK_CANCEL }, + { key: "Help", keyCode: KeyboardEvent.DOM_VK_HELP }, + { key: "Backspace", keyCode: KeyboardEvent.DOM_VK_BACK_SPACE }, + { key: "Tab", keyCode: KeyboardEvent.DOM_VK_TAB }, + { key: "Clear", keyCode: KeyboardEvent.DOM_VK_CLEAR }, + { key: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }, + { key: "Shift", keyCode: KeyboardEvent.DOM_VK_SHIFT, isModifier: true }, + { key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL, isModifier: true }, + { key: "Alt", keyCode: KeyboardEvent.DOM_VK_ALT, isModifier: true }, + { key: "Pause", keyCode: KeyboardEvent.DOM_VK_PAUSE }, + { key: "CapsLock", keyCode: KeyboardEvent.DOM_VK_CAPS_LOCK, isModifier: true, isLockableModifier: true }, + { key: "Hiragana", keyCode: KeyboardEvent.DOM_VK_KANA }, + { key: "Katakana", keyCode: KeyboardEvent.DOM_VK_KANA }, + { key: "HiraganaKatakana", keyCode: KeyboardEvent.DOM_VK_KANA }, + { key: "KanaMode", keyCode: KeyboardEvent.DOM_VK_KANA }, + { key: "HangulMode", keyCode: KeyboardEvent.DOM_VK_HANGUL }, + { key: "Eisu", keyCode: KeyboardEvent.DOM_VK_EISU }, + { key: "JunjaMode", keyCode: KeyboardEvent.DOM_VK_JUNJA }, + { key: "FinalMode", keyCode: KeyboardEvent.DOM_VK_FINAL }, + { key: "HanjaMode", keyCode: KeyboardEvent.DOM_VK_HANJA }, + { key: "KanjiMode", keyCode: KeyboardEvent.DOM_VK_KANJI }, + { key: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }, + { key: "Convert", keyCode: KeyboardEvent.DOM_VK_CONVERT }, + { key: "NonConvert", keyCode: KeyboardEvent.DOM_VK_NONCONVERT }, + { key: "Accept", keyCode: KeyboardEvent.DOM_VK_ACCEPT }, + { key: "ModeChange", keyCode: KeyboardEvent.DOM_VK_MODECHANGE }, + { key: "PageUp", keyCode: KeyboardEvent.DOM_VK_PAGE_UP }, + { key: "PageDown", keyCode: KeyboardEvent.DOM_VK_PAGE_DOWN }, + { key: "End", keyCode: KeyboardEvent.DOM_VK_END }, + { key: "Home", keyCode: KeyboardEvent.DOM_VK_HOME }, + { key: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT }, + { key: "ArrowUp", keyCode: KeyboardEvent.DOM_VK_UP }, + { key: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT }, + { key: "ArrowDown", keyCode: KeyboardEvent.DOM_VK_DOWN }, + { key: "Select", keyCode: KeyboardEvent.DOM_VK_SELECT }, + { key: "Print", keyCode: KeyboardEvent.DOM_VK_PRINT }, + { key: "Execute", keyCode: KeyboardEvent.DOM_VK_EXECUTE }, + { key: "PrintScreen", keyCode: KeyboardEvent.DOM_VK_PRINTSCREEN }, + { key: "Insert", keyCode: KeyboardEvent.DOM_VK_INSERT }, + { key: "Delete", keyCode: KeyboardEvent.DOM_VK_DELETE }, + { key: "OS", keyCode: KeyboardEvent.DOM_VK_WIN, isModifier: true }, + { key: "ContextMenu", keyCode: KeyboardEvent.DOM_VK_CONTEXT_MENU }, + { key: "F1", keyCode: KeyboardEvent.DOM_VK_F1 }, + { key: "F2", keyCode: KeyboardEvent.DOM_VK_F2 }, + { key: "F3", keyCode: KeyboardEvent.DOM_VK_F3 }, + { key: "F4", keyCode: KeyboardEvent.DOM_VK_F4 }, + { key: "F5", keyCode: KeyboardEvent.DOM_VK_F5 }, + { key: "F6", keyCode: KeyboardEvent.DOM_VK_F6 }, + { key: "F7", keyCode: KeyboardEvent.DOM_VK_F7 }, + { key: "F8", keyCode: KeyboardEvent.DOM_VK_F8 }, + { key: "F9", keyCode: KeyboardEvent.DOM_VK_F9 }, + { key: "F10", keyCode: KeyboardEvent.DOM_VK_F10 }, + { key: "F11", keyCode: KeyboardEvent.DOM_VK_F11 }, + { key: "F12", keyCode: KeyboardEvent.DOM_VK_F12 }, + { key: "F13", keyCode: KeyboardEvent.DOM_VK_F13 }, + { key: "F14", keyCode: KeyboardEvent.DOM_VK_F14 }, + { key: "F15", keyCode: KeyboardEvent.DOM_VK_F15 }, + { key: "F16", keyCode: KeyboardEvent.DOM_VK_F16 }, + { key: "F17", keyCode: KeyboardEvent.DOM_VK_F17 }, + { key: "F18", keyCode: KeyboardEvent.DOM_VK_F18 }, + { key: "F19", keyCode: KeyboardEvent.DOM_VK_F19 }, + { key: "F20", keyCode: KeyboardEvent.DOM_VK_F20 }, + { key: "F21", keyCode: KeyboardEvent.DOM_VK_F21 }, + { key: "F22", keyCode: KeyboardEvent.DOM_VK_F22 }, + { key: "F23", keyCode: KeyboardEvent.DOM_VK_F23 }, + { key: "F24", keyCode: KeyboardEvent.DOM_VK_F24 }, + { key: "NumLock", keyCode: KeyboardEvent.DOM_VK_NUM_LOCK, isModifier: true, isLockableModifier: true }, + { key: "ScrollLock", keyCode: KeyboardEvent.DOM_VK_SCROLL_LOCK, isModifier: true, isLockableModifier: true }, + { key: "AudioVolumeMute", keyCode: KeyboardEvent.DOM_VK_VOLUME_MUTE }, + { key: "AudioVolumeDown", keyCode: KeyboardEvent.DOM_VK_VOLUME_DOWN }, + { key: "AudioVolumeUp", keyCode: KeyboardEvent.DOM_VK_VOLUME_UP }, + { key: "Meta", keyCode: KeyboardEvent.DOM_VK_META, isModifier: true }, + { key: "AltGraph", keyCode: KeyboardEvent.DOM_VK_ALTGR, isModifier: true }, + { key: "Attn", keyCode: KeyboardEvent.DOM_VK_ATTN }, + { key: "CrSel", keyCode: KeyboardEvent.DOM_VK_CRSEL }, + { key: "ExSel", keyCode: KeyboardEvent.DOM_VK_EXSEL }, + { key: "EraseEof", keyCode: KeyboardEvent.DOM_VK_EREOF }, + { key: "Play", keyCode: KeyboardEvent.DOM_VK_PLAY }, + { key: "ZoomToggle", keyCode: KeyboardEvent.DOM_VK_ZOOM }, + { key: "ZoomIn", keyCode: KeyboardEvent.DOM_VK_ZOOM }, + { key: "ZoomOut", keyCode: KeyboardEvent.DOM_VK_ZOOM }, + { key: "Unidentified", keyCode: 0 }, + { key: "a", keyCode: 0, isPrintable: true }, + { key: "A", keyCode: 0, isPrintable: true }, + { key: " ", keyCode: 0, isPrintable: true }, + { key: "", keyCode: 0, isPrintable: true }, + ]; + + for (var i = 0; i < kKeyToKeyCode.length; i++) { + var keyEvent = new KeyboardEvent("", { key: kKeyToKeyCode[i].key }); + var causeKeypress = !kKeyToKeyCode[i].isModifier; + var baseFlags = kKeyToKeyCode[i].isPrintable ? 0 : TIP.KEY_NON_PRINTABLE_KEY; + reset(); + doPreventDefaults = [ "keypress" ]; + // If the keyCode isn't initialized or initialized with 0, it should be computed from the key value only when it's a printable key. + TIP.keydown(keyEvent, baseFlags); + TIP.keyup(keyEvent, baseFlags); + var longDesc = description + "testing computation of .keyCode of \"" + kKeyToKeyCode[i].key + "\", "; + is(events.length, causeKeypress ? 3 : 2, + longDesc + "keydown" + (causeKeypress ? ", keypress" : "") + " and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].keyCode, events[j].type == "keypress" && kKeyToKeyCode[i].isPrintable ? 0 : kKeyToKeyCode[i].keyCode, + longDesc + " type=\"" + events[j].type + "\", keyCode value is wrong"); + } + // However, if KEY_KEEP_KEYCODE_ZERO is specified, .keyCode value should be kept as 0. + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(keyEvent, TIP.KEY_KEEP_KEYCODE_ZERO | baseFlags); + TIP.keyup(keyEvent, TIP.KEY_KEEP_KEYCODE_ZERO | baseFlags); + var longDesc = description + "testing if .keyCode is forcibly set to KEY_KEEP_KEYCODE_ZERO, "; + is(events.length, causeKeypress ? 3 : 2, + longDesc + "keydown" + (causeKeypress ? ", keypress" : "") + " and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].keyCode, 0, + longDesc + " type=\"" + events[j].type + "\", keyCode value is not 0"); + } + // If .keyCode is initialized with non-zero value, the value shouldn't be computed again. + var keyEventWithLocation = new KeyboardEvent("", { key: kKeyToKeyCode[i].key, keyCode: 0xFF }); + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(keyEventWithLocation, baseFlags); + TIP.keyup(keyEventWithLocation, baseFlags); + longDesc = description + "testing if .keyCode is not computed for \"" + kKeyToKeyCode[i].key + "\", "; + is(events.length, causeKeypress ? 3 : 2, + longDesc + "keydown" + (causeKeypress ? ", keypress" : "") + " and keyup events should be fired"); + for (var j = 0; j < events.length; j++) { + is(events[j].keyCode, events[j].type == "keypress" && kKeyToKeyCode[i].isPrintable ? 0 : 0xFF, + longDesc + " type=\"" + events[j].type + "\", keyCode shouldn't be computed if it's initialized with non-zero value"); + } + // Unlock lockable modifier if the key is a lockable modifier key. + if (kKeyToKeyCode[i].isLockableModifier) { + TIP.keydown(keyEvent, baseFlags); + TIP.keyup(keyEvent, baseFlags); + } + } + + // Modifier state tests + var sharedTIP = createTIP(); + ok(sharedTIP.beginInputTransactionForTests(otherWindow), + description + "sharedTIP.beginInputTransactionForTests(otherWindow) should return true"); + TIP.shareModifierStateOf(sharedTIP); + var independentTIP = createTIP(); + const kModifierKeys = [ + { key: "Alt", code: "AltLeft", isLockable: false }, + { key: "Alt", code: "AltRight", isLockable: false }, + { key: "AltGraph", code: "AltRight", isLockable: false }, + { key: "CapsLock", code: "CapsLock", isLockable: true }, + { key: "Control", code: "ControlLeft", isLockable: false }, + { key: "Control", code: "ControlRight", isLockable: false }, + { key: "Fn", code: "Fn", isLockable: false }, + { key: "FnLock", code: "", isLockable: true }, + { key: "Meta", code: "OSLeft", isLockable: false }, + { key: "Meta", code: "OSRight", isLockable: false }, + { key: "NumLock", code: "NumLock", isLockable: true }, + { key: "ScrollLock", code: "ScrollLock", isLockable: true }, + { key: "Shift", code: "ShiftLeft", isLockable: false }, + { key: "Shift", code: "ShiftRight", isLockable: false }, + { key: "Symbol", code: "", isLockable: false }, + { key: "SymbolLock", code: "", isLockable: true }, + { key: "OS", code: "OSLeft", isLockable: false }, + { key: "OS", code: "OSRight", isLockable: false }, + ]; + + function checkModifiers(aTestDesc, aEvent, aType, aKey, aCode, aModifiers) + { + var desc = description + aTestDesc + ", type=\"" + aEvent.type + "\", key=\"" + aEvent.key + "\", code=\"" + aEvent.code + "\""; + is(aEvent.type, aType, + desc + ", .type value is wrong"); + if (aEvent.type != aType) { + return; + } + is(aEvent.key, aKey, + desc + ", .key value is wrong"); + is(aEvent.code, aCode, + desc + ", .code value is wrong"); + is(aEvent.altKey, aModifiers.includes("Alt"), + desc + ", .altKey value is wrong"); + is(aEvent.ctrlKey, aModifiers.includes("Control"), + desc + ", .ctrlKey value is wrong"); + is(aEvent.metaKey, aModifiers.includes("Meta"), + desc + ", .metaKey value is wrong"); + is(aEvent.shiftKey, aModifiers.includes("Shift"), + desc + ", .shiftKey value is wrong"); + /* eslint-disable-next-line no-shadow */ + for (var i = 0; i < kModifiers.length; i++) { + is(aEvent.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]), + desc + ", .getModifierState(\"" + kModifiers[i] + "\") returns wrong value"); + } + } + + function checkAllTIPModifiers(aTestDesc, aModifiers) + { + /* eslint-disable-next-line no-shadow */ + for (var i = 0; i < kModifiers.length; i++) { + is(TIP.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]), + aTestDesc + ", TIP.getModifierState(\"" + kModifiers[i] + "\") returns wrong value"); + is(sharedTIP.getModifierState(kModifiers[i]), TIP.getModifierState(kModifiers[i]), + aTestDesc + ", sharedTIP.getModifierState(\"" + kModifiers[i] + "\") returns different value from TIP"); + is(independentTIP.getModifierState(kModifiers[i]), false, + aTestDesc + ", independentTIP.getModifierState(\"" + kModifiers[i] + "\") should return false"); + } + } + + // First, all modifiers must be false. + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(keyA); + TIP.keyup(keyA); + + is(events.length, 3, + description + "TIP.keydown(keyA) and TIP.keyup(keyA) should cause keydown, keypress and keyup"); + checkModifiers("Before dispatching modifier key events", events[0], "keydown", "a", "KeyA", []); + checkModifiers("Before dispatching modifier key events", events[1], "keypress", "a", "KeyA", []); + checkModifiers("Before dispatching modifier key events", events[2], "keyup", "a", "KeyA", []); + + // Test each modifier keydown/keyup causes activating/inactivating the modifier state. + for (var i = 0; i < kModifierKeys.length; i++) { + reset(); + doPreventDefaults = [ "keypress" ]; + var modKey = new KeyboardEvent("", { key: kModifierKeys[i].key, code: kModifierKeys[i].code }); + var testDesc = "A modifier key \"" + kModifierKeys[i].key + "\" (\"" + kModifierKeys[i].code + "\") and a printable key"; + if (!kModifierKeys[i].isLockable) { + TIP.keydown(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" keydown", [ kModifierKeys[i].key ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ kModifierKeys[i].key ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ kModifierKeys[i].key ]); + TIP.keyup(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" keyup", [ ]); + is(events.length, 5, + description + testDesc + " should cause 5 events"); + checkModifiers(testDesc, events[0], "keydown", kModifierKeys[i].key, kModifierKeys[i].code, [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[1], "keydown", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[2], "keypress", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[3], "keyup", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[4], "keyup", kModifierKeys[i].key, kModifierKeys[i].code, [ ]); + + // KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT shouldn't cause key events of modifier keys, but should modify the modifier state. + reset(); + doPreventDefaults = [ "keypress" ]; + testDesc = "A modifier key \"" + kModifierKeys[i].key + "\" (\"" + kModifierKeys[i].code + "\") with KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT and a printable key"; + TIP.keydown(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keydown(keyA); + TIP.keyup(keyA); + TIP.keyup(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keydown(keyA); + TIP.keyup(keyA); + is(events.length, 6, + description + testDesc + " should cause 6 events"); + checkModifiers(testDesc, events[0], "keydown", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[1], "keypress", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[2], "keyup", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[3], "keydown", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[4], "keypress", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[5], "keyup", "a", "KeyA", [ ]); + } else { + TIP.keydown(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" first keydown", [ kModifierKeys[i].key ]); + TIP.keyup(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" first keyup", [ kModifierKeys[i].key ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ kModifierKeys[i].key ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ kModifierKeys[i].key ]); + TIP.keydown(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" second keydown", [ ]); + TIP.keyup(modKey); + checkAllTIPModifiers(testDesc + ", \"" + kModifierKeys[i].key + "\" second keyup", [ ]); + is(events.length, 7, + description + testDesc + " should cause 7 events"); + checkModifiers(testDesc, events[0], "keydown", kModifierKeys[i].key, kModifierKeys[i].code, [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[1], "keyup", kModifierKeys[i].key, kModifierKeys[i].code, [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[5], "keydown", kModifierKeys[i].key, kModifierKeys[i].code, [ ]); + checkModifiers(testDesc, events[6], "keyup", kModifierKeys[i].key, kModifierKeys[i].code, [ ]); + + // KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT shouldn't cause key events of modifier keys, but should modify the modifier state. + reset(); + doPreventDefaults = [ "keypress" ]; + testDesc = "A modifier key \"" + kModifierKeys[i].key + "\" (\"" + kModifierKeys[i].code + "\") with KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT and a printable key"; + TIP.keydown(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keyup(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keydown(keyA); + TIP.keyup(keyA); + TIP.keydown(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keyup(modKey, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keydown(keyA); + TIP.keyup(keyA); + is(events.length, 6, + description + testDesc + " should cause 6 events"); + checkModifiers(testDesc, events[0], "keydown", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[1], "keypress", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[2], "keyup", "a", "KeyA", [ kModifierKeys[i].key ]); + checkModifiers(testDesc, events[3], "keydown", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[4], "keypress", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[5], "keyup", "a", "KeyA", [ ]); + } + } + + // Modifier state should be inactivated only when all pressed modifiers are released + var shiftLeft = new KeyboardEvent("", { key: "Shift", code: "ShiftLeft" }); + var shiftRight = new KeyboardEvent("", { key: "Shift", code: "ShiftRight" }); + var shiftVirtual = new KeyboardEvent("", { key: "Shift", code: "" }); + var altGrVirtual = new KeyboardEvent("", { key: "AltGraph", code: "" }); + var ctrlVirtual = new KeyboardEvent("", { key: "Control", code: "" }); + + var testDesc = "ShiftLeft press -> ShiftRight press -> ShiftRight release -> ShiftLeft release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Right-Shift keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Right-Shift keyup)", [ "Shift" ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "ShiftLeft", [ ]); + + testDesc = "ShiftLeft press -> ShiftRight press -> ShiftLeft release -> ShiftRight release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup)", [ "Shift" ]); + TIP.keyup(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "ShiftRight", [ ]); + + testDesc = "ShiftLeft press -> virtual Shift press -> virtual Shift release -> ShiftLeft release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "Shift" ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "ShiftLeft", [ ]); + + testDesc = "virtual Shift press -> ShiftRight press -> ShiftRight release -> virtual Shift release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Right-Shift keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Right-Shift keyup)", [ "Shift" ]); + TIP.keyup(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "", [ ]); + + testDesc = "ShiftLeft press -> ShiftRight press -> ShiftRight release -> ShiftRight release -> ShiftLeft release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "Shift" ]); + TIP.keyup(shiftRight); + checkAllTIPModifiers(testDesc + ", Right-Shift keyup again", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup again)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup again)", [ "Shift" ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]); + + is(events.length, 14, + description + testDesc + " should cause 14 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "ShiftRight", [ "Shift" ]); + checkModifiers(testDesc, events[10], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[11], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[12], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[13], "keyup", "Shift", "ShiftLeft", [ ]); + + testDesc = "ShiftLeft press -> ShiftLeft press -> ShiftLeft release -> ShiftLeft release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]); + TIP.keydown(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keydown again", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup)", [ ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup)", [ ]); + TIP.keyup(shiftLeft); + checkAllTIPModifiers(testDesc + ", Left-Shift keyup again", [ ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup again)", [ ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup again)", [ ]); + + is(events.length, 13, + description + testDesc + " should cause 13 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "Shift", "ShiftLeft", [ "Shift" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "ShiftLeft", [ ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "ShiftLeft", [ ]); + checkModifiers(testDesc, events[10], "keydown", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[11], "keypress", "a", "KeyA", [ ]); + checkModifiers(testDesc, events[12], "keyup", "a", "KeyA", [ ]); + + testDesc = "virtual Shift press -> virtual AltGraph press -> virtual AltGraph release -> virtual Shift release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]); + TIP.keydown(altGrVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keydown", [ "Shift", "AltGraph" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift", "AltGraph" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift", "AltGraph" ]); + TIP.keyup(altGrVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keyup", [ "Shift" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-AltGraph keyup)", [ "Shift" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-AltGraph keyup)", [ "Shift" ]); + TIP.keyup(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "AltGraph", "", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[5], "keyup", "AltGraph", "", [ "Shift" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "Shift" ]); + checkModifiers(testDesc, events[9], "keyup", "Shift", "", [ ]); + + testDesc = "virtual Shift press -> virtual AltGraph press -> virtual Shift release -> virtual AltGr release"; + reset(); + doPreventDefaults = [ "keypress" ]; + TIP.keydown(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]); + TIP.keydown(altGrVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keydown", [ "Shift", "AltGraph" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift", "AltGraph" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift", "AltGraph" ]); + TIP.keyup(shiftVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ "AltGraph" ]); + TIP.keydown(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "AltGraph" ]); + TIP.keyup(keyA); + checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "AltGraph" ]); + TIP.keyup(altGrVirtual); + checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keyup", [ ]); + + is(events.length, 10, + description + testDesc + " should cause 10 events"); + checkModifiers(testDesc, events[0], "keydown", "Shift", "", [ "Shift" ]); + checkModifiers(testDesc, events[1], "keydown", "AltGraph", "", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[2], "keydown", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[3], "keypress", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[4], "keyup", "a", "KeyA", [ "Shift", "AltGraph" ]); + checkModifiers(testDesc, events[5], "keyup", "Shift", "", [ "AltGraph" ]); + checkModifiers(testDesc, events[6], "keydown", "a", "KeyA", [ "AltGraph" ]); + checkModifiers(testDesc, events[7], "keypress", "a", "KeyA", [ "AltGraph" ]); + checkModifiers(testDesc, events[8], "keyup", "a", "KeyA", [ "AltGraph" ]); + checkModifiers(testDesc, events[9], "keyup", "AltGraph", "", [ ]); + + // shareModifierStateOf(null) should cause resetting the modifier state + function checkTIPModifiers(aTestDesc, aTIP, aModifiers) + { + /* eslint-disable-next-line no-shadow */ + for (var i = 0; i < kModifiers.length; i++) { + is(aTIP.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]), + description + aTestDesc + ", aTIP.getModifierState(\"" + kModifiers[i] + "\") returns wrong value"); + } + } + TIP.keydown(shiftVirtual); + TIP.keydown(altGrVirtual); + sharedTIP.shareModifierStateOf(null); + checkTIPModifiers("sharedTIP.sharedModifierStateOf(null) shouldn't cause TIP's modifiers reset", TIP, [ "Shift", "AltGraph" ]); + checkTIPModifiers("sharedTIP.sharedModifierStateOf(null) should cause sharedTIP modifiers reset", sharedTIP, [ ]); + + // sharedTIP.shareModifierStateOf(null) should be unlinked from TIP. + TIP.keydown(ctrlVirtual); + checkTIPModifiers("TIP.keydown(ctrlVirtual) should cause TIP's modifiers set", TIP, [ "Shift", "AltGraph", "Control" ]); + checkTIPModifiers("TIP.keydown(ctrlVirtual) shouldn't cause sharedTIP modifiers set", sharedTIP, [ ]); + + // beginInputTransactionForTests() shouldn't cause modifier state reset. + ok(TIP.beginInputTransactionForTests(otherWindow), + description + "TIP.beginInputTransactionForTests(otherWindow) should return true"); + checkTIPModifiers("TIP.beginInputTransactionForTests(otherWindow) shouldn't cause TIP's modifiers set", TIP, [ "Shift", "AltGraph", "Control" ]); + TIP.keyup(shiftLeft); + TIP.keyup(altGrVirtual); + TIP.keyup(ctrlVirtual); + checkTIPModifiers("TIP should keep modifier's physical key state", TIP, [ "Shift" ]); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests(window) should return true"); + checkTIPModifiers("TIP.beginInputTransactionForTests(window) shouldn't cause TIP's modifiers set", TIP, [ "Shift" ]); + TIP.keyup(shiftVirtual); + checkTIPModifiers("TIP should keep modifier's physical key state", TIP, [ ]); + + window.removeEventListener("keydown", handler, false); + window.removeEventListener("keypress", handler, false); + window.removeEventListener("keyup", handler, false); +} + +function runErrorTests() +{ + var description = "runErrorTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + input.value = ""; + input.focus(); + + // startComposition() should throw an exception if there is already a composition + TIP.startComposition(); + try { + TIP.startComposition(); + ok(false, + description + "startComposition() should fail if it was already called"); + } catch (e) { + ok(e.message.includes("NS_ERROR_FAILURE"), + description + "startComposition() should cause NS_ERROR_FAILURE if there is already composition"); + } finally { + TIP.cancelComposition(); + } + + // cancelComposition() should throw an exception if there is no composition + try { + TIP.cancelComposition(); + ok(false, + description + "cancelComposition() should fail if there is no composition"); + } catch (e) { + ok(e.message.includes("NS_ERROR_FAILURE"), + description + "cancelComposition() should cause NS_ERROR_FAILURE if there is no composition"); + } + + // commitComposition() without commit string should throw an exception if there is no composition + try { + TIP.commitComposition(); + ok(false, + description + "commitComposition() should fail if there is no composition"); + } catch (e) { + ok(e.message.includes("NS_ERROR_FAILURE"), + description + "commitComposition() should cause NS_ERROR_FAILURE if there is no composition"); + } + + // commitCompositionWith("") should throw an exception if there is no composition + try { + TIP.commitCompositionWith(""); + ok(false, + description + "commitCompositionWith(\"\") should fail if there is no composition"); + } catch (e) { + ok(e.message.includes("NS_ERROR_FAILURE"), + description + "commitCompositionWith(\"\") should cause NS_ERROR_FAILURE if there is no composition"); + } + + // Pending composition string should allow to flush without clause information (for compatibility) + try { + TIP.setPendingCompositionString("foo"); + TIP.flushPendingComposition(); + ok(true, + description + "flushPendingComposition() should succeed even if appendClauseToPendingComposition() has never been called"); + TIP.cancelComposition(); + } catch (e) { + ok(false, + description + "flushPendingComposition() shouldn't cause an exception even if appendClauseToPendingComposition() has never been called"); + } + + // Pending composition string must be filled by clause information + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + ok(false, + description + "flushPendingComposition() should fail if appendClauseToPendingComposition() doesn't fill all composition string"); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "flushPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if appendClauseToPendingComposition() doesn't fill all composition string"); + } + + // Pending composition string must not be shorter than appended clause length + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + ok(false, + description + "flushPendingComposition() should fail if appendClauseToPendingComposition() appends longer clause information"); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "flushPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if appendClauseToPendingComposition() appends longer clause information"); + } + + // Pending composition must not have clause information with empty string + try { + TIP.appendClauseToPendingComposition(1, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + ok(false, + description + "flushPendingComposition() should fail if there is a clause with empty string"); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "flushPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if there is a clause with empty string"); + } + + // Appending a clause whose length is 0 should cause an exception + try { + TIP.appendClauseToPendingComposition(0, TIP.ATTR_RAW_CLAUSE); + ok(false, + description + "appendClauseToPendingComposition() should fail if the length is 0"); + TIP.flushPendingComposition(); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "appendClauseToPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if the length is 0"); + } + + // Appending a clause whose attribute is invalid should cause an exception + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, 0); + ok(false, + description + "appendClauseToPendingComposition() should fail if the attribute is invalid"); + TIP.flushPendingComposition(); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "appendClauseToPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if the attribute is invalid"); + } + + // Setting caret position outside of composition string should cause an exception + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(4); + TIP.flushPendingComposition(); + ok(false, + description + "flushPendingComposition() should fail if caret position is out of composition string"); + TIP.cancelComposition(); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "flushPendingComposition() should cause NS_ERROR_ILLEGAL_VALUE if caret position is out of composition string"); + } + + // Calling keydown() with a KeyboardEvent initialized with invalid code value should cause an exception. + input.value = ""; + try { + var keyInvalidCode = new KeyboardEvent("", { key: "f", code: "InvalidCodeValue", keyCode: KeyboardEvent.DOM_VK_F }); + TIP.keydown(keyInvalidCode); + ok(false, + description + "TIP.keydown(keyInvalidCode) should cause throwing an exception because its code value is not registered"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "TIP.keydown(keyInvalidCode) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // Calling keyup() with a KeyboardEvent initialized with invalid code value should cause an exception. + input.value = ""; + try { + var keyInvalidCode = new KeyboardEvent("", { key: "f", code: "InvalidCodeValue", keyCode: KeyboardEvent.DOM_VK_F }); + TIP.keyup(keyInvalidCode); + ok(false, + description + "TIP.keyup(keyInvalidCode) should cause throwing an exception because its code value is not registered"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "TIP.keyup(keyInvalidCode) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // Calling keydown(KEY_NON_PRINTABLE_KEY) with a KeyboardEvent initialized with non-key name should cause an exception. + input.value = ""; + try { + var keyInvalidKey = new KeyboardEvent("", { key: "ESCAPE", code: "Escape", keyCode: KeyboardEvent.DOM_VK_Escape}); + TIP.keydown(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY); + ok(false, + description + "TIP.keydown(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY) should cause throwing an exception because its key value is not registered"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keydown(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // Calling keyup(KEY_NON_PRINTABLE_KEY) with a KeyboardEvent initialized with non-key name should cause an exception. + input.value = ""; + try { + var keyInvalidKey = new KeyboardEvent("", { key: "ESCAPE", code: "Escape", keyCode: KeyboardEvent.DOM_VK_Escape}); + TIP.keydown(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY); + ok(false, + description + "TIP.keyup(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY) should cause throwing an exception because its key value is not registered"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keyup(keyInvalidKey, TIP.KEY_NON_PRINTABLE_KEY) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE"); + } finally { + is(input.value, "", + description + "The input element should not be modified"); + } + + // KEY_KEEP_KEY_LOCATION_STANDARD flag should be used only when .location is not initialized with non-zero value. + try { + var keyEvent = new KeyboardEvent("", { code: "Enter", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }); + TIP.keydown(keyEvent, TIP.KEY_KEEP_KEY_LOCATION_STANDARD); + ok(false, + description + "keydown(KEY_KEEP_KEY_LOCATION_STANDARD) should fail if the .location of the key event is initialized with non-zero value"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keydown(KEY_KEEP_KEY_LOCATION_STANDARD) should cause NS_ERROR_ILLEGAL_VALUE if the .location of the key event is initialized with nonzero value"); + } + try { + var keyEvent = new KeyboardEvent("", { code: "Enter", location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }); + TIP.keyup(keyEvent, TIP.KEY_KEEP_KEY_LOCATION_STANDARD); + ok(false, + description + "keyup(KEY_KEEP_KEY_LOCATION_STANDARD) should fail if the .location of the key event is initialized with non-zero value"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keyup(KEY_KEEP_KEY_LOCATION_STANDARD) should cause NS_ERROR_ILLEGAL_VALUE if the .location of the key event is initialized with nonzero value"); + } + + // KEY_KEEP_KEYCODE_ZERO flag should be used only when .keyCode is not initialized with non-zero value. + try { + var keyEvent = new KeyboardEvent("", { key: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + TIP.keydown(keyEvent, TIP.KEY_KEEP_KEYCODE_ZERO); + ok(false, + description + "keydown(KEY_KEEP_KEYCODE_ZERO) should fail if the .keyCode of the key event is initialized with non-zero value"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keydown(KEY_KEEP_KEYCODE_ZERO) should cause NS_ERROR_ILLEGAL_VALUE if the .keyCode of the key event is initialized with nonzero value"); + } + try { + var keyEvent = new KeyboardEvent("", { key: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + TIP.keyup(keyEvent, TIP.KEY_KEEP_KEYCODE_ZERO); + ok(false, + description + "keyup(KEY_KEEP_KEYCODE_ZERO) should fail if the .keyCode of the key event is initialized with non-zero value"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keyup(KEY_KEEP_KEYCODE_ZERO) should cause NS_ERROR_ILLEGAL_VALUE if the .keyCode of the key event is initialized with nonzero value"); + } + + // Specifying KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT with non-modifier key, it should cause an exception. + try { + var keyEvent = new KeyboardEvent("", { key: "a", code: "ShiftLeft" }); + TIP.keyup(keyEvent, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + ok(false, + description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should fail if the .key value isn't a modifier key"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should cause NS_ERROR_ILLEGAL_VALUE if the .key value isn't a modifier key"); + } + try { + var keyEvent = new KeyboardEvent("", { key: "Enter", code: "ShiftLeft" }); + TIP.keyup(keyEvent, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + ok(false, + description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should fail if the .key value isn't a modifier key"); + } catch (e) { + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should cause NS_ERROR_ILLEGAL_VALUE if the .key value isn't a modifier key"); + } + + // The type of key events specified to composition methods should be "" or "keydown". + var kKeyEventTypes = [ + { type: "keydown", valid: true }, + { type: "keypress", valid: false }, + { type: "keyup", valid: false }, + { type: "", valid: true }, + { type: "mousedown", valid: false }, + { type: "foo", valid: false }, + ]; + for (var i = 0; i < kKeyEventTypes[i].length; i++) { + var keyEvent = + new KeyboardEvent(kKeyEventTypes[i].type, { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var testDescription = description + "type=\"" + kKeyEventTypes[i].type + "\", "; + try { + TIP.startComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.startComposition(keyEvent) should not accept the event type"); + TIP.cancelComposition(); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.startComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.startComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.flushPendingComposition(keyEvent) should not accept the event type"); + TIP.cancelComposition(); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.flushPendingComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.flushPendingComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.startComposition(); + TIP.commitComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent) should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.commitComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + TIP.cancelComposition(); + } + try { + TIP.commitCompositionWith("foo", keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should not throw an exception for the event type"); + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.startComposition(); + TIP.cancelComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.cancelComposition(keyEvent) should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.cancelComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.cancelComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + TIP.cancelComposition(); + } + input.value = ""; + } +} + +function runCommitCompositionTests() +{ + var description = "runCommitCompositionTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + input.focus(); + + // commitComposition() should commit the composition with the last data. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + TIP.commitComposition(); + is(input.value, "foo", + description + "commitComposition() should commit the composition with the last data"); + + // commitCompositionWith("") should commit the composition with empty string. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + TIP.commitCompositionWith(""); + is(input.value, "", + description + "commitCompositionWith(\"\") should commit the composition with empty string"); + + function doCommit(aText) + { + TIP.commitCompositionWith(aText); + } + + // doCommit() should commit the composition with the last data. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommit(); + todo_is(input.value, "foo", + description + "doCommit() should commit the composition with the last data"); + + // doCommit("") should commit the composition with empty string. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommit(""); + is(input.value, "", + description + "doCommit(\"\") should commit the composition with empty string"); + + // doCommit(null) should commit the composition with empty string. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommit(null); + is(input.value, "", + description + "doCommit(null) should commit the composition with empty string"); + + // doCommit(undefined) should commit the composition with the last data. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommit(undefined); + todo_is(input.value, "foo", + description + "doCommit(undefined) should commit the composition with the last data"); + + function doCommitWithNullCheck(aText) + { + TIP.commitCompositionWith(aText ? aText : ""); + } + + // doCommitWithNullCheck() should commit the composition with the last data. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommitWithNullCheck(); + is(input.value, "", + description + "doCommitWithNullCheck() should commit the composition with empty string"); + + // doCommitWithNullCheck("") should commit the composition with empty string. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommitWithNullCheck(""); + is(input.value, "", + description + "doCommitWithNullCheck(\"\") should commit the composition with empty string"); + + // doCommitWithNullCheck(null) should commit the composition with empty string. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + doCommitWithNullCheck(null); + is(input.value, "", + description + "doCommitWithNullCheck(null) should commit the composition with empty string"); + + // doCommitWithNullCheck(undefined) should commit the composition with the last data. + input.value = ""; + TIP.setPendingCompositionString("foo"); + 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() +{ + return new Promise(resolve => { + let description = "runUnloadTests1(): "; + + let TIP1 = createTIP(); + ok(TIP1.beginInputTransactionForTests(childWindow), + description + "TIP1.beginInputTransactionForTests() should succeed"); + + let oldSrc = iframe.src; + let parentWindow = window; + + iframe.addEventListener("load", function (aEvent) { + ok(true, description + "dummy page is loaded"); + 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. + childWindow = iframe.contentWindow; + textareaInFrame = iframe.contentDocument.getElementById("textarea"); + SimpleTest.executeSoon(resolve); + }, {capture: true, once: true}); + + // The composition should be committed internally. So, another TIP should + // be able to steal the rights to using TextEventDispatcher. + let TIP2 = createTIP(); + ok(TIP2.beginInputTransactionForTests(parentWindow), + description + "TIP2.beginInputTransactionForTests() should succeed"); + + input.focus(); + input.value = ""; + + TIP2.setPendingCompositionString("foo"); + TIP2.appendClauseToPendingComposition(3, TIP2.ATTR_RAW_CLAUSE); + TIP2.setCaretInPendingComposition(3); + TIP2.flushPendingComposition(); + is(input.value, "foo", + description + "the input in the parent document should have composition string"); + + TIP2.cancelComposition(); + + // Restore the old iframe content. + iframe.src = oldSrc; + }, {capture: true, once: true}); + + // Start composition in the iframe. + textareaInFrame.value = ""; + textareaInFrame.focus(); + + TIP1.setPendingCompositionString("foo"); + TIP1.appendClauseToPendingComposition(3, TIP1.ATTR_RAW_CLAUSE); + TIP1.setCaretInPendingComposition(3); + 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() +{ + return new Promise(resolve => { + let description = "runUnloadTests2(): "; + + let TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(childWindow), + description + "TIP.beginInputTransactionForTests() should succeed"); + + let oldSrc = iframe.src; + + iframe.addEventListener("load", function (aEvent) { + ok(true, description + "dummy page is loaded"); + 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. + childWindow = iframe.contentWindow; + textareaInFrame = iframe.contentDocument.getElementById("textarea"); + SimpleTest.executeSoon(resolve); + }, {capture: true, once: 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); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + if (input.value == "") { + // XXX TextInputProcessor or TextEventDispatcher may have a bug. + todo_is(input.value, "bar", + description + "the input in the parent document should have composition string"); + } else { + is(input.value, "bar", + description + "the input in the parent document should have composition string"); + } + + TIP.cancelComposition(); + + // Restore the old iframe content. + iframe.src = oldSrc; + }, {capture: true, once: true}); + + // Start composition in the iframe. + textareaInFrame.value = ""; + textareaInFrame.focus(); + + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + 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>"; + }); +} + +async function runCallbackTests(aForTests) +{ + let description = "runCallbackTests(aForTests=" + aForTests + "): "; + + input.value = ""; + input.focus(); + input.blur(); + + let TIP = createTIP(); + let notifications = []; + let waitingNextNotification; + 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 (waitingNextNotification) { + SimpleTest.executeSoon(waitingNextNotification); + waitingNextNotification = undefined; + } + return true; + } + + function dumpUnexpectedNotifications(aExpectedCount) + { + if (notifications.length <= aExpectedCount) { + return; + } + for (let i = aExpectedCount; i < notifications.length; i++) { + ok(false, + description + "Unexpected notification: " + notifications[i].type); + } + } + + function waitUntilNotificationsReceived() + { + return new Promise(resolve => { + if (notifications.length) { + SimpleTest.executeSoon(resolve); + } else { + waitingNextNotification = resolve; + } + }); + } + + 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.hasRange, aExpected.hasRange !== false, + `${aDescription} should cause selection change notification whose hasRange is ${aExpected.hasRange}`); + if (aNotification.hasRange) { + 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.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.collapsed, aExpected.hasRange === false || !aExpected.text.length, + `${aDescription} should cause selection change notification whose collapsed is ${aExpected.hasRange === false || !aExpected.text.length}`); + 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(); + is(notifications.length, 1, + description + "input.focus() should cause a notification"); + is(notifications[0].type, "notify-focus", + description + "input.focus() should cause \"notify-focus\""); + dumpUnexpectedNotifications(1); + + notifications = []; + 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(); + await waitUntilNotificationsReceived(); + 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, 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); + + input.focus(); + await waitUntilNotificationsReceived(); + notifications = []; + // 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). + // Wayland also does not support it. + var isWayland = Services.prefs.getBoolPref("widget.wayland.test-workarounds.enabled", false); + if (!kIsMac && !isWayland) { + window.moveBy(0, 10); + await waitUntilNotificationsReceived(); + is(notifications.length, 1, + description + "window.moveBy(0, 10) should cause a notification"); + checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)"); + dumpUnexpectedNotifications(1); + + notifications = []; + window.moveBy(10, 0); + await 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; + await waitUntilNotificationsReceived(); + notifications = []; + let 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 = []; + let shiftKeyEvent = + new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT }); + let 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 = []; + input.editor.selection.removeAllRanges(); + await waitUntilNotificationsReceived(); + is(notifications.length, 1, + `${description}Removing all selection ranges should cause a selection change notification`); + checkSelectionChangeNotification( + notifications[0], + `${description}Removing all selection ranges in editor`, + { hasRange: false } + ); + dumpUnexpectedNotifications(1); + + notifications = []; + let 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); +} + +async function runFocusNotificationTestAfterDrop() { + const inputs = document.querySelectorAll("input[type=text]"); + inputs[0].value = "abc"; + inputs[1].value = ""; + + const TIP = createTIP(); + let notifications = []; + function callback(aTIP, aNotification) + { + if (aTIP != TIP) { + return true; + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + case "notify-focus": + case "notify-blur": + notifications.push(aNotification.type); + break; + } + return true; + } + + inputs[0].focus(); + TIP.beginInputTransactionForTests(window, callback); + inputs[0].select(); + try { + notifications = []; + await synthesizePlainDragAndDrop({ + srcSelection: SpecialPowers.wrap(inputs[0]).editor.selection, + destElement: inputs[1], + }); + } catch (ex) { + ok(false, `runFocusNotificationTestAfterDrop: unexpected error during DnD (${ex.message})`); + return; + } + is( + document.activeElement, + inputs[1], + "runFocusNotificationTestAfterDrop: Dropping to the second <input> should make it focused" + ); + ok( + notifications.length > 1, + "runFocusNotificationTestAfterDrop: At least two notifications should be fired" + ); + if (notifications.length) { + is( + notifications[notifications.length - 1], + "notify-focus", + "runFocusNotificationTestAfterDrop: focus notification should've been fired at last" + ); + } +} + +async function runQuerySelectionEventTestAtTextChangeNotification() { + contenteditable.innerHTML = "<p>abc</p><p>def</p>"; + contenteditable.focus(); + // Ensure to send notify-focus from IMEContentObserver + await new Promise( + resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve) + ) + ); + document.execCommand("selectall"); + // Ensure to send notify-selection-change from IMEContentObserver + await new Promise( + resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve) + ) + ); + + const kTestName = "runQuerySelectionEventTestAtTextChangeNotification"; + await new Promise(resolve => { + const TIP = createTIP(); + TIP.beginInputTransactionForTests(window, (aTIP, aNotification) => { + if (aTIP != TIP) { + return true; + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + case "notify-text-change": + const textContent = synthesizeQueryTextContent(0, 100); + if (textContent?.text.includes("abc")) { + break; // Different notification which we want to test, wait next one. + } + ok( + textContent?.succeeded, + `${kTestName}: query text content should succeed from notify-text-change handler` + ); + const selectedText = synthesizeQuerySelectedText(); + ok( + selectedText?.succeeded, + `${kTestName}: query selected text should succeed from notify-text-change handler` + ); + if (textContent?.succeeded && selectedText?.succeeded) { + is( + selectedText.text, + textContent.text, + `${kTestName}: selected text should be same as all text` + ); + } + resolve(); + break; + } + return true; + }); + // TODO: We want to do this while selection is batched but can flush + // pending notifications, however, I have no idea how to do it. + contenteditable.firstChild.remove(); + }); +} + +async function runIMEStateUpdateTests() { + const TIP = createTIP(); + let notifications = []; + function callback(aTIP, aNotification) + { + if (aTIP != TIP) { + return true; + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + case "notify-focus": + case "notify-blur": + notifications.push(aNotification.type); + break; + } + return true; + } + + contenteditable.focus(); + TIP.beginInputTransactionForTests(window, callback); + await new Promise(resolve => requestAnimationFrame(() => + requestAnimationFrame(resolve) + )); // wait for flushing pending notifications if there is. + + // run IMEStateManager::UpdateIMEState to disable IME + notifications = []; + const editor = getHTMLEditor(window); + editor.flags |= Ci.nsIEditor.eEditorReadonlyMask; + await new Promise(resolve => requestAnimationFrame(() => + requestAnimationFrame(resolve) + )); // wait for flush pending notification even if handled asynchronously. + is( + notifications.length ? notifications[0] : undefined, + "notify-blur", + "runIMEStateUpdateTests: Making the HTMLEditor readonly should cause a blur notification" + ); + is( + notifications.length, + 1, + `runIMEStateUpdateTests: Making the HTMLEditor readonly should not cause any other notifications, but got ${ + notifications.length > 1 ? notifications[1] : "" + } notification` + ); + is( + SpecialPowers.getDOMWindowUtils(window)?.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `runIMEStateUpdateTests: Making the HTMLEditor readonly should make IME disabled` + ); + + // run IMEStateManager::UpdateIMEState to enable IME + notifications = []; + editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask; + await new Promise(resolve => requestAnimationFrame(() => + requestAnimationFrame(resolve) + )); // wait for flush pending notification even if handled asynchronously. + is( + notifications.length ? notifications[0] : undefined, + "notify-focus", + "runIMEStateUpdateTests: Making the HTMLEditor editable should cause a focus notification without blur notification" + ); + is( + notifications.length, + 1, + `runIMEStateUpdateTests: Making the HTMLEditor editable should not cause any other notifications, but got ${ + notifications.length > 1 ? notifications[1] : "" + } notification` + ); + is( + SpecialPowers.getDOMWindowUtils(window)?.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `runIMEStateUpdateTests: Making the HTMLEditor readonly should make IME disabled` + ); +} + +async function runTextNotificationChangesDuringNoFrame() { + const TIP = createTIP(); + let onTextChange; + function callback(aTIP, aNotification) + { + if (aTIP != TIP) { + return true; + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + case "notify-text-change": + if (onTextChange) { + onTextChange(aNotification); + } + break; + } + return true; + } + + function promiseTextChangeNotification() { + return new Promise(resolve => onTextChange = resolve); + } + + function waitForTick() { + return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + } + + const input = document.querySelector("input[type=text]"); + input.focus(); + TIP.beginInputTransactionForTests(window, callback); + + await (async function test_text_change_notification_for_value_set_during_no_frame() { + const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_value_set_during_no_frame"; + input.value = "Start"; + input.style.display = "inline"; + input.getBoundingClientRect(); + await waitForTick(); + const waitNotifications = promiseTextChangeNotification(); + input.style.display = "block"; + input.value = "Changed"; + info(`${description}: waiting for notifications...`); + const notification = await waitNotifications; + is( + notification?.offset, + 0, + `${description}: offset should be 0` + ); + is( + notification?.removedLength, + "Start".length, + `${description}: removedLength should be the length of the old value` + ); + is( + notification?.addedLength, + "Changed".length, + `${description}: addedLength should be the length of the new value` + ); + })(); + + await (async function test_text_change_notification_for_multiple_value_set_during_no_frame() { + const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_multiple_value_set_during_no_frame"; + input.value = "Start"; + input.style.display = "inline"; + input.getBoundingClientRect(); + await waitForTick(); + const waitNotifications = promiseTextChangeNotification(); + input.style.display = "block"; + input.value = "Changed"; + input.value = "Again!"; + info(`${description}: waiting for notifications...`); + const notification = await waitNotifications; + is( + notification?.offset, + 0, + `${description}: offset should be 0` + ); + is( + notification?.removedLength, + "Start".length, + `${description}: removedLength should be the length of the old value` + ); + is( + notification?.addedLength, + "Again!".length, + `${description}: addedLength should be the length of the new value` + ); + })(); + + await (async function test_text_change_notification_for_value_set_and_typing_character_during_no_frame() { + const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_value_set_and_typing_character_during_no_frame"; + input.value = "Start"; + input.style.display = "inline"; + input.getBoundingClientRect(); + await waitForTick(); + const waitNotifications = promiseTextChangeNotification(); + input.style.display = "block"; + input.value = "Change"; + const dKey = new KeyboardEvent("", { code: "KeyD", key: "d", keyCode: KeyboardEvent.DOM_VK_D }); + TIP.keydown(dKey); + TIP.keyup(dKey); + info(`${description}: waiting for notifications...`); + const notification = await waitNotifications; + is( + notification?.offset, + 0, + `${description}: offset should be 0` + ); + is( + notification?.removedLength, + "Start".length, + `${description}: removedLength should be the length of the old value` + ); + is( + notification?.addedLength, + "Change".length, + `${description}: addedLength should be the length of the new (set) value` + ); + })(); + + input.style.display = ""; + + textarea.focus(); + TIP.beginInputTransaction(window, callback); + + await (async function test_text_change_notification_for_multi_line_value_set_during_no_frame() { + const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_multi_line_value_set_during_no_frame"; + textarea.value = "Start\n2nd Line"; + textarea.style.display = "inline"; + textarea.getBoundingClientRect(); + await waitForTick(); + const waitNotifications = promiseTextChangeNotification(); + textarea.style.display = "block"; + textarea.value = "Changed\n2nd Line"; + info(`${description}: waiting for notifications...`); + const notification = await waitNotifications; + is( + notification?.offset, + 0, + `${description}: offset should be 0` + ); + is( + notification?.removedLength, + getNativeText("Start\n2nd Line").length, + `${description}: removedLength should be the length of the old value` + ); + is( + notification?.addedLength, + getNativeText("Changed\n2nd Line").length, + `${description}: addedLength should be the length of the new value` + ); + })(); + + textarea.style.display = ""; +} + +async function runTests() +{ + textareaInFrame = iframe.contentDocument.getElementById("textarea"); + runBeginInputTransactionMethodTests(); + runReleaseTests(); + runCompositionTests(); + runCompositionWithKeyEventTests(); + runConsumingKeydownBeforeCompositionTests(); + runKeyTests(); + runErrorTests(); + runCommitCompositionTests(); + await runCallbackTests(false); + await runCallbackTests(true); + await runTextNotificationChangesDuringNoFrame(); + await runFocusNotificationTestAfterDrop(); + await runUnloadTests1(); + await runUnloadTests2(); + await runQuerySelectionEventTestAtTextChangeNotification(); + await runIMEStateUpdateTests(); + + finish(); +} + +]]> +</script> + +</window> |