Blob Blame History Raw
<?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 type="application/javascript"
        src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<body  xmlns="http://www.w3.org/1999/xhtml">
<p id="display">
<input id="input" type="text"/><br/>
<iframe id="iframe" width="300" height="150"
        src="data:text/html,&lt;textarea id='textarea' cols='20' rows='4'&gt;&lt;/textarea&gt;"></iframe><br/>
</p>
<div id="content" style="display: none">

</div>
<pre id="test">
</pre>
</body>

<script class="testbody" type="application/javascript">
<![CDATA[

var SpecialPowers = window.opener.wrappedJSObject.SpecialPowers;
var SimpleTest = window.opener.wrappedJSObject.SimpleTest;

SimpleTest.waitForFocus(runTests, window);

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 finish()
{
  window.close();
}

function onunload()
{
  SimpleTest.finish();
}

const kIsMac = (navigator.platform.indexOf("Mac") == 0);

var iframe = document.getElementById("iframe");
var childWindow = iframe.contentWindow;
var textareaInFrame;
var input = document.getElementById("input");
var otherWindow = window.opener;
var otherDocument = otherWindow.document;
var inputInChildWindow = otherDocument.getElementById("input");

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 be committed with the last composing string");

  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);
    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 a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("compositionupdate", 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 compositionupdate event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("text", 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 text event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 4,
     description + "compositionstart, compositionupdate, text 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, "input",
     description + "events[3] should be input");
  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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.commitComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.commitComposition();");
  }, false);
  TIP1.commitComposition();
  is(events.length, 3,
     description + "text, 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, "compositionend",
     description + "events[1] should be compositionend");
  is(events[2].type, "input",
     description + "events[2] should be input");

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
  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.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("compositionupdate", 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 during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("text", 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 during text event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("compositionend", 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 during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("input", 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 during input event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.commitCompositionWith("bar");
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, 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, "compositionend",
     description + "events[3] should be compositionend");
  is(events[4].type, "input",
     description + "events[4] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("text", 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 text event handler during a call of TIP1.cancelComposition();");
  }, false);
  input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.cancelComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.cancelComposition();");
  }, false);
  TIP1.cancelComposition();
  is(events.length, 4,
     description + "compositionupdate, text, 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, "compositionend",
     description + "events[2] should be compositionend");
  is(events[3].type, "input",
     description + "events[3] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("keypress", 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 keypress event handler during a call of TIP1.keydown();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.keydown();");
  }, false);
  input.addEventListener("keyup", 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 keyup event handler during a call of TIP1.keyup();");
  }, false);
  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 4,
     description + "keydown, keypress, 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, "input",
     description + "events[2] should be input");
  is(events[3].type, "keyup",
     description + "events[3] 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);
    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 a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("compositionupdate", 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 compositionupdate event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("text", 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 text event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.flushPendingComposition();");
  }, false);
  TIP1.beginInputTransactionForTests(window);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 4,
     description + "compositionstart, compositionupdate, text 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, "input",
     description + "events[3] should be input");
  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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.commitComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.commitComposition();");
  }, false);
  TIP1.commitComposition();
  is(events.length, 3,
     description + "text, 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, "compositionend",
     description + "events[1] should be compositionend");
  is(events[2].type, "input",
     description + "events[2] should be input");

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
  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.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("compositionupdate", 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 during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("text", 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 during text event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("compositionend", 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 during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  input.addEventListener("input", 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 during input event handler TIP1.commitCompositionWith(\"bar\");");
  }, false);
  TIP1.beginInputTransactionForTests(window);
  TIP1.commitCompositionWith("bar");
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, 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, "compositionend",
     description + "events[3] should be compositionend");
  is(events[4].type, "input",
     description + "events[4] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("text", 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 text event handler during a call of TIP1.cancelComposition();");
  }, false);
  input.addEventListener("compositionend", 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 compositionend event handler during a call of TIP1.cancelComposition();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.cancelComposition();");
  }, false);
  TIP1.cancelComposition();
  is(events.length, 4,
     description + "compositionupdate, text, 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, "compositionend",
     description + "events[2] should be compositionend");
  is(events[3].type, "input",
     description + "events[3] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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();");
  }, false);
  input.addEventListener("keypress", 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 keypress event handler during a call of TIP1.keydown();");
  }, false);
  input.addEventListener("input", 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 input event handler during a call of TIP1.keydown();");
  }, false);
  input.addEventListener("keyup", 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 keyup event handler during a call of TIP1.keyup();");
  }, false);
  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 4,
     description + "keydown, keypress, 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, "input",
     description + "events[2] should be input");
  is(events[3].type, "keyup",
     description + "events[3] 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, 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() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 4,
     description + "compositionstart, compositionupdate, text 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, "input",
     description + "events[3] should be input");
  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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.commitComposition();
  is(events.length, 3,
     description + "text, 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, "compositionend",
     description + "events[1] should be compositionend");
  is(events[2].type, "input",
     description + "events[2] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.commitCompositionWith("bar");
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, 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, "compositionend",
     description + "events[3] should be compositionend");
  is(events[4].type, "input",
     description + "events[4] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.cancelComposition();
  is(events.length, 4,
     description + "compositionupdate, text, 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, "compositionend",
     description + "events[2] should be compositionend");
  is(events[3].type, "input",
     description + "events[3] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 4,
     description + "keydown, keypress, 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, "input",
     description + "events[2] should be input");
  is(events[3].type, "keyup",
     description + "events[3] 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.beginInputTransactionForTests(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 4,
     description + "compositionstart, compositionupdate, text 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, "input",
     description + "events[3] should be input");
  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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.commitComposition();
  is(events.length, 3,
     description + "text, 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, "compositionend",
     description + "events[1] should be compositionend");
  is(events[2].type, "input",
     description + "events[2] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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\")");
    }
  }, false);
  TIP1.beginInputTransactionForTests(window, simpleCallback);
  TIP1.commitCompositionWith("bar");
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, 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, "compositionend",
     description + "events[3] should be compositionend");
  is(events[4].type, "input",
     description + "events[4] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("compositionend", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  TIP1.cancelComposition();
  is(events.length, 4,
     description + "compositionupdate, text, 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, "compositionend",
     description + "events[2] should be compositionend");
  is(events[3].type, "input",
     description + "events[3] should be input");

  // 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);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    input.removeEventListener(aEvent.type, arguments.callee, false);
    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()");
    }
  }, false);
  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 4,
     description + "keydown, keypress, 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, "input",
     description + "events[2] should be input");
  is(events[3].type, "keyup",
     description + "events[3] 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.
  SpecialPowers.gc();

  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 shoudn'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 });

  SpecialPowers.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");

  SpecialPowers.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)");

  SpecialPowers.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 });

  SpecialPowers.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");

  SpecialPowers.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");

  SpecialPowers.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 {
    SpecialPowers.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");

    SpecialPowers.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();
    SpecialPowers.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");
    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)
  {
    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)
  {
    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()
{
  var description = "runUnloadTests1(): ";

  var TIP1 = createTIP();
  ok(TIP1.beginInputTransactionForTests(childWindow),
     description + "TIP1.beginInputTransactionForTests() should succeed");

  var oldSrc = iframe.src;
  var parentWindow = window;

  iframe.addEventListener("load", function (aEvent) {
    ok(true, description + "dummy page is loaded");
    iframe.removeEventListener("load", arguments.callee, true);
    childWindow = iframe.contentWindow;
    textareaInFrame = null;
    iframe.addEventListener("load", function () {
      ok(true, description + "old iframe is restored");
      // And also restore the iframe information with restored contents.
      iframe.removeEventListener("load", arguments.callee, true);
      childWindow = iframe.contentWindow;
      textareaInFrame = iframe.contentDocument.getElementById("textarea");
      SimpleTest.executeSoon(continueTest);
    }, true);

    // The composition should be committed internally.  So, another TIP should
    // be able to steal the rights to using TextEventDispatcher.
    var TIP2 = createTIP();
    ok(TIP2.beginInputTransactionForTests(parentWindow),
       description + "TIP2.beginInputTransactionForTests() should succeed");

    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;
  }, 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()
{
  var description = "runUnloadTests2(): ";

  var TIP = createTIP();
  ok(TIP.beginInputTransactionForTests(childWindow),
     description + "TIP.beginInputTransactionForTests() should succeed");

  var oldSrc = iframe.src;
  var parentWindow = window;

  iframe.addEventListener("load", function (aEvent) {
    ok(true, description + "dummy page is loaded");
    iframe.removeEventListener("load", arguments.callee, true);
    childWindow = iframe.contentWindow;
    textareaInFrame = null;
    iframe.addEventListener("load", function () {
      ok(true, description + "old iframe is restored");
      // And also restore the iframe information with restored contents.
      iframe.removeEventListener("load", arguments.callee, true);
      childWindow = iframe.contentWindow;
      textareaInFrame = iframe.contentDocument.getElementById("textarea");
      SimpleTest.executeSoon(continueTest);
    }, true);

    input.focus();
    input.value = "";

    // TIP should be still available in the same top level widget.
    TIP.setPendingCompositionString("bar");
    TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
    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;
  }, 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>";
}

function* runCallbackTests(aForTests)
{
  var description = "runCallbackTests(aForTests=" + aForTests + "): ";

  input.value = "";
  input.focus();
  input.blur();

  var TIP = createTIP();
  var notifications = [];
  var callContinueTest = false;
  function callback(aTIP, aNotification)
  {
    if (aTIP == TIP) {
      notifications.push(aNotification);
    }
    switch (aNotification.type) {
      case "request-to-commit":
        aTIP.commitComposition();
        break;
      case "request-to-cancel":
        aTIP.cancelComposition();
        break;
    }
    if (callContinueTest) {
      callContinueTest = false;
      SimpleTest.executeSoon(continueTest);
    }
    return true;
  }

  function dumpUnexpectedNotifications(aExpectedCount)
  {
    if (notifications.length <= aExpectedCount) {
      return;
    }
    for (var i = aExpectedCount; i < notifications.length; i++) {
      ok(false,
         description + "Unexpected notification: " + notifications[i].type);
    }
  }

  function waitUntilNotificationsReceived()
  {
    if (notifications.length > 0) {
      SimpleTest.executeSoon(continueTest);
    } else {
      callContinueTest = true;
    }
  }

  function checkPositionChangeNotification(aNotification, aDescription)
  {
    is(!aNotification || aNotification.type, "notify-position-change",
       aDescription + " should cause position change notification");
  }

  function checkSelectionChangeNotification(aNotification, aDescription, aExpected)
  {
    is(aNotification.type, "notify-selection-change",
       aDescription + " should cause selection change notification");
    if (aNotification.type != "notify-selection-change") {
      return;
    }
    is(aNotification.offset, aExpected.offset,
       aDescription + " should cause selection change notification whose offset is " + aExpected.offset);
    is(aNotification.text, aExpected.text,
       aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'");
    is(aNotification.collapsed, aExpected.text.length == 0,
       aDescription + " should cause selection change notification whose collapsed is " + (aExpected.text.length == 0));
    is(aNotification.length, aExpected.text.length,
       aDescription + " should cause selection change notification whose length is " + aExpected.text.length);
    is(aNotification.reversed, aExpected.reversed || false,
       aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false));
    is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb",
       aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb"));
    is(aNotification.causedByComposition, aExpected.causedByComposition || false,
       aDescription + " should cause selection change notification whose causedByComposition is " + (aExpected.causedByComposition || false));
    is(aNotification.causedBySelectionEvent, aExpected.causedBySelectionEvent || false,
       aDescription + " should cause selection change notification whose causedBySelectionEvent is " + (aExpected.causedBySelectionEvent || false));
    is(aNotification.occurredDuringComposition, aExpected.occurredDuringComposition || false,
       aDescription + " should cause cause selection change notification whose occurredDuringComposition is " + (aExpected.occurredDuringComposition || false));
  }

  function checkTextChangeNotification(aNotification, aDescription, aExpected)
  {
    is(aNotification.type, "notify-text-change",
       aDescription + " should cause text change notification");
    if (aNotification.type != "notify-text-change") {
      return;
    }
    is(aNotification.offset, aExpected.offset,
       aDescription + " should cause text change notification whose offset is " + aExpected.offset);
    is(aNotification.removedLength, aExpected.removedLength,
       aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength);
    is(aNotification.addedLength, aExpected.addedLength,
       aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength);
    is(aNotification.causedOnlyByComposition, aExpected.causedOnlyByComposition || false,
       aDescription + " should cause text change notification whose causedOnlyByComposition is " + (aExpected.causedOnlyByComposition || false));
    is(aNotification.includingChangesDuringComposition, aExpected.includingChangesDuringComposition || false,
       aDescription + " should cause text change notification whose includingChangesDuringComposition is " + (aExpected.includingChangesDuringComposition || false));
    is(aNotification.includingChangesWithoutComposition, typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true,
       aDescription + " should cause text change notification whose includingChangesWithoutComposition is " + (typeof aExpected.includingChangesWithoutComposition === "boolean" ? aExpected.includingChangesWithoutComposition : true));
  }

  if (aForTests) {
    TIP.beginInputTransactionForTests(window, callback);
  } else {
    TIP.beginInputTransaction(window, callback);
  }

  notifications = [];
  input.focus();
  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();
  yield 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();
  yield 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).
  if (!kIsMac) {
    window.moveBy(0, 10);
    yield waitUntilNotificationsReceived();
    is(notifications.length, 1,
       description + "window.moveBy(0, 10) should cause a notification");
    checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)");
    dumpUnexpectedNotifications(1);

    notifications = [];
    window.moveBy(10, 0);
    yield waitUntilNotificationsReceived();
    is(notifications.length, 1,
       description + "window.moveBy(10, 0) should cause a notification");
    checkPositionChangeNotification(notifications[0], description + "window.moveBy(10, 0)");
    dumpUnexpectedNotifications(1);
  }

  input.focus();
  input.value = "abc"
  notifications = [];
  input.selectionStart = input.selectionEnd = 0;
  yield waitUntilNotificationsReceived();
  notifications = [];
  var rightArrowKeyEvent =
    new KeyboardEvent("", { key: "ArrowRight", code: "ArrowRight", keyCode: KeyboardEvent.DOM_VK_RIGHT });
  TIP.keydown(rightArrowKeyEvent);
  TIP.keyup(rightArrowKeyEvent);
  is(notifications.length, 1,
     description + "ArrowRight key press should cause a notification");
  checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press", { offset: 1, text: "" });
  dumpUnexpectedNotifications(1);

  notifications = [];
  var shiftKeyEvent =
    new KeyboardEvent("", { key: "Shift", code: "ShiftLeft", keyCode: KeyboardEvent.DOM_VK_SHIFT });
  var leftArrowKeyEvent =
    new KeyboardEvent("", { key: "ArrowLeft", code: "ArrowLeft", keyCode: KeyboardEvent.DOM_VK_LEFT });
  TIP.keydown(shiftKeyEvent);
  TIP.keydown(leftArrowKeyEvent);
  TIP.keyup(leftArrowKeyEvent);
  TIP.keyup(shiftKeyEvent);
  is(notifications.length, 1,
     description + "ArrowLeft key press with Shift should cause a notification");
  checkSelectionChangeNotification(notifications[0], description + "ArrowLeft key press with Shift", { offset: 0, text: "a", reversed: true });
  dumpUnexpectedNotifications(1);

  TIP.keydown(rightArrowKeyEvent);
  TIP.keyup(rightArrowKeyEvent);
  notifications = [];
  TIP.keydown(shiftKeyEvent);
  TIP.keydown(rightArrowKeyEvent);
  TIP.keyup(rightArrowKeyEvent);
  TIP.keyup(shiftKeyEvent);
  is(notifications.length, 1,
     description + "ArrowRight key press with Shift should cause a notification");
  checkSelectionChangeNotification(notifications[0], description + "ArrowRight key press with Shift", { offset: 1, text: "b" });
  dumpUnexpectedNotifications(1);

  notifications = [];
  var TIP2 = createTIP();
  if (aForTests) {
    TIP2.beginInputTransactionForTests(window, callback);
  } else {
    TIP2.beginInputTransaction(window, callback);
  }
  is(notifications.length, 1,
     description + "Initializing another TIP should cause a notification");
  is(notifications[0].type, "notify-end-input-transaction",
     description + "Initializing another TIP should cause \"notify-detached\"");
  dumpUnexpectedNotifications(1);
}

var gTestContinuation = null;

function continueTest()
{
  if (!gTestContinuation) {
    gTestContinuation = testBody();
  }
  var ret = gTestContinuation.next();
  if (ret.done) {
    finish();
  }
}

function* testBody()
{
  runBeginInputTransactionMethodTests();
  runReleaseTests();
  runCompositionTests();
  runCompositionWithKeyEventTests();
  runConsumingKeydownBeforeCompositionTests();
  runKeyTests();
  runErrorTests();
  runCommitCompositionTests();
  yield* runCallbackTests(false);
  yield* runCallbackTests(true);
  yield runUnloadTests1();
  yield runUnloadTests2();
}

function runTests()
{
  textareaInFrame = iframe.contentDocument.getElementById("textarea");
  continueTest();
}

]]>
</script>

</window>