Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  window_nsITextInputProcessor.xhtml   Sprache: unbekannt

 
Spracherkennung für: .xhtml vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                 type="text/css"?>
<window title="Testing nsITextInputProcessor behavior"
  xmlns:html="http://www.w3.org/1999/xhtml"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  onunload="onunload();">
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<body  xmlns="http://www.w3.org/1999/xhtml">
<div id="display">
<input id="input" type="text"/><input id="anotherInput" type="text"/><br/>
<textarea></textarea>
<iframe id="iframe" width="300" height="150"
        src="data:text/html,<textarea id='textarea' cols='20' rows='4'></textarea>"></iframe><br/>
<div contenteditable=""><br/></div>
</div>
<div id="content" style="display: none">

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

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

var SimpleTest = window.arguments[0].SimpleTest;

SimpleTest.waitForFocus(runTests, window);

function getHTMLEditor(aWindow) {
  return SpecialPowers.wrap(aWindow).docShell.editingSession?.getEditorForWindow(aWindow);
}

function ok(aCondition, aMessage)
{
  SimpleTest.ok(aCondition, aMessage);
}

function is(aLeft, aRight, aMessage)
{
  SimpleTest.is(aLeft, aRight, aMessage);
}

function isnot(aLeft, aRight, aMessage)
{
  SimpleTest.isnot(aLeft, aRight, aMessage);
}

function todo_is(aLeft, aRight, aMessage)
{
  SimpleTest.todo_is(aLeft, aRight, aMessage);
}

function info(aMessage) {
  SimpleTest.info(aMessage);
}

function finish()
{
  window.close();
}

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

function checkInputEvent(aEvent, aCancelable, aIsComposing, aInputType, aData, aDescription) {
  if (aEvent.type !== "input" && aEvent.type !== "beforeinput") {
    console.trace();
    throw new Error(`${aDescription}"${aEvent.type}" is not InputEvent`);
  }
  ok(InputEvent.isInstance(aEvent), `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface`);
  is(aEvent.cancelable, aCancelable, `${aDescription}"${aEvent.type}" event should ${aCancelable ? "be" : "not be"} cancelable`);
  is(aEvent.bubbles, true, `${aDescription}"${aEvent.type}" event should always bubble`);
  is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing of "${aEvent.type}" event should be ${aIsComposing}`);
  is(aEvent.inputType, aInputType, `${aDescription}inputType of "${aEvent.type}" event should be "${aInputType}"`);
  is(aEvent.data, aData, `${aDescription}data of "${aEvent.type}" event should be "${aData}"`);
  is(aEvent.dataTransfer, null, `${aDescription}dataTransfer of "${aEvent.type}" event should be null`);
  is(aEvent.getTargetRanges().length, 0, `${aDescription}getTargetRanges() of "${aEvent.type}" event should return empty array`);
}

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

const iframe = document.getElementById("iframe");
let childWindow = iframe.contentWindow;
let textareaInFrame;
let input = document.getElementById("input");
const textarea = document.querySelector("textarea");
const otherWindow = window.arguments[0];
const otherDocument = otherWindow.document;
const inputInChildWindow = otherDocument.getElementById("input");
const contenteditable = document.querySelector("div[contenteditable]");
const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const kLF = "\n";
const kExpectInputBeforeCompositionEnd = SpecialPowers.getBoolPref("dom.input_events.dispatch_before_compositionend");

function getNativeText(aXPText)
{
  if (kLF == "\n") {
    return aXPText;
  }
  return aXPText.replace(/\n/g, kLF);
}

function createTIP()
{
  return Cc["@mozilla.org/text-input-processor;1"].
           createInstance(Ci.nsITextInputProcessor);
}

function runBeginInputTransactionMethodTests()
{
  var description = "runBeginInputTransactionMethodTests: ";
  input.value = "";
  input.focus();

  var simpleCallback = function (aTIP, aNotification)
  {
    switch (aNotification.type) {
      case "request-to-commit":
        aTIP.commitComposition();
        break;
      case "request-to-cancel":
        aTIP.cancelComposition();
        break;
    }
    return true;
  };

  var TIP1 = createTIP();
  var TIP2 = createTIP();
  isnot(TIP1, TIP2,
        description + "TIP instances should be different");

  // beginInputTransaction() and beginInputTransactionForTests() can take ownership if there is no composition.
  ok(TIP1.beginInputTransaction(window, simpleCallback),
     description + "TIP1.beginInputTransaction(window) should succeed because there is no composition");
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests(window) should succeed because there is no composition");
  ok(TIP2.beginInputTransaction(window, simpleCallback),
     description + "TIP2.beginInputTransaction(window) should succeed because there is no composition");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests(window) should succeed because there is no composition");

  // Start composition with TIP1, then, other TIPs cannot take ownership during a composition.
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
  var composingStr = "foo";
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  ok(TIP1.flushPendingComposition(),
     description + "TIP1.flushPendingComposition() should return true becuase it should be valid composition");
  is(input.value, composingStr,
     description + "The input element should have composing string");

  // Composing nsITextInputProcessor instance shouldn't allow initialize it again.
  try {
    TIP1.beginInputTransaction(window, simpleCallback);
    ok(false,
       "TIP1.beginInputTransaction(window) should cause throwing an exception because it's composing with different purpose");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
       description + "TIP1.beginInputTransaction(window) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing for tests");
  }
  try {
    TIP1.beginInputTransactionForTests(otherWindow);
    ok(false,
       "TIP1.beginInputTransactionForTests(otherWindow) should cause throwing an exception because it's composing on different window");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
       description + "TIP1.beginInputTransaction(otherWindow) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing on this window");
  }
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests(window) should succeed because TextEventDispatcher was initialized with same purpose");
  ok(TIP1.beginInputTransactionForTests(childWindow),
     description + "TIP1.beginInputTransactionForTests(childWindow) should succeed because TextEventDispatcher was initialized with same purpose and is shared by window and childWindow");
  ok(!TIP2.beginInputTransaction(window, simpleCallback),
     description + "TIP2.beginInputTransaction(window) should not succeed because there is composition synthesized by TIP1");
  ok(!TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests(window) should not succeed because there is composition synthesized by TIP1");
  ok(!TIP2.beginInputTransaction(childWindow, simpleCallback),
     description + "TIP2.beginInputTransaction(childWindow) should not succeed because there is composition synthesized by TIP1");
  ok(!TIP2.beginInputTransactionForTests(childWindow),
     description + "TIP2.beginInputTransactionForTests(childWindow) should not succeed because there is composition synthesized by TIP1");
  ok(TIP2.beginInputTransaction(otherWindow, simpleCallback),
     description + "TIP2.beginInputTransaction(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window");
  ok(TIP2.beginInputTransactionForTests(otherWindow),
     description + "TIP2.beginInputTransactionForTests(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window");

  // Let's confirm that the composing string is NOT committed by above tests.
  TIP1.commitComposition();
  is(input.value, composingStr,
     description + "TIP1.commitString() without specifying commit string should commit current composition with the last composing string");
  ok(!TIP1.hasComposition,
     description + "TIP1.commitString() without specifying commit string should've end composition");

  ok(TIP1.beginInputTransaction(window, simpleCallback),
     description + "TIP1.beginInputTransaction() should succeed because there is no composition #2");
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition #2");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests() should succeed because the composition was already committed #2");

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during startComposition().
  var events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    // eslint-disable-next-line no-caller
    input.removeEventListener(aEvent.type, arguments.callee);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.startComposition();");
  });
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.startComposition();
  is(events.length, 1,
     description + "compositionstart event should be fired by TIP1.startComposition()");
  TIP1.cancelComposition();

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
  is(events[0].type, "compositionstart",
     description + "events[0] should be compositionstart");
  is(events[1].type, "compositionupdate",
     description + "events[1] should be compositionupdate");
  is(events[2].type, "text",
     description + "events[2] should be text");
  is(events[3].type, "beforeinput",
     description + "events[3] should be beforeinput");
  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
  is(events[4].type, "input",
     description + "events[4] should be input");
  checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description);
  TIP1.cancelComposition();

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition().
  (() => {
    events = [];
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.commitComposition();");
    }
    input.addEventListener("input", onInput);
    TIP1.commitComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 5 : 4,
      description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
    let index = -1;
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", composingStr, description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
  (() => {
    events = [];
    input.addEventListener("compositionstart", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction during text event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction during beforeinput event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction during input event handler TIP1.commitCompositionWith(\"bar\");");
    }
    input.addEventListener("input", onInput);
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.commitCompositionWith("bar");
    is(events.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
    let index = -1;
    is(events[++index].type, "compositionstart",
      `${description}events[${index}] should be compositionstart`);
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[3], false, true, "insertCompositionText", "bar", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "bar", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition().
  (() => {
    events = [];
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.cancelComposition();");
    }
    input.addEventListener("input", onInput);
    TIP1.cancelComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
    let index = -1;
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
  events = [];
  TIP1.beginInputTransaction(window, simpleCallback);
  input.addEventListener("keydown", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from keydown event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from keypress event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from beforeinput event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransaction(window, simpleCallback),
       description + "TIP2 shouldn't be able to begin input transaction from keyup event handler during a call of TIP1.keyup();");
  }, {once: true});
  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 5,
     description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
  is(events[0].type, "keydown",
     description + "events[0] should be keydown");
  is(events[1].type, "keypress",
     description + "events[1] should be keypress");
  is(events[2].type, "beforeinput",
     description + "events[2] should be beforeinput");
  checkInputEvent(events[2], true, false, "insertText", "a", description);
  is(events[3].type, "input",
     description + "events[3] should be input");
  checkInputEvent(events[3], false, false, "insertText", "a", description);
  is(events[4].type, "keyup",
     description + "events[4] should be keyup");

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during startComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    // eslint-disable-next-line no-caller
    input.removeEventListener(aEvent.type, arguments.callee);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.startComposition();");
  });
  TIP1.beginInputTransactionForTests(window);
  TIP1.startComposition();
  is(events.length, 1,
     description + "compositionstart event should be fired by TIP1.startComposition()");
  TIP1.cancelComposition();

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.flushPendingComposition();");
  }, {once: true});
  TIP1.beginInputTransactionForTests(window);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
  is(events[0].type, "compositionstart",
     description + "events[0] should be compositionstart");
  is(events[1].type, "compositionupdate",
     description + "events[1] should be compositionupdate");
  is(events[2].type, "text",
     description + "events[2] should be text");
  is(events[3].type, "beforeinput",
     description + "events[3] should be beforeinput");
  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
  is(events[4].type, "input",
     description + "events[4] should be input");
  checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description);
  TIP1.cancelComposition();

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition().
  (function () {
    events = [];
    TIP1.beginInputTransactionForTests(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.commitComposition();");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.commitComposition();");
    }
    input.addEventListener("input", onInput);
    TIP1.commitComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 5 : 4,
      description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
    let index = -1;
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", composingStr, description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
  (() => {
    events = [];
    input.addEventListener("compositionstart", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests during compositionupdate event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests during text event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests during compositionend event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests during beforeinput event handler TIP1.commitCompositionWith(\"bar\");");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests during input event handler TIP1.commitCompositionWith(\"bar\");");
    }
    input.addEventListener("input", onInput);
    TIP1.beginInputTransactionForTests(window);
    TIP1.commitCompositionWith("bar");
    is(events.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
    let index = -1;
    is(events[++index].type, "compositionstart",
      `${description}events[${index}] should be compositionstart`);
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "bar", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition().
  (() => {
    events = [];
    TIP1.beginInputTransactionForTests(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.cancelComposition();");
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.cancelComposition();");
    }
    input.addEventListener("input", onInput);
    TIP1.cancelComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
    let index = -1;
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
  events = [];
  TIP1.beginInputTransactionForTests(window);
  input.addEventListener("keydown", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests for tests from keydown event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from keypress event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from beforeinput event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.keydown();");
  }, {once: true});
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    ok(!TIP2.beginInputTransactionForTests(window),
       description + "TIP2 shouldn't be able to begin input transaction for tests from keyup event handler during a call of TIP1.keyup();");
  }, {once: true});
  keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 5,
     description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
  is(events[0].type, "keydown",
     description + "events[0] should be keydown");
  is(events[1].type, "keypress",
     description + "events[1] should be keypress");
  is(events[2].type, "beforeinput",
     description + "events[2] should be beforeinput");
  checkInputEvent(events[2], true, false, "insertText", "a", description);
  is(events[3].type, "input",
     description + "events[3] should be input");
  checkInputEvent(events[3], false, false, "insertText", "a", description);
  is(events[4].type, "keyup",
     description + "events[4] should be keyup");

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()");
    }
  }, {once: true});
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.startComposition();
  is(events.length, 1,
     description + "compositionstart event should be fired by TIP1.startComposition()");
  TIP1.cancelComposition();

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  TIP1.beginInputTransaction(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
  is(events[0].type, "compositionstart",
     description + "events[0] should be compositionstart");
  is(events[1].type, "compositionupdate",
     description + "events[1] should be compositionupdate");
  is(events[2].type, "text",
     description + "events[2] should be text");
  is(events[3].type, "beforeinput",
     description + "events[3] should be beforeinput");
  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
  is(events[4].type, "input",
     description + "events[4] should be input");
  checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description);
  TIP1.cancelComposition();

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
  (function () {
    events = [];
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.commitComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 5 : 4,
      description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
    let index = -1;
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", composingStr, description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
  (() => {
    events = [];
    input.addEventListener("compositionstart", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.commitCompositionWith("bar");
    is(events.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
    let index = -1;
    is(events[++index].type, "compositionstart",
      `${description}events[${index}] should be compositionstart`);
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "bar", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
  (() => {
    events = [];
    TIP1.beginInputTransaction(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransaction(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.cancelComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
    let index = -1;
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
  events = [];
  TIP1.beginInputTransaction(window, simpleCallback);
  input.addEventListener("keydown", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransaction(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransaction(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()");
    }
  }, {once: true});
  keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 5,
     description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
  is(events[0].type, "keydown",
     description + "events[0] should be keydown");
  is(events[1].type, "keypress",
     description + "events[1] should be keypress");
  is(events[2].type, "beforeinput",
     description + "events[2] should be beforeinput");
  checkInputEvent(events[2], true, false, "insertText", "a", description);
  is(events[3].type, "input",
     description + "events[3] should be input");
  checkInputEvent(events[3], false, false, "insertText", "a", description);
  is(events[4].type, "keyup",
     description + "events[4] should be keyup");

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during startComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during startComposition()");
    }
  }, {once: true});
  TIP1.beginInputTransactionForTests(window, simpleCallback);
  TIP1.startComposition();
  is(events.length, 1,
     description + "compositionstart event should be fired by TIP1.startComposition()");
  TIP1.cancelComposition();

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during flushPendingComposition().
  events = [];
  input.addEventListener("compositionstart", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("compositionupdate", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("text", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during flushPendingComposition()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during flushPendingComposition()");
    }
  }, {once: true});
  TIP1.beginInputTransactionForTests(window, simpleCallback);
  TIP1.setPendingCompositionString(composingStr);
  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
  TIP1.flushPendingComposition();
  is(events.length, 5,
     description + "compositionstart, compositionupdate, text, beforeinput and input events should be fired by TIP1.flushPendingComposition()");
  is(events[0].type, "compositionstart",
     description + "events[0] should be compositionstart");
  is(events[1].type, "compositionupdate",
     description + "events[1] should be compositionupdate");
  is(events[2].type, "text",
     description + "events[2] should be text");
  is(events[3].type, "beforeinput",
     description + "events[3] should be beforeinput");
  checkInputEvent(events[3], false, true, "insertCompositionText", composingStr, description);
  is(events[4].type, "input",
     description + "events[4] should be input");
  checkInputEvent(events[4], false, true, "insertCompositionText", composingStr, description);
  TIP1.cancelComposition();

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
  (() => {
    events = [];
    TIP1.beginInputTransactionForTests(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitComposition()");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.commitComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 5 : 4,
      description + "text, beforeinput, compositionend and input events should be fired by TIP1.commitComposition()");
    let index = -1;
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", composingStr, description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", composingStr, description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
  (() => {
    events = [];
    input.addEventListener("compositionstart", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionstart\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during commitCompositionWith(\"bar\")");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during commitCompositionWith(\"bar\")");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.beginInputTransactionForTests(window, simpleCallback);
    TIP1.commitCompositionWith("bar");
    is(events.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      description + "compositionstart, compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.commitCompositionWith(\"bar\")");
    let index = -1;
    is(events[++index].type, "compositionstart",
      `${description}events[${index}] should be compositionstart`);
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "bar", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "bar", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
  (() => {
    events = [];
    TIP1.beginInputTransactionForTests(window, simpleCallback);
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition();
    input.addEventListener("compositionupdate", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionupdate\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("text", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"text\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("compositionend", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"compositionend\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    input.addEventListener("beforeinput", function (aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }, {once: true});
    function onInput(aEvent) {
      events.push(aEvent);
      try {
        TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
        ok(false,
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during cancelComposition()");
      } catch (e) {
        ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
          description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during cancelComposition()");
      }
    }
    input.addEventListener("input", onInput);
    TIP1.cancelComposition();
    is(events.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      description + "compositionupdate, text, beforeinput, compositionend and input events should be fired by TIP1.cancelComposition()");
    let index = -1;
    is(events[++index].type, "compositionupdate",
      `${description}events[${index}] should be compositionupdate`);
    is(events[++index].type, "text",
      `${description}events[${index}] should be text`);
    is(events[++index].type, "beforeinput",
      `${description}events[${index}] should be beforeinput`);
    checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    if (kExpectInputBeforeCompositionEnd) {
      is(events[++index].type, "input",
        `${description}events[${index}] should be input`);
      checkInputEvent(events[index], false, true, "insertCompositionText", "", description);
    }
    is(events[++index].type, "compositionend",
      `${description}events[${index}] should be compositionend`);
    is(events[++index].type, "input",
      `${description}events[${index}] should be input`);
    checkInputEvent(events[index], false, false, "insertCompositionText", "", description);
    input.removeEventListener("input", onInput);
  })();

  // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
  events = [];
  TIP1.beginInputTransactionForTests(window, simpleCallback);
  input.addEventListener("keydown", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keydown\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("keypress", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keypress\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("beforeinput", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"beforeinput\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("input", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should throw an exception during keydown()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"input\" should cause NS_ERROR_ALREADY_INITIALIZED during keydown()");
    }
  }, {once: true});
  input.addEventListener("keyup", function (aEvent) {
    events.push(aEvent);
    try {
      TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
      ok(false,
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should throw an exception during keyup()");
    } catch (e) {
      ok(e.message.includes("NS_ERROR_ALREADY_INITIALIZED"),
         description + "TIP1.beginInputTransactionForTests(otherWindow, simpleCallback) called from \"keyup\" should cause NS_ERROR_ALREADY_INITIALIZED during keyup()");
    }
  }, {once: true});
  keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  TIP1.keydown(keyA);
  TIP1.keyup(keyA);
  is(events.length, 5,
     description + "keydown, keypress, beforeinput, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
  is(events[0].type, "keydown",
     description + "events[0] should be keydown");
  is(events[1].type, "keypress",
     description + "events[1] should be keypress");
  is(events[2].type, "beforeinput",
     description + "events[2] should be beforeinput");
  checkInputEvent(events[2], true, false, "insertText", "a", description);
  is(events[3].type, "input",
     description + "events[3] should be input");
  checkInputEvent(events[3], false, false, "insertText", "a", description);
  is(events[4].type, "keyup",
     description + "events[4] should be keyup");

  // Let's check if startComposition() throws an exception after ownership is stolen.
  input.value = "";
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition");
  try {
    TIP1.startComposition();
    ok(false,
       description + "TIP1.startComposition() should cause throwing an exception because TIP2 took the ownership");
    TIP1.cancelComposition();
  } catch (e) {
    ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"),
       description + "TIP1.startComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
  } finally {
    is(input.value, "",
       description + "The input element should not have commit string");
  }

  // Let's check if flushPendingComposition() throws an exception after ownership is stolen.
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition");
  input.value = "";
  try {
    TIP1.setPendingCompositionString(composingStr);
    TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
    TIP1.flushPendingComposition()
    ok(false,
       description + "TIP1.flushPendingComposition() should cause throwing an exception because TIP2 took the ownership");
    TIP1.cancelComposition();
  } catch (e) {
    ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"),
       description + "TIP1.flushPendingComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
  } finally {
    is(input.value, "",
       description + "The input element should not have commit string");
  }

  // Let's check if commitCompositionWith("bar") throws an exception after ownership is stolen.
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition");
  input.value = "";
  try {
    TIP1.commitCompositionWith("bar");
    ok(false,
       description + "TIP1.commitCompositionWith(\"bar\") should cause throwing an exception because TIP2 took the ownership");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"),
       description + "TIP1.commitCompositionWith(\"bar\") should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
  } finally {
    is(input.value, "",
       description + "The input element should not have commit string");
  }

  // Let's check if keydown() throws an exception after ownership is stolen.
  ok(TIP1.beginInputTransactionForTests(window),
     description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
  ok(TIP2.beginInputTransactionForTests(window),
     description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition");
  input.value = "";
  try {
    let 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 {
    let keyF = new KeyboardEvent("", { key: "f", code: "KeyF", keyCode: KeyboardEvent.DOM_VK_F });
    TIP1.keyup(keyF);
    ok(false,
       description + "TIP1.keyup(keyF) should cause throwing an exception because TIP2 took the ownership");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_NOT_INITIALIZED"),
       description + "TIP1.keyup(keyF) should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
  } finally {
    is(input.value, "",
       description + "The input element should not be modified");
  }

  // aCallback of nsITextInputProcessor.beginInputTransaction() must not be omitted.
  try {
    TIP1.beginInputTransaction(window);
    ok(false,
       description + "TIP1.beginInputTransaction(window) should be failed since aCallback is omitted");
  } catch (e) {
    ok(e.message.includes("Not enough arguments"),
       description + "TIP1.beginInputTransaction(window) should cause throwing an exception including \"Not enough arguments\" since aCallback is omitted");
  }

  // aCallback of nsITextInputProcessor.beginInputTransaction() must not be undefined.
  try {
    TIP1.beginInputTransaction(window, undefined);
    ok(false,
       description + "TIP1.beginInputTransaction(window, undefined) should be failed since aCallback is undefined");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
       description + "TIP1.beginInputTransaction(window, undefined) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is undefined");
  }

  // aCallback of nsITextInputProcessor.beginInputTransaction() must not be null.
  try {
    TIP1.beginInputTransaction(window, null);
    ok(false,
       description + "TIP1.beginInputTransaction(window, null) should be failed since aCallback is null");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
       description + "TIP1.beginInputTransaction(window, null) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is null");
  }
}

function runReleaseTests()
{
  var description = "runReleaseTests(): ";

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

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

  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  is(input.value, "foo",
     description + "the input should have composition string");

  // Release the TIP
  TIP = null;
  // Needs to run GC forcibly for testing this.
  Cu.forceGC();

  is(input.value, "",
     description + "the input should be empty because the composition should be canceled");

  TIP = createTIP();
  ok(TIP.beginInputTransactionForTests(window),
     description + "TIP.beginInputTransactionForTests() should succeed #2");
}

function runCompositionTests()
{
  var description = "runCompositionTests(): ";

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

  var events;

  function reset()
  {
    events = [];
  }

  function handler(aEvent)
  {
    events.push({ "type": aEvent.type, "data": aEvent.data });
  }

  window.addEventListener("compositionstart", handler);
  window.addEventListener("compositionupdate", handler);
  window.addEventListener("compositionend", handler);

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

  // nsITextInputProcessor.startComposition()
  reset();
  TIP.startComposition();
  is(events.length, 1,
     description + "startComposition() should cause only compositionstart");
  is(events[0].type, "compositionstart",
     description + "startComposition() should cause only compositionstart");
  is(input.value, "",
     description + "startComposition() shouldn't modify the focused editor");

  // Setting composition string "foo" as a raw clause
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 1,
     description + "flushPendingComposition() after startComposition() should cause compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "flushPendingComposition() after startComposition() should cause compositionupdate");
  is(events[0].data, "foo",
     description + "compositionupdate caused by flushPendingComposition() should have new composition string in its data");
  is(input.value, "foo",
     description + "modifying composition string should cause modifying the focused editor");

  // Changing the raw clause to a selected clause
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 0,
     description + "flushPendingComposition() changing only clause information shouldn't cause compositionupdate");
  is(input.value, "foo",
     description + "modifying composition clause shouldn't cause modifying the focused editor");

  // Separating the selected clause to two clauses
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE);
  TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE);
  TIP.setCaretInPendingComposition(2);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 0,
     description + "flushPendingComposition() separating a clause information shouldn't cause compositionupdate");
  is(input.value, "foo",
     description + "separating composition clause shouldn't cause modifying the focused editor");

  // Modifying the composition string
  TIP.setPendingCompositionString("FOo");
  TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE);
  TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE);
  TIP.setCaretInPendingComposition(2);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 1,
     description + "flushPendingComposition() causing modifying composition string should cause compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "flushPendingComposition() causing modifying composition string should cause compositionupdate");
  is(events[0].data, "FOo",
     description + "compositionupdate caused by flushPendingComposition() should have new composition string in its data");
  is(input.value, "FOo",
     description + "modifying composition clause shouldn't cause modifying the focused editor");

  // Committing the composition string
  reset();
  TIP.commitComposition();
  is(events.length, 1,
     description + "commitComposition() should cause compositionend but shouldn't cause compositionupdate");
  is(events[0].type, "compositionend",
     description + "commitComposition() should cause compositionend");
  is(events[0].data, "FOo",
     description + "compositionend caused by commitComposition() should have the committed string in its data");
  is(input.value, "FOo",
     description + "commitComposition() shouldn't cause modifying the focused editor");

  // Starting new composition without a call of startComposition()
  TIP.setPendingCompositionString("bar");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 2,
     description + "flushPendingComposition() without a call of startComposition() should cause both compositionstart and compositionupdate");
  is(events[0].type, "compositionstart",
     description + "flushPendingComposition() without a call of startComposition() should cause compositionstart");
  is(events[1].type, "compositionupdate",
     description + "flushPendingComposition() without a call of startComposition() should cause compositionupdate after compositionstart");
  is(events[1].data, "bar",
     description + "compositionupdate caused by flushPendingComposition() without a call of startComposition() should have the composition string in its data");
  is(input.value, "FOobar",
     description + "new composition string should cause appending composition string to the focused editor");

  // Canceling the composition
  reset();
  TIP.cancelComposition();
  is(events.length, 2,
     description + "cancelComposition() should cause both compositionupdate and compositionend");
  is(events[0].type, "compositionupdate",
     description + "cancelComposition() should cause compositionupdate");
  is(events[0].data, "",
     description + "compositionupdate caused by cancelComposition() should have empty string in its data");
  is(events[1].type, "compositionend",
     description + "cancelComposition() should cause compositionend after compositionupdate");
  is(events[1].data, "",
     description + "compositionend caused by cancelComposition() should have empty string in its data");
  is(input.value, "FOo",
     description + "canceled composition string should be removed from the focused editor");

  // Starting composition explicitly and canceling it
  reset();
  TIP.startComposition();
  TIP.cancelComposition();
  is(events.length, 2,
     description + "canceling composition immediately after startComposition() should cause compositionstart and compositionend");
  is(events[0].type, "compositionstart",
     description + "canceling composition immediately after startComposition() should cause compositionstart first");
  is(events[1].type, "compositionend",
     description + "canceling composition immediately after startComposition() should cause compositionend after compositionstart");
  is(events[1].data, "",
     description + "compositionend caused by canceling composition should have empty string in its data");
  is(input.value, "FOo",
     description + "canceling composition shouldn't modify the focused editor");

  // Create composition for next test.
  TIP.setPendingCompositionString("bar");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.flushPendingComposition();
  is(input.value, "FOobar",
     description + "The focused editor should have new composition string \"bar\"");

  // Allow to set empty composition string
  reset();
  TIP.flushPendingComposition();
  is(events.length, 1,
     description + "making composition string empty should cause only compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "making composition string empty should cause compositionupdate");
  is(events[0].data, "",
     description + "compositionupdate caused by making composition string empty should have empty string in its data");

  // Allow to insert new composition string without compositionend/compositionstart
  TIP.setPendingCompositionString("buzz");
  TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE);

  reset();
  TIP.flushPendingComposition();
  is(events.length, 1,
     description + "modifying composition string from empty string should cause only compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "modifying composition string from empty string should cause compositionupdate");
  is(events[0].data, "buzz",
     description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data");
  is(input.value, "FOobuzz",
     description + "new composition string should be appended to the focused editor");

  // Committing with different string
  reset();
  TIP.commitCompositionWith("bar");
  is(events.length, 2,
     description + "committing with different string should cause compositionupdate and compositionend");
  is(events[0].type, "compositionupdate",
     description + "committing with different string should cause compositionupdate first");
  is(events[0].data, "bar",
     description + "compositionupdate caused by committing with different string should have the committing string in its data");
  is(events[1].type, "compositionend",
     description + "committing with different string should cause compositionend after compositionupdate");
  is(events[1].data, "bar",
     description + "compositionend caused by committing with different string should have the committing string in its data");
  is(input.value, "FOobar",
     description + "new committed string should be appended to the focused editor");

  // Appending new composition string
  TIP.setPendingCompositionString("buzz");
  TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE);
  TIP.flushPendingComposition();
  is(input.value, "FOobarbuzz",
     description + "new composition string should be appended to the focused editor");

  // Committing with same string
  reset();
  TIP.commitCompositionWith("buzz");
  is(events.length, 1,
     description + "committing with same string should cause only compositionend");
  is(events[0].type, "compositionend",
     description + "committing with same string should cause compositionend");
  is(events[0].data, "buzz",
     description + "compositionend caused by committing with same string should have the committing string in its data");
  is(input.value, "FOobarbuzz",
     description + "new committed string should be appended to the focused editor");

  // Inserting commit string directly
  reset();
  TIP.commitCompositionWith("boo!");
  is(events.length, 3,
     description + "committing text directly should cause compositionstart, compositionupdate and compositionend");
  is(events[0].type, "compositionstart",
     description + "committing text directly should cause compositionstart first");
  is(events[1].type, "compositionupdate",
     description + "committing text directly should cause compositionupdate after compositionstart");
  is(events[1].data, "boo!",
     description + "compositionupdate caused by committing text directly should have the committing text in its data");
  is(events[2].type, "compositionend",
     description + "committing text directly should cause compositionend after compositionupdate");
  is(events[2].data, "boo!",
     description + "compositionend caused by committing text directly should have the committing text in its data");
  is(input.value, "FOobarbuzzboo!",
     description + "committing text directly should append the committing text to the focused editor");

  window.removeEventListener("compositionstart", handler);
  window.removeEventListener("compositionupdate", handler);
  window.removeEventListener("compositionend", handler);
}

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);
  window.addEventListener("compositionupdate", handler);
  window.addEventListener("compositionend", handler);
  window.addEventListener("keydown", handler);
  window.addEventListener("keypress", handler);
  window.addEventListener("keyup", handler);

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

  var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN });
  var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });
  var convertKeyEvent = new KeyboardEvent("", { key: "Convert", code: "Convert", keyCode: KeyboardEvent.DOM_VK_CONVERT });
  var backspaceKeyEvent = new KeyboardEvent("", { key: "Backspace", code: "Backspace", keyCode: KeyboardEvent.DOM_VK_BACK_SPACE });

  Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false);

  // nsITextInputProcessor.startComposition()
  reset();
  TIP.startComposition(printableKeyEvent);
  is(events.length, 2,
     description + "startComposition(printableKeyEvent) should cause keydown and compositionstart");
  is(events[0].type, "keydown",
     description + "startComposition(printableKeyEvent) should cause keydown");
  is(events[1].type, "compositionstart",
     description + "startComposition(printableKeyEvent) should cause compositionstart");
  is(input.value, "",
     description + "startComposition(printableKeyEvent) shouldn't modify the focused editor");

  // Setting composition string "foo" as a raw clause
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);

  reset();
  TIP.flushPendingComposition(printableKeyEvent);
  is(events.length, 1,
     description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate");
  is(events[0].data, "foo",
     description + "compositionupdate caused by flushPendingComposition(KeyupInternal) should have new composition string in its data");
  is(input.value, "foo",
     description + "modifying composition string should cause modifying the focused editor");

  // Changing the raw clause to a selected clause
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE);

  reset();
  TIP.flushPendingComposition(convertKeyEvent);
  is(events.length, 0,
     description + "flushPendingComposition(convertKeyEvent) changing only clause information shouldn't cause compositionupdate");
  is(input.value, "foo",
     description + "modifying composition clause shouldn't cause modifying the focused editor");

  // Separating the selected clause to two clauses
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE);
  TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE);
  TIP.setCaretInPendingComposition(2);

  reset();
  TIP.flushPendingComposition(convertKeyEvent);
  is(events.length, 0,
     description + "flushPendingComposition(convertKeyEvent) separating a clause information shouldn't cause compositionupdate");
  is(input.value, "foo",
     description + "separating composition clause shouldn't cause modifying the focused editor");

  // Modifying the composition string
  TIP.setPendingCompositionString("FOo");
  TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE);
  TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE);
  TIP.setCaretInPendingComposition(2);

  reset();
  TIP.flushPendingComposition(convertKeyEvent);
  is(events.length, 1,
     description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate");
  is(events[0].data, "FOo",
     description + "compositionupdate caused by flushPendingComposition(convertKeyEvent) should have new composition string in its data");
  is(input.value, "FOo",
     description + "modifying composition clause shouldn't cause modifying the focused editor");

  // Committing the composition string
  reset();
  TIP.commitComposition(enterKeyEvent);
  is(events.length, 2,
     description + "commitComposition(enterKeyEvent) should cause compositionend and keyup but shoudn't cause compositionupdate");
  is(events[0].type, "compositionend",
     description + "commitComposition(enterKeyEvent) should cause compositionend");
  is(events[0].data, "FOo",
     description + "compositionend caused by commitComposition(enterKeyEvent) should have the committed string in its data");
  is(events[1].type, "keyup",
     description + "commitComposition(enterKeyEvent) should cause keyup");
  is(input.value, "FOo",
     description + "commitComposition(enterKeyEvent) shouldn't cause modifying the focused editor");

  // Starting new composition without a call of startComposition()
  TIP.setPendingCompositionString("bar");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);

  reset();
  TIP.flushPendingComposition(printableKeyEvent);
  is(events.length, 3,
     description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause both compositionstart and compositionupdate");
  is(events[0].type, "keydown",
     description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause keydown");
  is(events[1].type, "compositionstart",
     description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionstart");
  is(events[2].type, "compositionupdate",
     description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionupdate after compositionstart");
  is(events[2].data, "bar",
     description + "compositionupdate caused by flushPendingComposition(printableKeyEvent) without a call of startComposition() should have the composition string in its data");
  is(input.value, "FOobar",
     description + "new composition string should cause appending composition string to the focused editor");

  // Canceling the composition
  reset();
  TIP.cancelComposition(escKeyEvent);
  is(events.length, 3,
     description + "cancelComposition(escKeyEvent) should cause both compositionupdate and compositionend");
  is(events[0].type, "compositionupdate",
     description + "cancelComposition(escKeyEvent) should cause compositionupdate");
  is(events[0].data, "",
     description + "compositionupdate caused by cancelComposition(escKeyEvent) should have empty string in its data");
  is(events[1].type, "compositionend",
     description + "cancelComposition(escKeyEvent) should cause compositionend after compositionupdate");
  is(events[1].data, "",
     description + "compositionend caused by cancelComposition(escKeyEvent) should have empty string in its data");
  is(events[2].type, "keyup",
     description + "cancelComposition(escKeyEvent) should cause keyup after compositionend");
  is(input.value, "FOo",
     description + "canceled composition string should be removed from the focused editor");

  // Starting composition explicitly and canceling it
  reset();
  TIP.startComposition(printableKeyEvent);
  TIP.cancelComposition(escKeyEvent);
  is(events.length, 4,
     description + "canceling composition immediately after startComposition() should cause keydown, compositionstart, compositionend and keyup");
  is(events[0].type, "keydown",
     description + "canceling composition immediately after startComposition() should cause keydown first");
  is(events[1].type, "compositionstart",
     description + "canceling composition immediately after startComposition() should cause compositionstart after keydown");
  is(events[2].type, "compositionend",
     description + "canceling composition immediately after startComposition() should cause compositionend after compositionstart");
  is(events[2].data, "",
     description + "compositionend caused by canceling composition should have empty string in its data");
  is(events[3].type, "keyup",
     description + "canceling composition immediately after startComposition() should cause keyup after compositionend");
  is(input.value, "FOo",
     description + "canceling composition shouldn't modify the focused editor");

  // Create composition for next test.
  TIP.setPendingCompositionString("bar");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.flushPendingComposition();
  is(input.value, "FOobar",
     description + "The focused editor should have new composition string \"bar\"");

  // Allow to set empty composition string
  reset();
  TIP.flushPendingComposition(backspaceKeyEvent);
  is(events.length, 1,
     description + "making composition string empty should cause only compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "making composition string empty should cause compositionupdate");
  is(events[0].data, "",
     description + "compositionupdate caused by making composition string empty should have empty string in its data");

  // Allow to insert new composition string without compositionend/compositionstart
  TIP.setPendingCompositionString("buzz");
  TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE);

  reset();
  TIP.flushPendingComposition(printableKeyEvent);
  is(events.length, 1,
     description + "modifying composition string from empty string should cause only compositionupdate");
  is(events[0].type, "compositionupdate",
     description + "modifying composition string from empty string should cause compositionupdate");
  is(events[0].data, "buzz",
     description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data");
  is(input.value, "FOobuzz",
     description + "new composition string should be appended to the focused editor");

  // Committing with different string
  reset();
  TIP.commitCompositionWith("bar", printableKeyEvent);
  is(events.length, 3,
     description + "committing with different string should cause compositionupdate and compositionend");
  is(events[0].type, "compositionupdate",
     description + "committing with different string should cause compositionupdate first");
  is(events[0].data, "bar",
     description + "compositionupdate caused by committing with different string should have the committing string in its data");
  is(events[1].type, "compositionend",
     description + "committing with different string should cause compositionend after compositionupdate");
  is(events[1].data, "bar",
     description + "compositionend caused by committing with different string should have the committing string in its data");
  is(events[2].type, "keyup",
     description + "committing with different string should cause keyup after compositionend");
  is(input.value, "FOobar",
     description + "new committed string should be appended to the focused editor");

  // Appending new composition string
  TIP.setPendingCompositionString("buzz");
  TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE);
  TIP.flushPendingComposition();
  is(input.value, "FOobarbuzz",
     description + "new composition string should be appended to the focused editor");

  // Committing with same string
  reset();
  TIP.commitCompositionWith("buzz", enterKeyEvent);
  is(events.length, 2,
     description + "committing with same string should cause only compositionend");
  is(events[0].type, "compositionend",
     description + "committing with same string should cause compositionend");
  is(events[0].data, "buzz",
     description + "compositionend caused by committing with same string should have the committing string in its data");
  is(events[1].type, "keyup",
     description + "committing with same string should cause keyup after compositionend");
  is(input.value, "FOobarbuzz",
     description + "new committed string should be appended to the focused editor");

  // Inserting commit string directly
  reset();
  TIP.commitCompositionWith("boo!", printableKeyEvent);
  is(events.length, 5,
     description + "committing text directly should cause compositionstart, compositionupdate and compositionend");
  is(events[0].type, "keydown",
     description + "committing text directly should cause keydown first");
  is(events[1].type, "compositionstart",
     description + "committing text directly should cause compositionstart after keydown");
  is(events[2].type, "compositionupdate",
     description + "committing text directly should cause compositionupdate after compositionstart");
  is(events[2].data, "boo!",
     description + "compositionupdate caused by committing text directly should have the committing text in its data");
  is(events[3].type, "compositionend",
     description + "committing text directly should cause compositionend after compositionupdate");
  is(events[3].data, "boo!",
     description + "compositionend caused by committing text directly should have the committing text in its data");
  is(events[4].type, "keyup",
     description + "committing text directly should cause keyup after compositionend");
  is(input.value, "FOobarbuzzboo!",
     description + "committing text directly should append the committing text to the focused editor");

  Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true);

  // Even if "dom.keyboardevent.dispatch_during_composition" is true, keypress event shouldn't be fired during composition
  reset();
  TIP.startComposition(printableKeyEvent);
  is(events.length, 3,
     description + "TIP.startComposition(printableKeyEvent) should cause keydown, compositionstart and keyup (keypress event shouldn't be fired during composition)");
  is(events[0].type, "keydown",
     description + "TIP.startComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)");
  is(events[1].type, "compositionstart",
     description + "TIP.startComposition(printableKeyEvent) should cause compositionstart (keypress event shouldn't be fired during composition)");
  is(events[2].type, "keyup",
     description + "TIP.startComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)");

  // TIP.flushPendingComposition(printableKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);

  reset();
  TIP.flushPendingComposition(printableKeyEvent);
  is(events.length, 3,
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown, compositionupdate and keyup (keypress event shouldn't be fired during composition)");
  is(events[0].type, "keydown",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)");
  is(events[1].type, "compositionupdate",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate (keypress event shouldn't be fired during composition)");
  is(events[2].type, "keyup",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)");

  // TIP.commitComposition(enterKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true
  reset();
  TIP.commitComposition(enterKeyEvent);
  is(events.length, 3,
     description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)");
  is(events[0].type, "keydown",
     description + "TIP.commitComposition(enterKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)");
  is(events[1].type, "compositionend",
     description + "TIP.commitComposition(enterKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)");
  is(events[2].type, "keyup",
     description + "TIP.commitComposition(enterKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)");

  // TIP.cancelComposition(escKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true
  TIP.startComposition();
  reset();
  TIP.cancelComposition(escKeyEvent);
  is(events.length, 3,
     description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)");
  is(events[0].type, "keydown",
     description + "TIP.cancelComposition(escKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)");
  is(events[1].type, "compositionend",
     description + "TIP.cancelComposition(escKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)");
  is(events[2].type, "keyup",
     description + "TIP.cancelComposition(escKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)");

  var printableKeydownEvent = new KeyboardEvent("keydown", { key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B });
  var enterKeydownEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN });
  var escKeydownEvent = new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });

  // TIP.startComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true
  reset();
  TIP.startComposition(printableKeydownEvent);
  is(events.length, 2,
     description + "TIP.startComposition(printableKeydownEvent) should cause keydown and compositionstart (keyup event shouldn't be fired)");
  is(events[0].type, "keydown",
     description + "TIP.startComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)");
  is(events[1].type, "compositionstart",
     description + "TIP.startComposition(printableKeydownEvent) should cause compositionstart (keyup event shouldn't be fired)");

  // TIP.flushPendingComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);

  reset();
  TIP.flushPendingComposition(printableKeydownEvent);
  is(events.length, 2,
     description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown and compositionupdate (keyup event shouldn't be fired)");
  is(events[0].type, "keydown",
     description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)");
  is(events[1].type, "compositionupdate",
     description + "TIP.flushPendingComposition(printableKeydownEvent) should cause compositionupdate (keyup event shouldn't be fired)");

  // TIP.commitComposition(enterKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true
  reset();
  TIP.commitComposition(enterKeydownEvent);
  is(events.length, 2,
     description + "TIP.commitComposition(enterKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)");
  is(events[0].type, "keydown",
     description + "TIP.commitComposition(enterKeydownEvent) should cause keydown (keyup event shouldn't be fired)");
  is(events[1].type, "compositionend",
     description + "TIP.commitComposition(enterKeydownEvent) should cause compositionend (keyup event shouldn't be fired)");

  // TIP.cancelComposition(escKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true
  TIP.startComposition();
  reset();
  TIP.cancelComposition(escKeydownEvent);
  is(events.length, 2,
     description + "TIP.cancelComposition(escKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)");
  is(events[0].type, "keydown",
     description + "TIP.cancelComposition(escKeydownEvent) should cause keydown (keyup event shouldn't be fired)");
  is(events[1].type, "compositionend",
     description + "TIP.cancelComposition(escKeydownEvent) should cause compositionend (keyup event shouldn't be fired)");

  Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition");

  window.removeEventListener("compositionstart", handler);
  window.removeEventListener("compositionupdate", handler);
  window.removeEventListener("compositionend", handler);
  window.removeEventListener("keydown", handler);
  window.removeEventListener("keypress", handler);
  window.removeEventListener("keyup", handler);
}

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);
  window.addEventListener("compositionupdate", handler);
  window.addEventListener("compositionend", handler);
  window.addEventListener("keydown", handler);
  window.addEventListener("keypress", handler);
  window.addEventListener("keyup", handler);

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

  var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
  var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN });
  var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });

  Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false);

  // If keydown before compositionstart is consumed, composition shouldn't be started.
  reset();
  ok(!TIP.startComposition(printableKeyEvent),
     description + "TIP.startComposition(printableKeyEvent) should return false because it's keydown is consumed");
  is(events.length, 2,
     description + "TIP.startComposition(printableKeyEvent) should cause only keydown and keyup events");
  is(events[0].type, "keydown",
     description + "TIP.startComposition(printableKeyEvent) should cause keydown event first");
  is(events[1].type, "keyup",
     description + "TIP.startComposition(printableKeyEvent) should cause keyup event after keydown");
  ok(!TIP.hasComposition,
     description + "TIP.startComposition(printableKeyEvent) shouldn't cause composition");
  is(input.value, "",
     description + "TIP.startComposition(printableKeyEvent) shouldn't cause inserting text");

  // If keydown before compositionstart caused by flushPendingComposition(printableKeyEvent) is consumed, composition shouldn't be started.
  reset();
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  ok(!TIP.flushPendingComposition(printableKeyEvent),
     description + "TIP.flushPendingComposition(printableKeyEvent) should return false because it's keydown is consumed");
  is(events.length, 2,
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events");
  is(events[0].type, "keydown",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first");
  is(events[1].type, "keyup",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after keydown");
  ok(!TIP.hasComposition,
     description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause composition");
  is(input.value, "",
     description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause inserting text");

  // If keydown before compositionstart is consumed, composition shouldn't be started.
  reset();
  ok(!TIP.commitCompositionWith("foo", printableKeyEvent),
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should return false because it's keydown is consumed");
  is(events.length, 2,
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause only keydown and keyup events");
  is(events[0].type, "keydown",
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause keydown event first");
  is(events[1].type, "keyup",
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) should cause keyup event after keydown");
  ok(!TIP.hasComposition,
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) shouldn't cause composition");
  is(input.value, "",
     description + "TIP.commitCompositionWith(\"foo\", printableKeyEvent) shouldn't cause inserting text");

  Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true);

  // If composition is already started, TIP.flushPendingComposition(printableKeyEvent) shouldn't be canceled.
  TIP.startComposition();
  ok(TIP.hasComposition,
     description + "Before TIP.flushPendingComposition(printableKeyEvent), composition should've been created");
  reset();
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  ok(TIP.flushPendingComposition(printableKeyEvent),
     description + "TIP.flushPendingComposition(printableKeyEvent) should return true even if preceding keydown is consumed because there was a composition already");
  is(events.length, 3,
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events");
  is(events[0].type, "keydown",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first");
  is(events[1].type, "compositionupdate",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate event after keydown");
  is(events[2].type, "keyup",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after compositionupdate");
  ok(TIP.hasComposition,
     description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause canceling composition");
  is(input.value, "foo",
     description + "TIP.flushPendingComposition(printableKeyEvent) should cause inserting text even if preceding keydown is consumed because there was a composition already");

  // If composition is already started, TIP.commitComposition(enterKeyEvent) shouldn't be canceled.
  reset();
  TIP.commitComposition(enterKeyEvent);
  is(events.length, 3,
     description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup events");
  is(events[0].type, "keydown",
     description + "TIP.commitComposition(enterKeyEvent) should cause keydown event first");
  is(events[1].type, "compositionend",
     description + "TIP.commitComposition(enterKeyEvent) should cause compositionend event after keydown");
  is(events[2].type, "keyup",
     description + "TIP.commitComposition(enterKeyEvent) should cause keyup event after compositionend");
  ok(!TIP.hasComposition,
     description + "TIP.commitComposition(enterKeyEvent) should cause committing composition even if preceding keydown is consumed because there was a composition already");
  is(input.value, "foo",
     description + "TIP.commitComposition(enterKeyEvent) should commit composition even if preceding keydown is consumed because there was a composition already");

  // cancelComposition() should work even if preceding keydown event is consumed.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  ok(TIP.hasComposition,
     description + "Before TIP.cancelComposition(escKeyEvent), composition should've been created");
  is(input.value, "foo",
     description + "Before TIP.cancelComposition(escKeyEvent) should have composition string");
  reset();
  TIP.cancelComposition(escKeyEvent);
  is(events.length, 4,
     description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionupdate, compositionend and keyup events even if preceding keydown is consumed because there was a composition already");
  is(events[0].type, "keydown",
     description + "TIP.cancelComposition(escKeyEvent) should cause keydown event first");
  is(events[1].type, "compositionupdate",
     description + "TIP.cancelComposition(escKeyEvent) should cause compositionupdate event after keydown");
  is(events[2].type, "compositionend",
     description + "TIP.cancelComposition(escKeyEvent) should cause compositionend event after compositionupdate");
  is(events[3].type, "keyup",
     description + "TIP.cancelComposition(escKeyEvent) should cause keyup event after compositionend");
  ok(!TIP.hasComposition,
     description + "TIP.cancelComposition(escKeyEvent) should cause canceling composition even if preceding keydown is consumed because there was a composition already");
  is(input.value, "",
     description + "TIP.cancelComposition(escKeyEvent) should cancel composition even if preceding keydown is consumed because there was a composition already");

  Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition");

  window.removeEventListener("compositionstart", handler);
  window.removeEventListener("compositionupdate", handler);
  window.removeEventListener("compositionend", handler);
  window.removeEventListener("keydown", handler);
  window.removeEventListener("keypress", handler);
  window.removeEventListener("keyup", handler);
}

async 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);
  window.addEventListener("keypress", handler);
  window.addEventListener("keyup", handler);

  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\"");

  // Emulates pressing and releasing a key which introduces a surrogate pair.
  async function test_press_and_release_surrogate_pair_key(
    aTestPerSurrogateKeyPress,
    aTestIllFormedUTF16KeyValue = false
  ) {
    await SpecialPowers.pushPrefEnv({
      set: [
        ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
        ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
      ],
    });

    const settingDescription =
      `aTestPerSurrogateKeyPress=${aTestPerSurrogateKeyPress}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
    const allowIllFormedUTF16 = aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
    const keySurrogatePair = new KeyboardEvent("", { key: "\uD842\uDFB7", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });

    input.value = "";
    reset();
    doDefaultKeydown = TIP.keydown(keySurrogatePair);
    doDefaultKeyup = TIP.keyup(keySurrogatePair);

    is(
      doDefaultKeydown,
      TIP.KEYPRESS_IS_CONSUMED,
      `${
        description
      }TIP.keydown(keySurrogatePair), ${
        settingDescription
      }, should return 0x02 because the keypress event should be consumed by the input element`
    );
    is(
      doDefaultKeyup,
      true,
      `${description}TIP.keyup(keySurrogatePair) should return true`
    );
    is(
      events.length,
      aTestPerSurrogateKeyPress ? 4 : 3,
      `${description}TIP.keydown(keySurrogatePair), ${
        settingDescription
      }, should cause keydown, keypress${
        aTestPerSurrogateKeyPress ? ", keypress" : ""
      } and keyup event`
    );
    checkKeyAttrs(
      `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
      events[0],
      {
        type: "keydown",
        key: "\uD842\uDFB7",
        code: "KeyA",
        keyCode: KeyboardEvent.DOM_VK_A,
        charCode: 0,
      }
    );
    if (aTestPerSurrogateKeyPress) {
      checkKeyAttrs(
        `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
        events[1],
        {
          type: "keypress",
          key: allowIllFormedUTF16
            ? "\uD842"
            : "\uD842\uDFB7", // First keypress should have the surrogate pair
          code: "KeyA",
          keyCode: 0,
          charCode: "\uD842\uDFB7".charCodeAt(0),
          defaultPrevented: true,
        }
      );
      checkKeyAttrs(
        `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
        events[2],
        {
          type: "keypress",
          key: allowIllFormedUTF16
            ? "\uDFB7"
            : "", // But the following keypress should have empty string, instead
          code: "KeyA",
          keyCode: 0,
          charCode: "\uD842\uDFB7".charCodeAt(1),
          defaultPrevented: true,
        }
      );
    } else {
      checkKeyAttrs(
        `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`,
        events[1],
        {
          type: "keypress",
          key: "\uD842\uDFB7",
          code: "KeyA",
          keyCode: 0,
          charCode: 0x20BB7,
          defaultPrevented: true,
        }
      );
    }
    checkKeyAttrs(
      `${description}TIP.keyup(keySurrogatePair), ${settingDescription}`,
      events[aTestPerSurrogateKeyPress ? 3 : 2],
      {
        type: "keyup",
        key: "\uD842\uDFB7",
        code: "KeyA",
        keyCode: KeyboardEvent.DOM_VK_A,
        charCode: 0,
      }
    );
    is(
      input.value,
      "\uD842\uDFB7",
      `${description}${settingDescription}, input.value should be the surrogate pair`
    );
  };

  await test_press_and_release_surrogate_pair_key(true, true);
  await test_press_and_release_surrogate_pair_key(true, false);
  await test_press_and_release_surrogate_pair_key(false);

  // If KEY_FORCE_PRINTABLE_KEY is specified, registered key names can be a printable key which inputs the specified value.
  input.value = "";
  var keyEnterPrintable = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN });

  reset();
  doDefaultKeydown = TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY);
  doDefaultKeyup   = TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY);

  is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED,
     description + "TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should return 0x02 because the keypress events should be consumed by the input element");
  ok(doDefaultKeyup,
     description + "TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should return true");
  is(events.length, 7,
     description + "TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) should cause keydown, keypress, keypress, keypress, keypress, keypress and keyup event");
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[0],
                { type: "keydown",  key: "Enter",           code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0,                    defaultPrevented: false });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[1],
                { type: "keypress", key: "Enter".charAt(0), code: "Enter", keyCode: 0,                           charCode: "Enter".charCodeAt(0), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[2],
                { type: "keypress", key: "Enter".charAt(1), code: "Enter", keyCode: 0,                           charCode: "Enter".charCodeAt(1), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[3],
                { type: "keypress", key: "Enter".charAt(2), code: "Enter", keyCode: 0,                           charCode: "Enter".charCodeAt(2), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[4],
                { type: "keypress", key: "Enter".charAt(3), code: "Enter", keyCode: 0,                           charCode: "Enter".charCodeAt(3), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[5],
                { type: "keypress", key: "Enter".charAt(4), code: "Enter", keyCode: 0,                           charCode: "Enter".charCodeAt(4), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY) and TIP.keyup(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)", events[6],
                { type: "keyup",    key: "Enter",           code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0,                     defaultPrevented: false });
  is(input.value, "Enter",
     description + "input.value should be \"Enter\"");

  // modifiers should be ignored.
  var keyWithModifiers = new KeyboardEvent("", { key: "Escape", code: "Escape", shiftKey: true, ctrlKey: true, altKey: true, metaKey: true });

  reset();
  doDefaultKeydown = TIP.keydown(keyWithModifiers);
  doDefaultKeyup   = TIP.keyup(keyWithModifiers);

  is(doDefaultKeydown, 0,
     description + "TIP.keydown(keyWithModifiers) should return 0");
  ok(doDefaultKeyup,
     description + "TIP.keyup(keyWithModifiers) should return true");
  is(events.length, 3,
     description + "TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers) should cause keydown, keypress and keyup event");
  checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[0],
                { type: "keydown",  key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });
  checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[1],
                { type: "keypress", key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });
  checkKeyAttrs("TIP.keydown(keyWithModifiers) and TIP.keyup(keyWithModifiers)", events[2],
                { type: "keyup",    key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE });
  is(input.value, "Enter",
     description + "input.value should stay \"Enter\" which was inputted by TIP.keydown(keyEnterPrintable, TIP.KEY_FORCE_PRINTABLE_KEY)");

  // Call preventDefault() at keydown
  input.value = "";
  reset();
  doPreventDefaults = [ "keydown" ];
  doDefaultKeydown = TIP.keydown(keyA);
  doDefaultKeyup   = TIP.keyup(keyA);

  is(doDefaultKeydown, TIP.KEYDOWN_IS_CONSUMED,
     description + "TIP.keydown(keyA) should return 0x01 because keydown event's preventDefault should be called");
  ok(doDefaultKeyup,
     description + "TIP.keyup(keyA) should return true");
  is(events.length, 2,
     description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown should cause keydown and keyup event");
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown", events[0],
                { type: "keydown",  key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keydown", events[1],
                { type: "keyup",    key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, defaultPrevented: false });
  is(input.value, "",
     description + "input.value shouldn't be modified by TIP.keyup(keyA) if the keydown event is consumed");

  // Call preventDefault() at keypress
  reset();
  doPreventDefaults = [ "keypress" ];
  doDefaultKeydown = TIP.keydown(keyA);
  doDefaultKeyup   = TIP.keyup(keyA);

  is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED,
     description + "TIP.keydown(keyA) should return 0x02 because keypress event's preventDefault should be called");
  ok(doDefaultKeyup,
     description + "TIP.keyup(keyA) should return true");
  is(events.length, 3,
     description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress should cause keydown, keypress and keyup event");
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[0],
                { type: "keydown",  key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0,                 defaultPrevented: false });
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[1],
                { type: "keypress", key: "a", code: "KeyA", keyCode: 0,                      charCode: "a".charCodeAt(0), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keypress", events[2],
                { type: "keyup",    key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0,                 defaultPrevented: false });
  is(input.value, "",
     description + "input.value shouldn't be modified by TIP.keyup(keyA) if the keypress event is consumed");

  // Call preventDefault() at keyup
  input.value = "";
  reset();
  doPreventDefaults = [ "keyup" ];
  doDefaultKeydown = TIP.keydown(keyA);
  doDefaultKeyup   = TIP.keyup(keyA);

  is(doDefaultKeydown, TIP.KEYPRESS_IS_CONSUMED,
     description + "TIP.keydown(keyA) should return 0x02 because the key event should be consumed by the input element");
  ok(!doDefaultKeyup,
     description + "TIP.keyup(keyA) should return false because keyup event's preventDefault should be called");
  is(events.length, 3,
     description + "TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup should cause keydown, keypress and keyup event");
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[0],
                { type: "keydown",  key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0,                 defaultPrevented: false });
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[1],
                { type: "keypress", key: "a", code: "KeyA", keyCode: 0,                      charCode: "a".charCodeAt(0), defaultPrevented: true });
  checkKeyAttrs("TIP.keydown(keyA) and TIP.keyup(keyA) with preventing default of keyup", events[2],
                { type: "keyup",    key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0,                 defaultPrevented: true });
  is(input.value, "a",
     description + "input.value should be \"a\" by TIP.keyup(keyA) even if the keyup event is consumed");

  // key events during composition
  try {
    Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", false);

    ok(TIP.startComposition(), "TIP.startComposition() should start composition");

    input.value = "";
    reset();
    TIP.keydown(keyA);
    is(events.length, 0,
       description + "TIP.keydown(keyA) shouldn't cause key events during composition if it's disabled by the pref");
    reset();
    TIP.keyup(keyA);
    is(events.length, 0,
       description + "TIP.keyup(keyA) shouldn't cause key events during composition if it's disabled by the pref");

    Services.prefs.setBoolPref("dom.keyboardevent.dispatch_during_composition", true);
    reset();
    TIP.keydown(keyA);
    is(events.length, 1,
       description + "TIP.keydown(keyA) should cause keydown event even composition if it's enabled by the pref");
    checkKeyAttrs("TIP.keydown(keyA) during composition", events[0],
                  { type: "keydown",  key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, isComposing: true });
    reset();
    TIP.keyup(keyA);
    is(events.length, 1,
       description + "TIP.keyup(keyA) should cause keyup event even composition if it's enabled by the pref");
    checkKeyAttrs("TIP.keyup(keyA) during composition", events[0],
                  { type: "keyup",    key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A, charCode: 0, isComposing: true });

  } finally {
    TIP.cancelComposition();
    Services.prefs.clearUserPref("dom.keyboardevent.dispatch_during_composition");
  }

  // Test .location computation
  const kCodeToLocation = [
    { code: "BracketLeft",              location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "BracketRight",             location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Comma",                    location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit0",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit1",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit2",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit3",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit4",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit5",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit6",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit7",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit8",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Digit9",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Equal",                    location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Minus",                    location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Period",                   location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "Slash",                    location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "AltLeft",                  location: KeyboardEvent.DOM_KEY_LOCATION_LEFT },
    { code: "AltRight",                 location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT },
    { code: "CapsLock",                 location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "ContextMenu",              location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "ControlLeft",              location: KeyboardEvent.DOM_KEY_LOCATION_LEFT },
    { code: "ControlRight",             location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT },
    { code: "Enter",                    location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD },
    { code: "MetaLeft",                 location: KeyboardEvent.DOM_KEY_LOCATION_LEFT },
    { code: "MetaRight",                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 (let i = 0; i < kCodeToLocation.length; i++) {
    let 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);
    let longDesc = description + "testing computation of .location of \"" + kCodeToLocation[i].code + "\", ";
    is(events.length, 3,
       longDesc + "keydown, keypress and keyup events should be fired");
    for (let 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);
    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 (let 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.
    let 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 (let 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: "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: kIsMac
                                          ? KeyboardEvent.DOM_VK_META
                                          : KeyboardEvent.DOM_VK_WIN,                     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 (let i = 0; i < kKeyToKeyCode.length; i++) {
    let 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);
    let 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 (let 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);
    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 (let 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.
    let 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 (let 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: "MetaLeft",     isLockable: false },
    { key: "Meta",       code: "MetaRight",    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 },
  ];

  function checkModifiers(aTestDesc, aEvent, aType, aKey, aCode, aModifiers)
  {
    var desc = description + aTestDesc + ", type=\"" + aEvent.type + "\", key=\"" + aEvent.key + "\", code=\"" + aEvent.code + "\"";
    is(aEvent.type, aType,
       desc + ", .type value is wrong");
    if (aEvent.type != aType) {
      return;
    }
    is(aEvent.key, aKey,
       desc + ", .key value is wrong");
    is(aEvent.code, aCode,
       desc + ", .code value is wrong");
    is(aEvent.altKey, aModifiers.includes("Alt"),
       desc + ", .altKey value is wrong");
    is(aEvent.ctrlKey, aModifiers.includes("Control"),
       desc + ", .ctrlKey value is wrong");
    is(aEvent.metaKey, aModifiers.includes("Meta"),
       desc + ", .metaKey value is wrong");
    is(aEvent.shiftKey, aModifiers.includes("Shift"),
       desc + ", .shiftKey value is wrong");
    /* eslint-disable-next-line no-shadow */
    for (var i = 0; i < kModifiers.length; i++) {
      is(aEvent.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]),
         desc + ", .getModifierState(\"" + kModifiers[i] + "\") returns wrong value");
    }
  }

  function checkAllTIPModifiers(aTestDesc, aModifiers)
  {
    /* eslint-disable-next-line no-shadow */
    for (var i = 0; i < kModifiers.length; i++) {
      is(TIP.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]),
         aTestDesc + ", TIP.getModifierState(\"" + kModifiers[i] + "\") returns wrong value");
      is(sharedTIP.getModifierState(kModifiers[i]), TIP.getModifierState(kModifiers[i]),
         aTestDesc + ", sharedTIP.getModifierState(\"" + kModifiers[i] + "\") returns different value from TIP");
      is(independentTIP.getModifierState(kModifiers[i]), false,
         aTestDesc + ", independentTIP.getModifierState(\"" + kModifiers[i] + "\") should return false");
    }
  }

  // First, all modifiers must be false.
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(keyA);
  TIP.keyup(keyA);

  is(events.length, 3,
     description + "TIP.keydown(keyA) and TIP.keyup(keyA) should cause keydown, keypress and keyup");
  checkModifiers("Before dispatching modifier key events", events[0], "keydown",  "a", "KeyA", []);
  checkModifiers("Before dispatching modifier key events", events[1], "keypress", "a", "KeyA", []);
  checkModifiers("Before dispatching modifier key events", events[2], "keyup",    "a", "KeyA", []);

  // Test each modifier keydown/keyup causes activating/inactivating the modifier state.
  for (var i = 0; i < kModifierKeys.length; i++) {
    reset();
    doPreventDefaults = [ "keypress" ];
    var modKey = new KeyboardEvent("", { key: kModifierKeys[i].key, code: kModifierKeys[i].code });
    let 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: "" });

  let testDesc = "ShiftLeft press -> ShiftRight press -> ShiftRight release -> ShiftLeft release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Right-Shift keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Right-Shift keyup)", [ "Shift" ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5], "keyup",    "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[9], "keyup",    "Shift", "ShiftLeft",  [ ]);

  testDesc = "ShiftLeft press -> ShiftRight press -> ShiftLeft release -> ShiftRight release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup)", [ "Shift" ]);
  TIP.keyup(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5], "keyup",    "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[9], "keyup",    "Shift", "ShiftRight",  [ ]);

  testDesc = "ShiftLeft press -> virtual Shift press -> virtual Shift release -> ShiftLeft release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "Shift" ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "Shift", "",           [ "Shift" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5], "keyup",    "Shift", "",           [ "Shift" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[9], "keyup",    "Shift", "ShiftLeft",  [ ]);

  testDesc = "virtual Shift press -> ShiftRight press -> ShiftRight release -> virtual Shift release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Right-Shift keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Right-Shift keyup)", [ "Shift" ]);
  TIP.keyup(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift", "",           [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5], "keyup",    "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[9], "keyup",    "Shift", "",           [ ]);

  testDesc = "ShiftLeft press -> ShiftRight press -> ShiftRight release -> ShiftRight release -> ShiftLeft release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keydown", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "Shift" ]);
  TIP.keyup(shiftRight);
  checkAllTIPModifiers(testDesc + ", Right-Shift keyup again", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup again)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup again)", [ "Shift" ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]);

  is(events.length, 14,
     description + testDesc + " should cause 14 events");
  checkModifiers(testDesc, events[0],  "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[1],  "keydown",  "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[2],  "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3],  "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4],  "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5],  "keyup",    "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[6],  "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[7],  "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[8],  "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[9],  "keyup",    "Shift", "ShiftRight", [ "Shift" ]);
  checkModifiers(testDesc, events[10], "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[11], "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[12], "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[13], "keyup",    "Shift", "ShiftLeft",  [ ]);

  testDesc = "ShiftLeft press -> ShiftLeft press -> ShiftLeft release -> ShiftLeft release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown", [ "Shift" ]);
  TIP.keydown(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keydown again", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift" ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup", [ ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup)", [ ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup)", [ ]);
  TIP.keyup(shiftLeft);
  checkAllTIPModifiers(testDesc + ", Left-Shift keyup again", [ ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Left-Shift keyup again)", [ ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Left-Shift keyup again)", [ ]);

  is(events.length, 13,
     description + testDesc + " should cause 13 events");
  checkModifiers(testDesc, events[0],  "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[1],  "keydown",  "Shift", "ShiftLeft",  [ "Shift" ]);
  checkModifiers(testDesc, events[2],  "keydown",  "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[3],  "keypress", "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[4],  "keyup",    "a",     "KeyA",       [ "Shift" ]);
  checkModifiers(testDesc, events[5],  "keyup",    "Shift", "ShiftLeft",  [ ]);
  checkModifiers(testDesc, events[6],  "keydown",  "a",     "KeyA",       [ ]);
  checkModifiers(testDesc, events[7],  "keypress", "a",     "KeyA",       [ ]);
  checkModifiers(testDesc, events[8],  "keyup",    "a",     "KeyA",       [ ]);
  checkModifiers(testDesc, events[9],  "keyup",    "Shift", "ShiftLeft",  [ ]);
  checkModifiers(testDesc, events[10], "keydown",  "a",     "KeyA",       [ ]);
  checkModifiers(testDesc, events[11], "keypress", "a",     "KeyA",       [ ]);
  checkModifiers(testDesc, events[12], "keyup",    "a",     "KeyA",       [ ]);

  testDesc = "virtual Shift press -> virtual AltGraph press -> virtual AltGraph release -> virtual Shift release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]);
  TIP.keydown(altGrVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keydown", [ "Shift", "AltGraph" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift", "AltGraph" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift", "AltGraph" ]);
  TIP.keyup(altGrVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keyup", [ "Shift" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-AltGraph keyup)", [ "Shift" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-AltGraph keyup)", [ "Shift" ]);
  TIP.keyup(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift",    "",     [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "AltGraph", "",     [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[5], "keyup",    "AltGraph", "",     [ "Shift" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",        "KeyA", [ "Shift" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",        "KeyA", [ "Shift" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",        "KeyA", [ "Shift" ]);
  checkModifiers(testDesc, events[9], "keyup",    "Shift",    "",     [ ]);

  testDesc = "virtual Shift press -> virtual AltGraph press -> virtual Shift release -> virtual AltGr release";
  reset();
  doPreventDefaults = [ "keypress" ];
  TIP.keydown(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keydown", [ "Shift" ]);
  TIP.keydown(altGrVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keydown", [ "Shift", "AltGraph" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown", [ "Shift", "AltGraph" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup", [ "Shift", "AltGraph" ]);
  TIP.keyup(shiftVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-Shift keyup", [ "AltGraph" ]);
  TIP.keydown(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keydown (after Virtual-Shift keyup)", [ "AltGraph" ]);
  TIP.keyup(keyA);
  checkAllTIPModifiers(testDesc + ", \"a\" keyup (after Virtual-Shift keyup)", [ "AltGraph" ]);
  TIP.keyup(altGrVirtual);
  checkAllTIPModifiers(testDesc + ", Virtual-AltGraph keyup", [ ]);

  is(events.length, 10,
     description + testDesc + " should cause 10 events");
  checkModifiers(testDesc, events[0], "keydown",  "Shift",    "",     [ "Shift" ]);
  checkModifiers(testDesc, events[1], "keydown",  "AltGraph", "",     [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[2], "keydown",  "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[3], "keypress", "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[4], "keyup",    "a",        "KeyA", [ "Shift", "AltGraph" ]);
  checkModifiers(testDesc, events[5], "keyup",    "Shift",    "",     [ "AltGraph" ]);
  checkModifiers(testDesc, events[6], "keydown",  "a",        "KeyA", [ "AltGraph" ]);
  checkModifiers(testDesc, events[7], "keypress", "a",        "KeyA", [ "AltGraph" ]);
  checkModifiers(testDesc, events[8], "keyup",    "a",        "KeyA", [ "AltGraph" ]);
  checkModifiers(testDesc, events[9], "keyup",    "AltGraph", "",     [ ]);

  // shareModifierStateOf(null) should cause resetting the modifier state
  function checkTIPModifiers(aTestDesc, aTIP, aModifiers)
  {
    /* eslint-disable-next-line no-shadow */
    for (var i = 0; i < kModifiers.length; i++) {
      is(aTIP.getModifierState(kModifiers[i]), aModifiers.includes(kModifiers[i]),
         description + aTestDesc + ", aTIP.getModifierState(\"" + kModifiers[i] + "\") returns wrong value");
    }
  }
  TIP.keydown(shiftVirtual);
  TIP.keydown(altGrVirtual);
  sharedTIP.shareModifierStateOf(null);
  checkTIPModifiers("sharedTIP.sharedModifierStateOf(null) shouldn't cause TIP's modifiers reset", TIP, [ "Shift", "AltGraph" ]);
  checkTIPModifiers("sharedTIP.sharedModifierStateOf(null) should cause sharedTIP modifiers reset", sharedTIP, [ ]);

  // sharedTIP.shareModifierStateOf(null) should be unlinked from TIP.
  TIP.keydown(ctrlVirtual);
  checkTIPModifiers("TIP.keydown(ctrlVirtual) should cause TIP's modifiers set", TIP, [ "Shift", "AltGraph", "Control" ]);
  checkTIPModifiers("TIP.keydown(ctrlVirtual) shouldn't cause sharedTIP modifiers set", sharedTIP, [ ]);

  // beginInputTransactionForTests() shouldn't cause modifier state reset.
  ok(TIP.beginInputTransactionForTests(otherWindow),
     description + "TIP.beginInputTransactionForTests(otherWindow) should return true");
  checkTIPModifiers("TIP.beginInputTransactionForTests(otherWindow) shouldn't cause TIP's modifiers set", TIP, [ "Shift", "AltGraph", "Control" ]);
  TIP.keyup(shiftLeft);
  TIP.keyup(altGrVirtual);
  TIP.keyup(ctrlVirtual);
  checkTIPModifiers("TIP should keep modifier's physical key state", TIP, [ "Shift" ]);
  ok(TIP.beginInputTransactionForTests(window),
     description + "TIP.beginInputTransactionForTests(window) should return true");
  checkTIPModifiers("TIP.beginInputTransactionForTests(window) shouldn't cause TIP's modifiers set", TIP, [ "Shift" ]);
  TIP.keyup(shiftVirtual);
  checkTIPModifiers("TIP should keep modifier's physical key state", TIP, [ ]);

  window.removeEventListener("keydown", handler);
  window.removeEventListener("keypress", handler);
  window.removeEventListener("keyup", handler);
}

function runInsertTextWithKeyPressTests() {
  const description = "runInsertTextWithKeyPressTests(): ";

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

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

  let events = [];
  function handler(aEvent) {
    events.push({
      type: aEvent.type,
      key: aEvent.key,
      code: aEvent.code,
      shiftKey: aEvent.shiftKey,
      altKey: aEvent.altKey,
      ctrlKey: aEvent.ctrlKey,
      metaKey: aEvent.metaKey,
    });
  }
  function stringifyEvents(aEvents) {
    if (!aEvents.length) {
      return "[]";
    }
    function stringifyEvent(aEvent) {
      return `{ type: "${aEvent.type}", key: "${aEvent.key}", code: ${aEvent.code}, shiftKey: ${
          aEvent.shiftKey
        }, altKey: ${aEvent.altKey}, ctrlKey: ${aEvent.ctrlKey}, metaKey: ${aEvent.metaKey}}`;
    }
    let result = "";
    for (const event of aEvents) {
      if (result == "") {
        result = "[ ";
      } else {
        result += ", ";
      }
      result += stringifyEvent(event);
    }
    return result + " ]";
  }
  window.addEventListener("keydown", handler);
  window.addEventListener("keypress", handler);
  window.addEventListener("keyup", handler);

  events = [];
  input.value = "";
  TIP.insertTextWithKeyPress("X");
  is(
    input.value,
    "X",
    `${description}insertTextWithKeyPress without optional args should cause inserting the string`
  );
  is(
    stringifyEvents(events),
    stringifyEvents([
      { type: "keypress", key: "X", code: "", shiftKey: false, altKey: false, ctrlKey: false, metaKey: false },
    ]),
    `${description}insertTextWithPress without optional args should cause only a "keypress" event`
  );

  events = [];
  input.value = "";
  TIP.insertTextWithKeyPress("Y", new KeyboardEvent("keydown", {key: "Alt", code: "AltLeft"}));
  is(
    input.value,
    "Y",
    `${description}insertTextWithKeyPress with Alt keydown event should cause inserting the string`
  );
  // The key value should be the inserted string and the code value should be same as the source event's.
  is(
    stringifyEvents(events),
    stringifyEvents([
      { type: "keypress", key: "Y", code: "AltLeft", shiftKey: false, altKey: false, ctrlKey: false, metaKey: false },
    ]),
    `${description}insertTextWithKeyPress with Alt keydown event should cause only a "keypress" event whose code is "AltLeft"`
  );

  events = [];
  input.value = "";
  TIP.insertTextWithKeyPress("Z", new KeyboardEvent("keydown", {key: "Alt", code: "AltLeft", altKey: true}));
  is(
    input.value,
    "Z",
    `${description}insertTextWithKeyPress with Alt keydown whose altKey is true should cause inserting the string`
  );
  // TIP should use its internal modifier state instead of specified modifier state.
  is(
    stringifyEvents(events),
    stringifyEvents([
      { type: "keypress", key: "Z", code: "AltLeft", shiftKey: false, altKey: false, ctrlKey: false, metaKey: false },
    ]),
    `${description}insertTextWithKeyPress with Alt keydown whose altKey is true should cause only a "keypress" event whose altKey is false"`
  );

  TIP.keydown(new KeyboardEvent("keydown", { key: "Alt" }));
  events = [];
  input.value = "";
  TIP.insertTextWithKeyPress("X", new KeyboardEvent("keydown", {key: "Shift", code: "ShiftLeft", shiftKey: true}));
  is(
    input.value,
    kIsMac ? "X" : "",
    `${description}insertTextWithKeyPress after Alt keydown should${kIsMac ? "" : " not"} cause inserting the string`
  );
  is(
    stringifyEvents(events),
    stringifyEvents([
      { type: "keypress", key: "X", code: "ShiftLeft", shiftKey: false, altKey: true, ctrlKey: false, metaKey: false },
    ]),
    `${description}insertTextWithPress after Alt keydown should cause only a "keypress" event whose altKey is true"`
  );
  TIP.keyup(new KeyboardEvent("keyup ", { key: "Alt" }));

  window.removeEventListener("keydown", handler);
  window.removeEventListener("keypress", handler);
  window.removeEventListener("keyup", handler);
}

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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let 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 {
    let keyEvent = new KeyboardEvent("", { key: "Enter", code: "ShiftLeft" });
    TIP.keyup(keyEvent, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
    ok(false,
       description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should fail if the .key value isn't a modifier key");
  } catch (e) {
    ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
       description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should cause NS_ERROR_ILLEGAL_VALUE if the .key value isn't a modifier key");
  }

  // The type of key events specified to composition methods should be "" or "keydown".
  var kKeyEventTypes = [
    { type: "keydown",   valid: true },
    { type: "keypress",  valid: false },
    { type: "keyup",     valid: false },
    { type: "",          valid: true },
    { type: "mousedown", valid: false },
    { type: "foo",       valid: false },
  ];
  for (var i = 0; i < kKeyEventTypes[i].length; i++) {
    var keyEvent =
      new KeyboardEvent(kKeyEventTypes[i].type, { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
    var testDescription = description + "type=\"" + kKeyEventTypes[i].type + "\", ";
    try {
      TIP.startComposition(keyEvent);
      ok(kKeyEventTypes[i].valid,
         testDescription + "TIP.startComposition(keyEvent) should not accept the event type");
      TIP.cancelComposition();
    } catch (e) {
      ok(!kKeyEventTypes[i].valid,
         testDescription + "TIP.startComposition(keyEvent) should not throw an exception for the event type");
      ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
         testDescription + "TIP.startComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid");
    }
    try {
      TIP.setPendingCompositionString("foo");
      TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
      TIP.setCaretInPendingComposition(3);
      TIP.flushPendingComposition(keyEvent);
      ok(kKeyEventTypes[i].valid,
         testDescription + "TIP.flushPendingComposition(keyEvent) should not accept the event type");
      TIP.cancelComposition();
    } catch (e) {
      ok(!kKeyEventTypes[i].valid,
         testDescription + "TIP.flushPendingComposition(keyEvent) should not throw an exception for the event type");
      ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
         testDescription + "TIP.flushPendingComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid");
    }
    try {
      TIP.startComposition();
      TIP.commitComposition(keyEvent);
      ok(kKeyEventTypes[i].valid,
         testDescription + "TIP.commitComposition(keyEvent) should not accept the event type");
    } catch (e) {
      ok(!kKeyEventTypes[i].valid,
         testDescription + "TIP.commitComposition(keyEvent) should not throw an exception for the event type");
      ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
         testDescription + "TIP.commitComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid");
      TIP.cancelComposition();
    }
    try {
      TIP.commitCompositionWith("foo", keyEvent);
      ok(kKeyEventTypes[i].valid,
         testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should not accept the event type");
    } catch (e) {
      ok(!kKeyEventTypes[i].valid,
         testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should not throw an exception for the event type");
      ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
         testDescription + "TIP.commitCompositionWith(\"foo\", keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid");
    }
    try {
      TIP.startComposition();
      TIP.cancelComposition(keyEvent);
      ok(kKeyEventTypes[i].valid,
         testDescription + "TIP.cancelComposition(keyEvent) should not accept the event type");
    } catch (e) {
      ok(!kKeyEventTypes[i].valid,
         testDescription + "TIP.cancelComposition(keyEvent) should not throw an exception for the event type");
      ok(e.message.includes("NS_ERROR_ILLEGAL_VALUE"),
         testDescription + "TIP.cancelComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid");
      TIP.cancelComposition();
    }
    input.value = "";
  }
}

function runCommitCompositionTests()
{
  var description = "runCommitCompositionTests(): ";

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

  input.focus();

  // commitComposition() should commit the composition with the last data.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  TIP.commitComposition();
  is(input.value, "foo",
     description + "commitComposition() should commit the composition with the last data");

  // commitCompositionWith("") should commit the composition with empty string.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  TIP.commitCompositionWith("");
  is(input.value, "",
     description + "commitCompositionWith(\"\") should commit the composition with empty string");

  function doCommit(aText)
  {
    TIP.commitCompositionWith(aText);
  }

  // doCommit() should commit the composition with the last data.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommit();
  todo_is(input.value, "foo",
          description + "doCommit() should commit the composition with the last data");

  // doCommit("") should commit the composition with empty string.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommit("");
  is(input.value, "",
     description + "doCommit(\"\") should commit the composition with empty string");

  // doCommit(null) should commit the composition with empty string.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommit(null);
  is(input.value, "",
     description + "doCommit(null) should commit the composition with empty string");

  // doCommit(undefined) should commit the composition with the last data.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommit(undefined);
  todo_is(input.value, "foo",
          description + "doCommit(undefined) should commit the composition with the last data");

  function doCommitWithNullCheck(aText)
  {
    TIP.commitCompositionWith(aText ? aText : "");
  }

  // doCommitWithNullCheck() should commit the composition with the last data.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommitWithNullCheck();
  is(input.value, "",
     description + "doCommitWithNullCheck() should commit the composition with empty string");

  // doCommitWithNullCheck("") should commit the composition with empty string.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommitWithNullCheck("");
  is(input.value, "",
     description + "doCommitWithNullCheck(\"\") should commit the composition with empty string");

  // doCommitWithNullCheck(null) should commit the composition with empty string.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommitWithNullCheck(null);
  is(input.value, "",
     description + "doCommitWithNullCheck(null) should commit the composition with empty string");

  // doCommitWithNullCheck(undefined) should commit the composition with the last data.
  input.value = "";
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.setCaretInPendingComposition(3);
  TIP.flushPendingComposition();
  doCommitWithNullCheck(undefined);
  is(input.value, "",
     description + "doCommitWithNullCheck(undefined) should commit the composition with empty string");
}

function runUnloadTests1()
{
  return new Promise(resolve => {
    let description = "runUnloadTests1(): ";

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

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

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

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

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

      TIP2.setPendingCompositionString("foo");
      TIP2.appendClauseToPendingComposition(3, TIP2.ATTR_RAW_CLAUSE);
      TIP2.setCaretInPendingComposition(3);
      TIP2.flushPendingComposition();
      is(input.value, "foo",
         description + "the input in the parent document should have composition string");

      TIP2.cancelComposition();

      // Restore the old iframe content.
      iframe.src = oldSrc;
    }, {capture: true, once: true});

    // Start composition in the iframe.
    textareaInFrame.value = "";
    textareaInFrame.focus();

    TIP1.setPendingCompositionString("foo");
    TIP1.appendClauseToPendingComposition(3, TIP1.ATTR_RAW_CLAUSE);
    TIP1.setCaretInPendingComposition(3);
    TIP1.flushPendingComposition();
    is(textareaInFrame.value, "foo",
       description + "the textarea in the iframe should have composition string");

    // Load different web page on the frame.
    iframe.src = "data:text/html,<body>dummy page</body>";
  });
}

function runUnloadTests2()
{
  return new Promise(resolve => {
    let description = "runUnloadTests2(): ";

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

    let oldSrc = iframe.src;

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

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

      // TIP should be still available in the same top level widget.
      TIP.setPendingCompositionString("bar");
      TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
      TIP.setCaretInPendingComposition(3);
      TIP.flushPendingComposition();
      if (input.value == "") {
        // XXX TextInputProcessor or TextEventDispatcher may have a bug.
        todo_is(input.value, "bar",
                description + "the input in the parent document should have composition string");
      } else {
        is(input.value, "bar",
           description + "the input in the parent document should have composition string");
      }

      TIP.cancelComposition();

      // Restore the old iframe content.
      iframe.src = oldSrc;
    }, {capture: true, once: true});

    // Start composition in the iframe.
    textareaInFrame.value = "";
    textareaInFrame.focus();

    TIP.setPendingCompositionString("foo");
    TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
    TIP.setCaretInPendingComposition(3);
    TIP.flushPendingComposition();
    is(textareaInFrame.value, "foo",
       description + "the textarea in the iframe should have composition string");

    // Load different web page on the frame.
    iframe.src = "data:text/html,<body>dummy page</body>";
  });
}

async function runCallbackTests(aForTests)
{
  let description = "runCallbackTests(aForTests=" + aForTests + "): ";

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

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

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

  function waitUntilNotificationsReceived()
  {
    return new Promise(resolve => {
      if (notifications.length) {
        SimpleTest.executeSoon(resolve);
      } else {
        waitingNextNotification = resolve;
      }
    });
  }

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

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

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

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

  notifications = [];
  input.focus();
  is(notifications.length, 1,
     description + "input.focus() should cause a notification");
  is(notifications[0].type, "notify-focus",
     description + "input.focus() should cause \"notify-focus\"");
  dumpUnexpectedNotifications(1);

  notifications = [];
  input.blur();
  is(notifications.length, 1,
     description + "input.blur() should cause a notification");
  is(notifications[0].type, "notify-blur",
     description + "input.blur() should cause \"notify-focus\"");
  dumpUnexpectedNotifications(1);

  input.focus();
  await waitUntilNotificationsReceived();
  notifications = [];
  TIP.setPendingCompositionString("foo");
  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
  TIP.flushPendingComposition();
  is(notifications.length, 3,
     description + "creating composition string 'foo' should cause 3 notifications");
  checkTextChangeNotification(notifications[0], description + "creating composition string 'foo'",
                              { offset: 0, removedLength: 0, addedLength: 3,
                                causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
  checkSelectionChangeNotification(notifications[1], description + "creating composition string 'foo'",
                                   { offset: 3, text: "", causedByComposition: true, occurredDuringComposition: true });
  checkPositionChangeNotification(notifications[2], description + "creating composition string 'foo'");
  dumpUnexpectedNotifications(3);

  notifications = [];
  synthesizeMouseAtCenter(input, {});
  is(notifications.length, 3,
     description + "synthesizeMouseAtCenter(input, {}) during composition should cause 3 notifications");
  is(notifications[0].type, "request-to-commit",
     description + "synthesizeMouseAtCenter(input, {}) during composition should cause \"request-to-commit\"");
  checkTextChangeNotification(notifications[1], description + "synthesizeMouseAtCenter(input, {}) during composition",
                              { offset: 0, removedLength: 3, addedLength: 3,
                                causedOnlyByComposition: true, includingChangesDuringComposition: false, includingChangesWithoutComposition: false});
  checkPositionChangeNotification(notifications[2], description + "synthesizeMouseAtCenter(input, {}) during composition");
  dumpUnexpectedNotifications(3);

  input.focus();
  await waitUntilNotificationsReceived();
  notifications = [];
  // XXX On macOS, window.moveBy() doesn't cause notify-position-change.
  //     Investigate this later (although, we cannot notify position change to
  //     native IME on macOS).
  // Wayland also does not support it.
  var isWayland = Services.prefs.getBoolPref("widget.wayland.test-workarounds.enabled", false);
  if (!kIsMac && !isWayland) {
    window.moveBy(0, 10);
    await waitUntilNotificationsReceived();
    is(notifications.length, 1,
       description + "window.moveBy(0, 10) should cause a notification");
    checkPositionChangeNotification(notifications[0], description + "window.moveBy(0, 10)");
    dumpUnexpectedNotifications(1);

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

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

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

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

  notifications = [];
  input.editor.selection.removeAllRanges();
  await waitUntilNotificationsReceived();
  is(notifications.length, 1,
    `${description}Removing all selection ranges should cause a selection change notification`);
  checkSelectionChangeNotification(
    notifications[0],
    `${description}Removing all selection ranges in editor`,
    { hasRange: false }
  );
  dumpUnexpectedNotifications(1);

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

async function runFocusNotificationTestAfterDrop() {
  const inputs = document.querySelectorAll("input[type=text]");
  inputs[0].value = "abc";
  inputs[1].value = "";

  const TIP = createTIP();
  let notifications = [];
  function callback(aTIP, aNotification)
  {
    if (aTIP != TIP) {
      return true;
    }
    switch (aNotification.type) {
      case "request-to-commit":
        aTIP.commitComposition();
        break;
      case "request-to-cancel":
        aTIP.cancelComposition();
        break;
      case "notify-focus":
      case "notify-blur":
        notifications.push(aNotification.type);
        break;
    }
    return true;
  }

  inputs[0].focus();
  TIP.beginInputTransactionForTests(window, callback);
  inputs[0].select();
  try {
    notifications = [];
    await synthesizePlainDragAndDrop({
      srcSelection: SpecialPowers.wrap(inputs[0]).editor.selection,
      destElement: inputs[1],
    });
  } catch (ex) {
    ok(false, `runFocusNotificationTestAfterDrop: unexpected error during DnD (${ex.message})`);
    return;
  }
  is(
    document.activeElement,
    inputs[1],
    "runFocusNotificationTestAfterDrop: Dropping to the second <input> should make it focused"
  );
  ok(
    notifications.length > 1,
    "runFocusNotificationTestAfterDrop: At least two notifications should be fired"
  );
  if (notifications.length) {
    is(
      notifications[notifications.length - 1],
      "notify-focus",
      "runFocusNotificationTestAfterDrop: focus notification should've been fired at last"
    );
  }
}

async function runQuerySelectionEventTestAtTextChangeNotification() {
  contenteditable.innerHTML = "<p>abc</p><p>def</p>";
  contenteditable.focus();
  // Ensure to send notify-focus from IMEContentObserver
  await new Promise(
    resolve => requestAnimationFrame(
      () => requestAnimationFrame(resolve)
    )
  );
  document.execCommand("selectall");
  // Ensure to send notify-selection-change from IMEContentObserver
  await new Promise(
    resolve => requestAnimationFrame(
      () => requestAnimationFrame(resolve)
    )
  );

  const kTestName = "runQuerySelectionEventTestAtTextChangeNotification";
  await new Promise(resolve => {
    const TIP = createTIP();
    TIP.beginInputTransactionForTests(window, (aTIP, aNotification) => {
      if (aTIP != TIP) {
        return true;
      }
      switch (aNotification.type) {
        case "request-to-commit":
          aTIP.commitComposition();
          break;
        case "request-to-cancel":
          aTIP.cancelComposition();
          break;
        case "notify-text-change":
          const textContent = synthesizeQueryTextContent(0, 100);
          if (textContent?.text.includes("abc")) {
            break;  // Different notification which we want to test, wait next one.
          }
          ok(
            textContent?.succeeded,
            `${kTestName}: query text content should succeed from notify-text-change handler`
          );
          const selectedText = synthesizeQuerySelectedText();
          ok(
            selectedText?.succeeded,
            `${kTestName}: query selected text should succeed from notify-text-change handler`
          );
          if (textContent?.succeeded && selectedText?.succeeded) {
            is(
              selectedText.text,
              textContent.text,
              `${kTestName}: selected text should be same as all text`
            );
          }
          resolve();
          break;
      }
      return true;
    });
    // TODO: We want to do this  while selection is batched but can flush
    //       pending notifications, however, I have no idea how to do it.
    contenteditable.firstChild.remove();
  });
}

async function runIMEStateUpdateTests() {
  const TIP = createTIP();
  let notifications = [];
  function callback(aTIP, aNotification)
  {
    if (aTIP != TIP) {
      return true;
    }
    switch (aNotification.type) {
      case "request-to-commit":
        aTIP.commitComposition();
        break;
      case "request-to-cancel":
        aTIP.cancelComposition();
        break;
      case "notify-focus":
      case "notify-blur":
        notifications.push(aNotification.type);
        break;
    }
    return true;
  }

  contenteditable.focus();
  TIP.beginInputTransactionForTests(window, callback);
  await new Promise(resolve => requestAnimationFrame(() =>
    requestAnimationFrame(resolve)
  )); // wait for flushing pending notifications if there is.

  // run IMEStateManager::UpdateIMEState to disable IME
  notifications = [];
  const editor = getHTMLEditor(window);
  editor.flags |= Ci.nsIEditor.eEditorReadonlyMask;
  await new Promise(resolve => requestAnimationFrame(() =>
    requestAnimationFrame(resolve)
  )); // wait for flush pending notification even if handled asynchronously.
  is(
    notifications.length ? notifications[0] : undefined,
    "notify-blur",
    "runIMEStateUpdateTests: Making the HTMLEditor readonly should cause a blur notification"
  );
  is(
    notifications.length,
    1,
    `runIMEStateUpdateTests: Making the HTMLEditor readonly should not cause any other notifications, but got ${
      notifications.length > 1 ? notifications[1] : ""
    } notification`
  );
  is(
    SpecialPowers.getDOMWindowUtils(window)?.IMEStatus,
    Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
    `runIMEStateUpdateTests: Making the HTMLEditor readonly should make IME disabled`
  );

  // run IMEStateManager::UpdateIMEState to enable IME
  notifications = [];
  editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask;
  await new Promise(resolve => requestAnimationFrame(() =>
    requestAnimationFrame(resolve)
  )); // wait for flush pending notification even if handled asynchronously.
  is(
    notifications.length ? notifications[0] : undefined,
    "notify-focus",
    "runIMEStateUpdateTests: Making the HTMLEditor editable should cause a focus notification without blur notification"
  );
  is(
    notifications.length,
    1,
    `runIMEStateUpdateTests: Making the HTMLEditor editable should not cause any other notifications, but got ${
      notifications.length > 1 ? notifications[1] : ""
    } notification`
  );
  is(
    SpecialPowers.getDOMWindowUtils(window)?.IMEStatus,
    Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
    `runIMEStateUpdateTests: Making the HTMLEditor readonly should make IME disabled`
  );
}

async function runTextNotificationChangesDuringNoFrame() {
  const TIP = createTIP();
  let onTextChange;
  function callback(aTIP, aNotification)
  {
    if (aTIP != TIP) {
      return true;
    }
    switch (aNotification.type) {
      case "request-to-commit":
        aTIP.commitComposition();
        break;
      case "request-to-cancel":
        aTIP.cancelComposition();
        break;
      case "notify-text-change":
        if (onTextChange) {
          onTextChange(aNotification);
        }
        break;
    }
    return true;
  }

  function promiseTextChangeNotification() {
    return new Promise(resolve => onTextChange = resolve);
  }

  function waitForTick() {
    return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
  }

  input = document.querySelector("input[type=text]");
  input.focus();
  TIP.beginInputTransactionForTests(window, callback);

  await (async function test_text_change_notification_for_value_set_during_no_frame() {
    const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_value_set_during_no_frame";
    input.value = "Start";
    input.style.display = "inline";
    input.getBoundingClientRect();
    await waitForTick();
    const waitNotifications = promiseTextChangeNotification();
    input.style.display = "block";
    input.value = "Changed";
    info(`${description}: waiting for notifications...`);
    const notification = await waitNotifications;
    is(
      notification?.offset,
      0,
      `${description}: offset should be 0`
    );
    is(
      notification?.removedLength,
      "Start".length,
      `${description}: removedLength should be the length of the old value`
    );
    is(
      notification?.addedLength,
      "Changed".length,
      `${description}: addedLength should be the length of the new value`
    );
  })();

  await (async function test_text_change_notification_for_multiple_value_set_during_no_frame() {
    const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_multiple_value_set_during_no_frame";
    input.value = "Start";
    input.style.display = "inline";
    input.getBoundingClientRect();
    await waitForTick();
    const waitNotifications = promiseTextChangeNotification();
    input.style.display = "block";
    input.value = "Changed";
    input.value = "Again!";
    info(`${description}: waiting for notifications...`);
    const notification = await waitNotifications;
    is(
      notification?.offset,
      0,
      `${description}: offset should be 0`
    );
    is(
      notification?.removedLength,
      "Start".length,
      `${description}: removedLength should be the length of the old value`
    );
    is(
      notification?.addedLength,
      "Again!".length,
      `${description}: addedLength should be the length of the new value`
    );
  })();

  await (async function test_text_change_notification_for_value_set_and_typing_character_during_no_frame() {
    const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_value_set_and_typing_character_during_no_frame";
    input.value = "Start";
    input.style.display = "inline";
    input.getBoundingClientRect();
    await waitForTick();
    const waitNotifications = promiseTextChangeNotification();
    input.style.display = "block";
    input.value = "Change";
    const dKey = new KeyboardEvent("", { code: "KeyD", key: "d", keyCode: KeyboardEvent.DOM_VK_D });
    TIP.keydown(dKey);
    TIP.keyup(dKey);
    info(`${description}: waiting for notifications...`);
    const notification = await waitNotifications;
    is(
      notification?.offset,
      0,
      `${description}: offset should be 0`
    );
    is(
      notification?.removedLength,
      "Start".length,
      `${description}: removedLength should be the length of the old value`
    );
    is(
      notification?.addedLength,
      "Change".length,
      `${description}: addedLength should be the length of the new (set) value`
    );
  })();

  input.style.display = "";

  textarea.focus();
  TIP.beginInputTransaction(window, callback);

  await (async function test_text_change_notification_for_multi_line_value_set_during_no_frame() {
    const description = "runTextNotificationChangesDuringNoFrame: test_text_change_notification_for_multi_line_value_set_during_no_frame";
    textarea.value = "Start\n2nd Line";
    textarea.style.display = "inline";
    textarea.getBoundingClientRect();
    await waitForTick();
    const waitNotifications = promiseTextChangeNotification();
    textarea.style.display = "block";
    textarea.value = "Changed\n2nd Line";
    info(`${description}: waiting for notifications...`);
    const notification = await waitNotifications;
    is(
      notification?.offset,
      0,
      `${description}: offset should be 0`
    );
    is(
      notification?.removedLength,
      getNativeText("Start\n2nd Line").length,
      `${description}: removedLength should be the length of the old value`
    );
    is(
      notification?.addedLength,
      getNativeText("Changed\n2nd Line").length,
      `${description}: addedLength should be the length of the new value`
    );
  })();

  textarea.style.display = "";
}

async function runTests()
{
  await SpecialPowers.pushPrefEnv({
    set: [["test.ime_content_observer.assert_invalid_cache", true]],
  });

  textareaInFrame = iframe.contentDocument.getElementById("textarea");
  runBeginInputTransactionMethodTests();
  runReleaseTests();
  runCompositionTests();
  runCompositionWithKeyEventTests();
  runConsumingKeydownBeforeCompositionTests();
  await runKeyTests();
  runInsertTextWithKeyPressTests();
  runErrorTests();
  runCommitCompositionTests();
  await runCallbackTests(false);
  await runCallbackTests(true);
  await runTextNotificationChangesDuringNoFrame();
  await runFocusNotificationTestAfterDrop();
  await runUnloadTests1();
  await runUnloadTests2();
  await runQuerySelectionEventTestAtTextChangeNotification();
  await runIMEStateUpdateTests();

  finish();
}

]]>
</script>

</window>

[zur Elbe Produktseite wechseln0.207QuellennavigatorsAnalyse erneut starten2026-04-29]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge